diff --git a/CHANGELOG.md b/CHANGELOG.md index f449a7e83..0a30b4e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# v0.9.34 +## 08/04/2015 + +1. [](#new) + * Added new `cache_all` system setting + media `cache()` method + * Added base languages configuration + * Added property language to page to help plugins identify page language + * New `Utils::arrayFilterRecursive()` method +2. [](#improved) + * Improved Session handling to support site and admin independently + * Allow Twig variables to be modified in other events + * Blueprint updates in preparation for Admin plugin + * Changed `Inflector` from static to object and added multi-language support + * Support for admin override of a page's blueprints +3. [](#bugfix) + * Removed unused `use` in `VideoMedium` that was causing error + * Array fix in `User.authorise()` method + * Fix for typo in `translations_fallback` + * Fixed moving page to the root + # v0.9.33 ## 07/21/2015 diff --git a/composer.json b/composer.json index 2203028c0..775a77125 100644 --- a/composer.json +++ b/composer.json @@ -9,9 +9,9 @@ "php": ">=5.4.0", "twig/twig": "~1.16", "erusev/parsedown-extra": "~0.7", - "symfony/yaml": "2.7.*", - "symfony/console": "2.7.*", - "symfony/event-dispatcher": "2.7.*", + "symfony/yaml": "2.7.3", + "symfony/console": "2.7.3", + "symfony/event-dispatcher": "2.7.3", "doctrine/cache": "~1.4", "maximebf/debugbar": "dev-master", "filp/whoops": "1.2.*@dev", diff --git a/system/blueprints/config/site.yaml b/system/blueprints/config/site.yaml index 604be106d..9c74d59d0 100644 --- a/system/blueprints/config/site.yaml +++ b/system/blueprints/config/site.yaml @@ -13,60 +13,101 @@ form: label: Site Title size: large placeholder: "Site wide title" - help: Default title for your site + help: "Default title for your site, often used in themes" author.name: type: text size: large label: Default Author + help: "A default author name, often used in themes or page content" author.email: type: text size: large label: Default Email + help: "A default email to reference in themes or pages" validate: type: email taxonomies: - type: text + type: selectize size: large label: Taxonomy Types classes: fancy + help: "Taxonomy types must be defined here if you wish to use them in pages" validate: type: commalist - metadata: - type: array - label: Metadata - placeholder_key: Name - placeholder_value: Content - - blog: + summary: type: section - title: Blog + title: Page Summary fields: - blog.route: - type: text - size: large - label: Blog URL + summary.enabled: + type: toggle + label: Enabled + highlight: 1 + help: "Enable page summary (the summary returns the same as the page content)" + options: + 1: Yes + 0: No + validate: + type: bool summary.size: type: text size: x-small label: Summary Size + help: "The amount of characters of a page to use as a content summary" validate: type: int min: 0 max: 65536 - routes: + summary.format: + type: toggle + label: Format + classes: fancy + help: "short = use the first occurrence of delimiter or size; long = summary delimiter will be ignored" + highlight: short + options: + 'short': 'Short' + 'long': 'Long' + + summary.delimiter: + type: text + size: x-small + label: Delimiter + help: "The summary delimiter (default '===')" + + metadata: type: section - title: Routes + title: Metadata fields: + metadata: + type: array + label: Metadata + help: "Default metadata values that will be displayed on every page unless overridden by the page" + placeholder_key: Name + placeholder_value: Content + + + routes: + type: section + title: Redirects & Routes + + fields: + redirects: + type: array + label: Custom Redirects + help: "routes to redirect to other pages. Standard Regex replacement is valid" + placeholder_key: /your/alias + placeholder_value: /your/redirect + routes: type: array - label: Custom + label: Custom Routes + help: "routes to alias to other pages. Standard Regex replacement is valid" placeholder_key: /your/alias placeholder_value: /your/route diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index 071a5c1b2..ab1fead2c 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -14,7 +14,7 @@ form: type: pages size: medium classes: fancy - label: Home Page + label: Home page show_all: false show_modular: false show_root: false @@ -23,61 +23,65 @@ form: pages.theme: type: themeselect classes: fancy + selectize: true size: medium - label: Default Theme - help: "Set the theme (defaults to 'default')" - - pages.markdown.extra: - type: toggle - label: Markdown Extra - highlight: 1 - options: - 1: Yes - 0: No - validate: - type: bool + label: Default theme + help: "Set the default theme for Grav to use (default is Antimatter)" pages.process: type: checkboxes label: Process + help: "Control how pages are processed. Can be set per-page rather than globally" default: [markdown: true, twig: true] options: markdown: Markdown twig: Twig use: keys + timezone: + type: select + label: Timezone + size: medium + classes: fancy + help: "Override the default timezone the server" + @data-options: '\Grav\Common\Utils::timezones' + default: '' + options: + '': 'Default (Server Timezone)' + pages.dateformat.short: type: select size: medium classes: fancy - label: Short Date Format - help: "Set the short date format" - default: 'jS M Y' + label: Short date format + help: "Set the short date format that can be used by themes" + default: "jS M Y" options: - 'F jS \\a\\t g:ia': "January 1st at 11:59pm" - 'l jS of F g:i A': "Monday 1st of January at 11:59 PM" - 'D, m M Y G:i:s': "Mon, 01 Jan 2014 23:59:00" - 'd-m-y G:i': "01-01-14 23:59" - 'jS M Y': "10th Feb 2014" + "F jS \\a\\t g:ia": "January 1st at 11:59pm" + "l jS of F g:i A": "Monday 1st of January at 11:59 PM" + "D, m M Y G:i:s": "Mon, 01 Jan 2014 23:59:00" + "d-m-y G:i": "01-01-14 23:59" + "jS M Y": "10th Feb 2014" pages.dateformat.long: type: select size: medium classes: fancy - label: Long Date Format - help: "Set the long date format" + label: Long date format + help: "Set the long date format that can be used by themes" options: - 'F jS \a\t g:ia': "January 1st at 11:59pm" - 'l jS of F g:i A': "Monday 1st of January at 11:59 PM" - 'D, m M Y G:i:s': "Mon, 01 Jan 2014 23:59:00" - 'd-m-y G:i': "01-01-14 23:59" - 'jS M Y': "10th Feb 2014" + "F jS \\a\\t g:ia": "January 1st at 11:59pm" + "l jS of F g:i A": "Monday 1st of January at 11:59 PM" + "D, m M Y G:i:s": "Mon, 01 Jan 2014 23:59:00" + "d-m-y G:i": "01-01-14 23:59" + "jS M Y": "10th Feb 2014" pages.order.by: type: select size: medium classes: fancy - label: Default Ordering + label: Default ordering + help: "Pages in a list will render using this order unless it is overridden" options: default: Default - based on folder name folder: Folder - based on prefix-less folder name @@ -86,9 +90,10 @@ form: pages.order.dir: type: toggle - label: Default Order Direction + label: Default order direction highlight: asc default: desc + help: "The direction of pages in a list" options: asc: Ascending desc: Descending @@ -96,15 +101,16 @@ form: pages.list.count: type: text size: x-small - label: Default Item Count - help: "Default max pages count" + label: Default page count + help: "Default maximum pages count in a list" validate: type: number min: 1 + pages.publish_dates: type: toggle label: Date-based publishing - help: Automatically (un)publish posts based on their date + help: "Automatically (un)publish posts based on their date" highlight: 1 options: 1: Yes @@ -112,17 +118,47 @@ form: validate: type: bool + pages.events: + type: checkboxes + label: Events + help: "Enable or Disable specific events. Disabling these can break plugins" + default: [page: true, twig: true] + options: + page: Page Events + twig: Twig Events + use: keys + pages.redirect_default_route: + type: toggle + label: Redirect default route + help: "Automatically redirect to a page's default route" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool - events: + languages: type: section - title: Events + title: Languages underline: true fields: - pages.events.page: + + languages.supported: + type: selectize + size: large + label: Supported + help: "Comma separated list of 2 letter language codes (for example 'en,fr,de')" + classes: fancy + validate: + type: commalist + + languages.translations: type: toggle - label: Page events + label: Translations enabled + help: "Support translations in Grav, plugins and extensions" highlight: 1 options: 1: Yes @@ -130,9 +166,10 @@ form: validate: type: bool - pages.events.twig: + languages.translations_fallback: type: toggle - label: Twig events + label: Translations fallback + help: "Fallback through supported translations if active language doesn't exist" highlight: 1 options: 1: Yes @@ -140,6 +177,131 @@ form: validate: type: bool + languages.session_store_active: + type: toggle + label: Active language in session + help: "Support translations in Grav, plugins and extensions" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + + languages.home_redirect.include_lang: + type: toggle + label: Home redirect include language + help: "Include language in home redirect (/en)" + highlight: 1 + options: + 1: Yes + 0: No + validate: + type: bool + + languages.home_redirect.include_route: + type: toggle + label: Home redirect include route + help: "Include route in home redirect (/blog)" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + + http_headers: + type: section + title: HTTP Headers + underline: true + + fields: + pages.expires: + type: text + size: small + label: Expires + help: "Sets the expires header. The value is in seconds." + validate: + type: number + min: 1 + pages.last_modified: + type: toggle + label: Last modified + help: "Sets the last modified header that can help optimize proxy and browser caching" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + pages.etag: + type: toggle + label: ETag + help: "Sets the etag header to help identify when a page has been modified" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + pages.vary_accept_encoding: + type: toggle + label: Vary accept encoding + help: "Sets the `Vary: Accept Encoding` header to help with proxy and CDN caching" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + + markdown: + type: section + title: Markdown + underline: true + + fields: + pages.markdown.extra: + type: toggle + label: Markdown extra + help: "Enable default support for Markdown Extra - https://michelf.ca/projects/php-markdown/extra/" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + pages.markdown.auto_line_breaks: + type: toggle + label: Auto line breaks + help: "Enable support for automatic line breaks in markdown" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + pages.markdown.auto_url_links: + type: toggle + label: Auto URL links + help: "Enable automatic conversion of URLs into HTML hyperlinks" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + pages.markdown.escape_markup: + type: toggle + label: Escape markup + help: "Escape markup tags into HTML entities" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + caching: type: section title: Caching @@ -149,6 +311,7 @@ form: cache.enabled: type: toggle label: Caching + help: "Global ON/OFF switch to enable/disable Grav caching" highlight: 1 options: 1: Yes @@ -160,7 +323,8 @@ form: type: select size: small classes: fancy - label: Cache Check Method + label: Cache check method + help: "Select the method that Grav uses to check if page files have been modified." options: file: File folder: Folder @@ -171,6 +335,7 @@ form: size: small classes: fancy label: Cache driver + help: "Choose which cache driver Grav should use. 'Auto Detect' attempts to find the best for you" options: auto: Auto detect file: File @@ -182,38 +347,29 @@ form: cache.prefix: type: text size: x-small - label: Cache Prefix + label: Cache prefix + help: "An identifier for part of the Grav key. Don't change unless you know what your doing." placeholder: "Derived from base URL (override by entering random string)" + cache.lifetime: + type: text + size: small + label: Lifetime + help: "Sets the cache lifetime in seconds. 0 = infinite" + validate: + type: number + cache.gzip: type: toggle - label: GZIP compression - highlight: 1 + label: Gzip compression + help: "Enable GZip compression of the Grav page for increased performance." + highlight: 0 options: 1: Yes 0: No validate: type: bool - assets.enable_asset_timestamp: - type: toggle - label: Enable timestamps on assets - highlight: 1 - options: - 1: Yes - 0: No - validate: - type: bool - - media.enable_media_timestamp: - type: toggle - label: Enable timestamps on media - highlight: 1 - options: - 1: Yes - 0: No - validate: - type: bool twig: type: section @@ -224,6 +380,7 @@ form: twig.cache: type: toggle label: Twig caching + help: "Control the Twig caching mechanism. Leave this enabled for best performance." highlight: 1 options: 1: Yes @@ -234,7 +391,8 @@ form: twig.debug: type: toggle label: Twig debug - highlight: 1 + help: "Allows the option of not loading the Twig Debugger extension" + highlight: 0 options: 1: Yes 0: No @@ -244,6 +402,7 @@ form: twig.auto_reload: type: toggle label: Detect changes + help: "Twig will automatically recompile the Twig cache if it detects any changes in Twig templates" highlight: 1 options: 1: Yes @@ -254,7 +413,8 @@ form: twig.autoescape: type: toggle label: Autoescape variables - highlight: 1 + help: "Autoescapes all variables. This will break your site most likely" + highlight: 0 options: 1: Yes 0: No @@ -269,8 +429,9 @@ form: fields: assets.css_pipeline: type: toggle - label: CSS Pipeline - highlight: 1 + label: CSS pipeline + help: "The CSS pipeline is the unification of multiple CSS resources into one file" + highlight: 0 options: 1: Yes 0: No @@ -279,7 +440,8 @@ form: assets.css_minify: type: toggle - label: CSS Minify + label: CSS minify + help: "Minify the CSS during pipelining" highlight: 1 options: 1: Yes @@ -289,8 +451,9 @@ form: assets.css_minify_windows: type: toggle - label: CSS Minify Windows Override - highlight: 1 + label: CSS minify Windows override + help: "Minify Override for Windows platforms. False by default due to ThreadStackSize" + highlight: 0 options: 1: Yes 0: No @@ -299,7 +462,8 @@ form: assets.css_rewrite: type: toggle - label: CSS Rewrite + label: CSS rewrite + help: "Rewrite any CSS relative URLs during pipelining" highlight: 1 options: 1: Yes @@ -309,8 +473,9 @@ form: assets.js_pipeline: type: toggle - label: JavaScript Pipeline - highlight: 01 + label: JavaScript pipeline + help: "The JS pipeline is the unification of multiple JS resources into one file" + highlight: 0 options: 1: Yes 0: No @@ -319,7 +484,8 @@ form: assets.js_minify: type: toggle - label: JavaScript Minify + label: JavaScript minify + help: "Minify the JS during pipelining" highlight: 1 options: 1: Yes @@ -327,6 +493,23 @@ form: validate: type: bool + assets.enable_asset_timestamp: + type: toggle + label: Enable timestamps on assets + help: "Enable asset timestamps" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + + assets.collections: + type: array + label: Collections + placeholder_key: collection_name + placeholder_value: collection_path + errors: type: section title: Error handler @@ -336,6 +519,7 @@ form: errors.display: type: toggle label: Display errors + help: "Display full backtrace-style error page" highlight: 1 options: 1: Yes @@ -346,6 +530,7 @@ form: errors.log: type: toggle label: Log errors + help: "Log errors to /logs folder" highlight: 1 options: 1: Yes @@ -362,56 +547,18 @@ form: debugger.enabled: type: toggle label: Debugger - highlight: 1 + help: "Enable Grav debugger and following settings" + highlight: 0 options: 1: Yes 0: No validate: type: bool - debugger.mode: - type: select - size: small - classes: fancy - label: Mode - options: - detect: Auto-Detect - development: Development - production: Production - - debugger.strict: + debugger.twig: type: toggle - label: Strict - highlight: 1 - options: - 1: Yes - 0: No - validate: - type: bool - - debugger.max_depth: - type: select - size: small - classes: fancy - label: Detail Level - placeholder: "How many nested levels to display for objects or arrays" - options: - 1: 1 level - 2: 2 levels - 3: 3 levels - 4: 4 levels - 5: 5 levels - 6: 6 levels - 7: 7 levels - 8: 8 levels - 9: 9 levels - 10: 10 levels - validate: - type: number - - debugger.log.enabled: - type: toggle - label: Logging + label: Debug Twig + help: "Enable debugging of Twig templates" highlight: 1 options: 1: Yes @@ -421,7 +568,8 @@ form: debugger.shutdown.close_connection: type: toggle - label: Shutdown Close Connection + label: Shutdown close connection + help: "Close the connection before calling onShutdown(). false for debugging" highlight: 1 options: 1: Yes @@ -438,16 +586,29 @@ form: images.default_image_quality: type: text label: Default image quality + help: "Default image quality to use when resampling or caching images (85%)" classes: x-small validate: type: number min: 1 max: 100 + images.cache_all: + type: toggle + label: Cache all images + help: "Run all images through Grav's cache system even if they have no media manipulations" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + images.debug: type: toggle label: Image debug watermark - highlight: 1 + help: "Show an overlay over images indicating the pixel depth of the image when working with retina for example" + highlight: 0 options: 1: Yes 0: No @@ -457,30 +618,80 @@ form: media.upload_limit: type: text label: File upload limit + help: "Set maximum upload size in bytes (0 is unlimited)" classes: small validate: type: number - system: + media.enable_media_timestamp: + type: toggle + label: Enable timestamps on media + help: "Appends a timestamp based on last modified date to each media item" + highlight: 0 + options: + 1: Yes + 0: No + validate: + type: bool + + session: type: section - title: System + title: Session underline: true fields: - timezone: - type: select - label: Timezone - classes: fancy - @data-options: '\Grav\Common\Utils::timezones' - default: '' + session.enabled: + type: toggle + label: Enabled + help: "Enable session support within Grav" + highlight: 1 options: - '': '- None -' + 1: Yes + 0: No + validate: + type: bool + + session.timeout: + type: text + size: small + label: Timeout + help: "Sets the session timeout in seconds" + validate: + type: number + min: 1 + + session.name: + type: text + size: small + label: Name + help: "An identifier used to form the name of the session cookie" + + + advanced: + type: section + title: Advanced + underline: true + + fields: + absolute_urls: + type: toggle + label: Absolute URLs + highlight: 0 + help: "Absolute or relative URLs for `base_url`" + options: + 1: Yes + 0: No + validate: + type: bool + + param_sep: type: select label: Parameter separator classes: fancy + help: "Separater for passed parameters that can be changed for Apache on Windows" default: '' options: ':': ': (default)' - ';': '; (use this for apache on Windows)' + ';': '; (for Apache running on Windows)' diff --git a/system/blueprints/pages/default.yaml b/system/blueprints/pages/default.yaml new file mode 100644 index 000000000..64a4bd029 --- /dev/null +++ b/system/blueprints/pages/default.yaml @@ -0,0 +1,278 @@ +title: Default + +rules: + slug: + pattern: "[a-z][a-z0-9_\-]+" + min: 2 + max: 80 + +form: + validation: loose + + fields: + type: + type: hidden + label: Page Type + default: default + + tabs: + type: tabs + active: 1 + + fields: + content: + type: tab + title: Content + + fields: + header.title: + type: text + style: vertical + label: Title + validate: + required: true + + content: + type: markdown + label: Content + validate: + type: textarea + + uploads: + type: uploads + label: Page Media + + options: + type: tab + title: Options + + fields: + + publishing: + type: section + title: Publishing + underline: true + + fields: + header.published: + type: toggle + label: Published + help: "By default, a page is published unless you explicitly set published: false or via a publish_date being in the future, or unpublish_date in the past" + highlight: 1 + size: medium + options: + '': Global + 1: Yes + 0: No + validate: + type: bool + + header.date: + type: datetime + label: Date + toggleable: true + help: "The date variable allows you to specifically set a date associated with this page." + + + + header.published_date: + type: datetime + label: Published Date + toggleable: true + help: "Can provide a date to automatically trigger publication." + + header.unpublished_date: + type: datetime + label: Unublished Date + toggleable: true + help: "can provide a date to automatically trigger un-publication." + + + + meta: + type: section + title: Metadata + underline: true + + fields: + header.metadata.description: + type: textarea + toggleable: true + label: Description + default: + validate: + max: 155 + + header.metadata.keywords: + type: text + toggleable: true + label: Keywords + validate: + max: 120 + + header.metadata.author: + type: text + toggleable: true + label: Author + validate: + max: 120 + + header.metadata.robots: + type: checkboxes + toggleable: true + label: Robots + options: + noindex: No index + nofollow: No follow + use: keys + + taxonomies: + type: section + title: Taxonomies + underline: true + + fields: + header.taxonomy: + type: taxonomy + label: Taxonomy + multiple: true + validate: + type: array + + advanced: + type: tab + title: Advanced + + fields: + columns: + type: columns + fields: + column1: + type: column + fields: + folder: + type: text + label: Folder Name + validate: + type: slug + + route: + type: select + label: Parent + classes: fancy + @data-options: '\Grav\Common\Page\Pages::parents' + @data-default: '\Grav\Plugin\admin::route' + options: + '/': '- Root -' + + type: + type: select + classes: fancy + label: Display Template + default: default + @data-options: '\Grav\Common\Page\Pages::types' + + column2: + type: column + + fields: + order: + type: order + label: Ordering + sitemap: + + overrides: + type: section + title: Overrides + underline: true + + fields: + + header.menu: + type: text + label: Menu + toggleable: true + help: "The string to be used in a menu. If not set, Title will be used." + + header.slug: + type: text + label: Slug + toggleable: true + help: "The slug variable allows you to specifically set the page's portion of the URL" + validate: + message: A slug must contain only lowercase alphanumeric characters and dashes + rule: slug + + + + header.process: + type: checkboxes + label: Process + toggleable: true + @config-default: system.pages.process + default: + markdown: true + twig: false + options: + markdown: Markdown + twig: Twig + use: keys + + header.child_type: + type: select + toggleable: true + label: Default Child Type + default: default + placeholder: Use Global + @data-options: '\Grav\Common\Page\Pages::types' + + header.visible: + type: toggle + label: Visible + help: "Determines if a page is visible in the navigation." + highlight: 1 + options: + '': Global + 1: Enabled + 0: Disabled + validate: + type: bool + + header.routable: + type: toggle + label: Routable + help: If this page is reachable by a URL + highlight: 1 + default: '' + options: + '': Global + 1: Enabled + 0: Disabled + validate: + type: bool + + header.cache_enable: + type: toggle + label: Caching + highlight: 1 + options: + '': Global + 1: Enabled + 0: Disabled + validate: + type: bool + + + + + + header.order_by: + type: hidden + + header.order_manual: + type: hidden + validate: + type: commalist + + blueprint: + type: blueprint diff --git a/system/blueprints/pages/modular.yaml b/system/blueprints/pages/modular.yaml new file mode 100644 index 000000000..e90f1d2ee --- /dev/null +++ b/system/blueprints/pages/modular.yaml @@ -0,0 +1,46 @@ +title: Modular +@extends: + type: default + context: blueprints://pages + +form: + fields: + tabs: + type: tabs + active: 1 + + fields: + content: + fields: + + header.content.items: + type: select + label: Items + default: @self.modular + options: + @self.modular: Children + + header.content.order.by: + type: select + label: Order By + default: date + options: + folder: Folder + title: Title + date: Date + default: Default + + header.content.order.dir: + type: select + label: Order + default: desc + options: + asc: Ascending + desc: Descending + + header.process: + type: ignore + content: + type: ignore + uploads: + type: ignore diff --git a/system/blueprints/pages/modular_new.yaml b/system/blueprints/pages/modular_new.yaml index 8951b8899..fe366439a 100644 --- a/system/blueprints/pages/modular_new.yaml +++ b/system/blueprints/pages/modular_new.yaml @@ -52,3 +52,7 @@ form: default: 1 validate: type: bool + + + blueprint: + type: blueprint diff --git a/system/blueprints/pages/modular_raw.yaml b/system/blueprints/pages/modular_raw.yaml index f7877772f..c22efe421 100644 --- a/system/blueprints/pages/modular_raw.yaml +++ b/system/blueprints/pages/modular_raw.yaml @@ -82,3 +82,5 @@ form: type: order label: Ordering + blueprint: + type: blueprint diff --git a/system/blueprints/pages/move.yaml b/system/blueprints/pages/move.yaml new file mode 100644 index 000000000..cd90dea8d --- /dev/null +++ b/system/blueprints/pages/move.yaml @@ -0,0 +1,17 @@ +rules: + slug: + pattern: "[a-z][a-z0-9_\-]+" + min: 2 + max: 80 + +form: + validation: loose + fields: + route: + type: select + label: Parent + classes: fancy + @data-options: '\Grav\Common\Page\Pages::parents' + @data-default: '\Grav\Plugin\admin::route' + options: + '/': '- Root -' diff --git a/system/blueprints/pages/new.yaml b/system/blueprints/pages/new.yaml index 3d554bff1..b48000f2c 100644 --- a/system/blueprints/pages/new.yaml +++ b/system/blueprints/pages/new.yaml @@ -46,3 +46,7 @@ form: @data-options: '\Grav\Common\Page\Pages::types' validate: required: true + + + blueprint: + type: blueprint diff --git a/system/blueprints/pages/page.yaml b/system/blueprints/pages/page.yaml deleted file mode 100644 index 81948231b..000000000 --- a/system/blueprints/pages/page.yaml +++ /dev/null @@ -1,42 +0,0 @@ -rules: - slug: - pattern: "[a-z][a-z0-9_\-]+" - min: 2 - max: 80 - -form: - validation: loose - fields: - - title: - type: text - label: Title - validate: - required: true - - folder: - type: text - label: Folder - validate: - type: slug - required: true - - route: - type: select - label: Parent - classes: fancy - @data-options: '\Grav\Common\Page\Pages::parents' - @data-default: '\Grav\Plugin\admin::route' - options: - '/': '- Root -' - validate: - required: true - - type: - type: select - classes: fancy - label: Display Template - default: default - @data-options: '\Grav\Common\Page\Pages::types' - validate: - required: true diff --git a/system/blueprints/pages/raw.yaml b/system/blueprints/pages/raw.yaml index 311cf77e3..c4f91ac3f 100644 --- a/system/blueprints/pages/raw.yaml +++ b/system/blueprints/pages/raw.yaml @@ -82,3 +82,5 @@ form: type: order label: Ordering + blueprint: + type: blueprint diff --git a/system/blueprints/user/account.yaml b/system/blueprints/user/account.yaml index 7ed36145a..7f9dfc9dc 100644 --- a/system/blueprints/user/account.yaml +++ b/system/blueprints/user/account.yaml @@ -15,10 +15,12 @@ form: readonly: true email: - type: text + type: email size: large label: Email validate: + type: email + message: Must be a valid email address required: true password: @@ -27,6 +29,8 @@ form: label: Password validate: required: true + message: Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters + pattern: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}' fullname: type: text diff --git a/system/config/system.yaml b/system/config/system.yaml index 17ef03d8a..77edb1d93 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -3,7 +3,14 @@ timezone: '' # Valid values: http://php.net/manual/en/ param_sep: ':' # Parameter separator, use ';' for Apache on windows languages: - translations: true # Enable translations by default + supported: [] # List of languages supported. eg: [en, fr, de] + 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 + home_redirect: + include_lang: true # Include language in home redirect (/en) + include_route: false # Include route in home redirect (/blog) + home: alias: '/home' # Default path for home, ie / @@ -80,6 +87,7 @@ debugger: 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 media: diff --git a/system/defines.php b/system/defines.php index 851848604..707e7498f 100644 --- a/system/defines.php +++ b/system/defines.php @@ -2,7 +2,7 @@ // Some standard defines define('GRAV', true); -define('GRAV_VERSION', '0.9.33'); +define('GRAV_VERSION', '0.9.34'); define('DS', '/'); // Directories and Paths diff --git a/system/languages/en.yaml b/system/languages/en.yaml new file mode 100644 index 000000000..7ee1ca42b --- /dev/null +++ b/system/languages/en.yaml @@ -0,0 +1,58 @@ +INFLECTOR_PLURALS: + '/(quiz)$/i': '\1zes' + '/^(ox)$/i': '\1en' + '/([m|l])ouse$/i': '\1ice' + '/(matr|vert|ind)ix|ex$/i': '\1ices' + '/(x|ch|ss|sh)$/i': '\1es' + '/([^aeiouy]|qu)ies$/i': '\1y' + '/([^aeiouy]|qu)y$/i': '\1ies' + '/(hive)$/i': '\1s' + '/(?:([^f])fe|([lr])f)$/i': '\1\2ves' + '/sis$/i': 'ses' + '/([ti])um$/i': '\1a' + '/(buffal|tomat)o$/i': '\1oes' + '/(bu)s$/i': '\1ses' + '/(alias|status)/i': '\1es' + '/(octop|vir)us$/i': '\1i' + '/(ax|test)is$/i': '\1es' + '/s$/i': 's' + '/$/': 's' +INFLECTOR_SINGULAR: + '/(quiz)zes$/i': '\1' + '/(matr)ices$/i': '\1ix' + '/(vert|ind)ices$/i': '\1ex' + '/^(ox)en/i': '\1' + '/(alias|status)es$/i': '\1' + '/([octop|vir])i$/i': '\1us' + '/(cris|ax|test)es$/i': '\1is' + '/(shoe)s$/i': '\1' + '/(o)es$/i': '\1' + '/(bus)es$/i': '\1' + '/([m|l])ice$/i': '\1ouse' + '/(x|ch|ss|sh)es$/i': '\1' + '/(m)ovies$/i': '\1ovie' + '/(s)eries$/i': '\1eries' + '/([^aeiouy]|qu)ies$/i': '\1y' + '/([lr])ves$/i': '\1f' + '/(tive)s$/i': '\1' + '/(hive)s$/i': '\1' + '/([^f])ves$/i': '\1fe' + '/(^analy)ses$/i': '\1sis' + '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis' + '/([ti])a$/i': '\1um' + '/(n)ews$/i': '\1ews' + '/s$/i': '' +INFLECTOR_UNCOUNTABLE: ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep'] +INFLECTOR_IRREGULAR: + 'person': 'people' + 'man': 'men' + 'child': 'children' + 'sex': 'sexes' + 'move': 'moves' +INFLECTOR_ORDINALS: + 'default': 'th' + 'first': 'st' + 'second': 'nd' + 'third': 'rd' + + diff --git a/system/src/Grav/Common/Config/Config.php b/system/src/Grav/Common/Config/Config.php index 646abee27..98a7b3da8 100644 --- a/system/src/Grav/Common/Config/Config.php +++ b/system/src/Grav/Common/Config/Config.php @@ -120,7 +120,6 @@ class Config extends Data $setup['streams']['schemes'] += $this->streams; $setup = $this->autoDetectEnvironmentConfig($setup); - $this->messages[] = $setup['streams']['schemes']['config']['prefixes']['']; $this->setup = $setup; parent::__construct($setup); @@ -202,24 +201,20 @@ class Config extends Data // Generate checksum according to the configuration settings. if (!$checkConfig) { - $this->messages[] = 'Check configuration timestamps from system.yaml files.'; // Just check changes in system.yaml files and ignore all the other files. $cc = $checkSystem ? $this->finder->locateConfigFile($this->configLookup, 'system') : []; } else { - $this->messages[] = 'Check configuration timestamps from all configuration files.'; // Check changes in all configuration files. $cc = $this->finder->locateConfigFiles($this->configLookup, $this->pluginLookup); } if ($checkBlueprints) { - $this->messages[] = 'Check blueprint timestamps from all blueprint files.'; $cb = $this->finder->locateBlueprintFiles($this->blueprintLookup, $this->pluginLookup); } else { $cb = []; } if ($checkLanguages) { - $this->messages[] = 'Check language timestamps from all language files.'; $cl = $this->finder->locateLanguageFiles($this->languagesLookup, $this->pluginLookup); } else { $cl = []; @@ -341,6 +336,11 @@ class Config extends Data $this->items = $cache['data']; } + /** + * @param $languages + * @param $plugins + * @param null $filename + */ protected function loadCompiledLanguages($languages, $plugins, $filename = null) { $checksum = md5(json_encode($languages)); @@ -366,11 +366,24 @@ class Config extends Data // Load languages. $this->languages = new Languages; - foreach ($languageFiles as $files) { - $this->loadLanguagesFiles($files); + + if (isset($languageFiles['user/plugins'])) { + foreach ((array) $languageFiles['user/plugins'] as $plugin => $item) { + $lang_file = CompiledYamlFile::instance($item['file']); + $content = $lang_file->content(); + foreach ((array) $content as $lang => $value) { + $this->languages->join($lang, $value, '/'); + } + } } - $this->languages->reformat(); + if (isset($languageFiles['system/languages'])) { + foreach ((array) $languageFiles['system/languages'] as $lang => $item) { + $lang_file = CompiledYamlFile::instance($item['file']); + $content = $lang_file->content(); + $this->languages->join($lang, $content, '/'); + } + } $cache = [ '@class' => $class, @@ -415,14 +428,6 @@ class Config extends Data } } - public function loadLanguagesFiles(array $files) - { - foreach ($files as $name => $item) { - $file = CompiledYamlFile::instance($item['file']); - $this->languages->join($name, $file->content(), '/'); - } - } - /** * Initialize resource locator by using the configuration. * diff --git a/system/src/Grav/Common/Config/ConfigFinder.php b/system/src/Grav/Common/Config/ConfigFinder.php index 7d79b765b..46069410d 100644 --- a/system/src/Grav/Common/Config/ConfigFinder.php +++ b/system/src/Grav/Common/Config/ConfigFinder.php @@ -143,7 +143,7 @@ class ConfigFinder $filename = "{$path}/{$name}/$find"; if (file_exists($filename)) { - $list["plugins"] = ['file' => $filename, 'modified' => filemtime($filename)]; + $list[$name] = ['file' => $filename, 'modified' => filemtime($filename)]; } } } diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index 19448bc58..d92976ddd 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -1,6 +1,7 @@ validateArray($data, $this->nested); } catch (\RuntimeException $e) { - throw new \RuntimeException(sprintf('Page validation failed: %s', $e->getMessage())); + throw new \RuntimeException(sprintf('Validation failed: %s', $e->getMessage())); } } @@ -117,7 +118,17 @@ class Blueprint { // Initialize data $this->fields(); - return $this->extraArray($data, $this->nested, $prefix); + $rules = $this->nested; + + // Drill down to prefix level + if (!empty($prefix)) { + $parts = explode('.', trim($prefix, '.')); + foreach ($parts as $part) { + $rules = isset($rules[$part]) ? $rules[$part] : []; + } + } + + return $this->extraArray($data, $rules, $prefix); } /** @@ -286,7 +297,7 @@ class Blueprint // Item has been defined in blueprints. } elseif (is_array($field) && is_array($val)) { // Array has been defined in blueprints. - $array += $this->ExtraArray($field, $val, $prefix); + $array += $this->ExtraArray($field, $val, $prefix . $key . '.'); } else { // Undefined/extra item. $array[$prefix.$key] = $field; @@ -313,11 +324,11 @@ class Blueprint $field['name'] = $prefix . $key; $field += $params; - if (isset($field['fields'])) { + if (isset($field['fields']) && $field['type'] !== 'list') { // Recursively get all the nested fields. $newParams = array_intersect_key($this->filter, $field); $this->parseFormFields($field['fields'], $newParams, $prefix, $current[$key]['fields']); - } else { + } else if ($field['type'] !== 'ignore') { // Add rule. $this->rules[$prefix . $key] = &$field; $this->addProperty($prefix . $key); @@ -362,10 +373,20 @@ class Blueprint } } } + + elseif (substr($name, 0, 8) == '@config-') { + $property = substr($name, 8); + $default = isset($field[$property]) ? $field[$property] : null; + $config = self::getGrav()['config']->get($value, $default); + + if (!is_null($config)) { + $field[$property] = $config; + } + } } // Initialize predefined validation rule. - if (isset($field['validate']['rule'])) { + if (isset($field['validate']['rule']) && $field['type'] !== 'ignore') { $field['validate'] += $this->getRule($field['validate']['rule']); } } diff --git a/system/src/Grav/Common/Data/Blueprints.php b/system/src/Grav/Common/Data/Blueprints.php index a92f235f2..a024c87f7 100644 --- a/system/src/Grav/Common/Data/Blueprints.php +++ b/system/src/Grav/Common/Data/Blueprints.php @@ -2,6 +2,7 @@ namespace Grav\Common\Data; use Grav\Common\File\CompiledYamlFile; +use Grav\Common\GravTrait; /** * Blueprints class keeps track on blueprint instances. @@ -11,6 +12,8 @@ use Grav\Common\File\CompiledYamlFile; */ class Blueprints { + use GravTrait; + protected $search; protected $types; protected $instances = array(); @@ -55,8 +58,20 @@ class Blueprints if (isset($blueprints['@extends'])) { // Extend blueprint by other blueprints. $extends = (array) $blueprints['@extends']; - foreach ($extends as $extendType) { - $blueprint->extend($this->get($extendType)); + + if (is_string(key($extends))) { + $extends = [ $extends ]; + } + + foreach ($extends as $extendConfig) { + $extendType = !is_string($extendConfig) ? empty($extendConfig['type']) ? false : $extendConfig['type'] : $extendConfig; + + if (!$extendType) { + continue; + } + + $context = is_string($extendConfig) || empty($extendConfig['context']) ? $this : new self(self::getGrav()['locator']->findResource($extendConfig['context'])); + $blueprint->extend($context->get($extendType)); } } diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index bd1a34d10..30db908f7 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -28,14 +28,16 @@ class Validation // Validate type with fallback type text. $type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type']; $method = 'type'.strtr($type, '-', '_'); + $name = ucfirst($field['label'] ? $field['label'] : $field['name']); + $message = (string) isset($field['validate']['message']) ? $field['validate']['message'] : 'Invalid input in ' . $name; + if (method_exists(__CLASS__, $method)) { $success = self::$method($value, $validate, $field); } else { $success = self::typeText($value, $validate, $field); } if (!$success) { - $name = $field['label'] ? $field['label'] : $field['name']; - throw new \RuntimeException("invalid input in {$name}"); + throw new \RuntimeException($message); } // Check individual rules @@ -43,8 +45,9 @@ class Validation $method = 'validate'.strtr($rule, '-', '_'); if (method_exists(__CLASS__, $method)) { $success = self::$method($value, $params); + if (!$success) { - throw new \RuntimeException('Failed'); + throw new \RuntimeException($message); } } } @@ -489,6 +492,7 @@ class Validation { $values = (array) $value; $options = isset($field['options']) ? array_keys($field['options']) : array(); + $multi = isset($field['multiple']) ? $field['multiple'] : false; if ($options) { $useKey = isset($field['use']) && $field['use'] == 'keys'; @@ -497,9 +501,39 @@ class Validation } } + if ($multi) { + foreach ($values as $key => $value) { + $values[$key] = explode(',', $value[0]); + } + } + return $values; } + public static function typeList($value, array $params, array $field) + { + if (!is_array($value)) { + return false; + } + + if (isset($field['fields'])) { + foreach ($value as $key => $item) { + foreach ($field['fields'] as $subKey => $subField) { + $subKey = trim($subKey, '.'); + $subValue = isset($item[$subKey]) ? $item[$subKey] : null; + self::validate($subValue, $subField); + } + } + } + + return true; + } + + protected static function filterList($value, array $params, array $field) + { + return (array) $value; + } + /** * Custom input: ignore (will not validate) * @@ -513,6 +547,11 @@ class Validation return true; } + public static function filterIgnore($value, array $params, array $field) + { + return $value; + } + // HTML5 attributes (min, max and range are handled inside the types) public static function validateRequired($value, $params) diff --git a/system/src/Grav/Common/File/CompiledFile.php b/system/src/Grav/Common/File/CompiledFile.php index d9cf58d42..27fffc50b 100644 --- a/system/src/Grav/Common/File/CompiledFile.php +++ b/system/src/Grav/Common/File/CompiledFile.php @@ -25,7 +25,7 @@ trait CompiledFile // If nothing has been loaded, attempt to get pre-compiled version of the file first. if ($var === null && $this->raw === null && $this->content === null) { $key = md5($this->filename); - $file = PhpFile::instance(CACHE_DIR . "/compiled/files/{$key}{$this->extension}.php"); + $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php"); $modified = $this->modified(); if (!$modified) { diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php index a6eefce5a..1118b2955 100644 --- a/system/src/Grav/Common/GPM/GPM.php +++ b/system/src/Grav/Common/GPM/GPM.php @@ -349,6 +349,7 @@ class GPM extends Iterator public function findPackages($searches = []) { $packages = ['total' => 0, 'not_found' => []]; + $inflector = new Inflector(); foreach ($searches as $search) { $repository = ''; @@ -380,7 +381,7 @@ class GPM extends Iterator } $not_found = new \stdClass(); - $not_found->name = Inflector::camelize($search); + $not_found->name = $inflector->camelize($search); $not_found->slug = $search; $not_found->package_type = $type; $not_found->install_path = str_replace('%name%', $search, $this->install_paths[$type]); diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index c145192e4..5e4737411 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -159,6 +159,8 @@ class Grav extends Container $container->register(new StreamsServiceProvider); $container->register(new ConfigServiceProvider); + $container['inflector'] = new Inflector(); + $container['debugger']->stopTimer('_init'); return $container; @@ -172,8 +174,8 @@ class Grav extends Container // Initialize configuration. $debugger->startTimer('_config', 'Configuration'); $this['config']->init(); - $this['session']->init(); $this['uri']->init(); + $this['session']->init(); $this['errors']->resetHandlers(); $debugger->init(); $this['config']->debug(); diff --git a/system/src/Grav/Common/Inflector.php b/system/src/Grav/Common/Inflector.php index d707448c4..ff1030ef7 100644 --- a/system/src/Grav/Common/Inflector.php +++ b/system/src/Grav/Common/Inflector.php @@ -4,6 +4,7 @@ namespace Grav\Common; /** * This file was originally part of the Akelos Framework */ +use Grav\Common\Language\Language; /** * Inflector for pluralize and singularize English nouns. @@ -20,65 +21,55 @@ namespace Grav\Common; class Inflector { + use GravTrait; + + protected $plural; + protected $singular; + protected $uncountable; + protected $irregular; + protected $ordinals; + + public function init() + { + if (empty($this->plural)) { + $language = self::getGrav()['language']; + $this->plural = $language->translate('INFLECTOR_PLURALS', null, true); + $this->singular = $language->translate('INFLECTOR_SINGULAR', null, true); + $this->uncountable = $language->translate('INFLECTOR_UNCOUNTABLE', null, true); + $this->irregular = $language->translate('INFLECTOR_IRREGULAR', null, true); + $this->ordinals = $language->translate('INFLECTOR_ORDINALS', null, true); + } + } /** * Pluralizes English nouns. * - * @access static public - * @static * @param string $word English noun to pluralize * @return string Plural noun */ - public static function pluralize($word, $count = 2) + public function pluralize($word, $count = 2) { + $this->init(); + if ($count == 1) { return $word; } - $plural = array( - '/(quiz)$/i' => '\1zes', - '/^(ox)$/i' => '\1en', - '/([m|l])ouse$/i' => '\1ice', - '/(matr|vert|ind)ix|ex$/i' => '\1ices', - '/(x|ch|ss|sh)$/i' => '\1es', - '/([^aeiouy]|qu)ies$/i' => '\1y', - '/([^aeiouy]|qu)y$/i' => '\1ies', - '/(hive)$/i' => '\1s', - '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', - '/sis$/i' => 'ses', - '/([ti])um$/i' => '\1a', - '/(buffal|tomat)o$/i' => '\1oes', - '/(bu)s$/i' => '\1ses', - '/(alias|status)/i'=> '\1es', - '/(octop|vir)us$/i'=> '\1i', - '/(ax|test)is$/i'=> '\1es', - '/s$/i'=> 's', - '/$/'=> 's'); - - $uncountable = array('equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep'); - - $irregular = array( - 'person' => 'people', - 'man' => 'men', - 'child' => 'children', - 'sex' => 'sexes', - 'move' => 'moves'); - $lowercased_word = strtolower($word); - foreach ($uncountable as $_uncountable) { + foreach ($this->uncountable as $_uncountable) { if (substr($lowercased_word, (-1*strlen($_uncountable))) == $_uncountable) { return $word; } } - foreach ($irregular as $_plural => $_singular) { + foreach ($this->irregular as $_plural => $_singular) { if (preg_match('/('.$_plural.')$/i', $word, $arr)) { return preg_replace('/('.$_plural.')$/i', substr($arr[0], 0, 1).substr($_singular, 1), $word); } } - foreach ($plural as $rule => $replacement) { + foreach ($this->plural as $rule => $replacement) { if (preg_match($rule, $word)) { return preg_replace($rule, $replacement, $word); } @@ -94,62 +85,28 @@ class Inflector * @param int $count * @return string Singular noun. */ - public static function singularize($word, $count = 1) + public function singularize($word, $count = 1) { + $this->init(); + if ($count != 1) { return $word; } - $singular = array ( - '/(quiz)zes$/i' => '\1', - '/(matr)ices$/i' => '\1ix', - '/(vert|ind)ices$/i' => '\1ex', - '/^(ox)en/i' => '\1', - '/(alias|status)es$/i' => '\1', - '/([octop|vir])i$/i' => '\1us', - '/(cris|ax|test)es$/i' => '\1is', - '/(shoe)s$/i' => '\1', - '/(o)es$/i' => '\1', - '/(bus)es$/i' => '\1', - '/([m|l])ice$/i' => '\1ouse', - '/(x|ch|ss|sh)es$/i' => '\1', - '/(m)ovies$/i' => '\1ovie', - '/(s)eries$/i' => '\1eries', - '/([^aeiouy]|qu)ies$/i' => '\1y', - '/([lr])ves$/i' => '\1f', - '/(tive)s$/i' => '\1', - '/(hive)s$/i' => '\1', - '/([^f])ves$/i' => '\1fe', - '/(^analy)ses$/i' => '\1sis', - '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', - '/([ti])a$/i' => '\1um', - '/(n)ews$/i' => '\1ews', - '/s$/i' => '', - ); - - $uncountable = array('equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep'); - - $irregular = array( - 'person' => 'people', - 'man' => 'men', - 'child' => 'children', - 'sex' => 'sexes', - 'move' => 'moves'); - $lowercased_word = strtolower($word); - foreach ($uncountable as $_uncountable) { + foreach ($this->uncountable as $_uncountable) { if (substr($lowercased_word, (-1*strlen($_uncountable))) == $_uncountable) { return $word; } } - foreach ($irregular as $_plural => $_singular) { + foreach ($this->irregular as $_plural => $_singular) { if (preg_match('/('.$_singular.')$/i', $word, $arr)) { return preg_replace('/('.$_singular.')$/i', substr($arr[0], 0, 1).substr($_plural, 1), $word); } } - foreach ($singular as $rule => $replacement) { + foreach ($this->singular as $rule => $replacement) { if (preg_match($rule, $word)) { return preg_replace($rule, $replacement, $word); } @@ -162,24 +119,22 @@ class Inflector * Converts an underscored or CamelCase word into a English * sentence. * - * The titleize static public function converts text like "WelcomePage", + * The titleize public function converts text like "WelcomePage", * "welcome_page" or "welcome page" to this "Welcome * Page". * If second parameter is set to 'first' it will only * capitalize the first character of the title. * - * @access static public - * @static * @param string $word Word to format as tile * @param string $uppercase If set to 'first' it will only uppercase the * first character. Otherwise it will uppercase all * the words in the title. * @return string Text formatted as title */ - public static function titleize($word, $uppercase = '') + public function titleize($word, $uppercase = '') { $uppercase = $uppercase == 'first' ? 'ucfirst' : 'ucwords'; - return $uppercase(static::humanize(static::underscorize($word))); + return $uppercase($this->humanize($this->underscorize($word))); } /** @@ -189,13 +144,11 @@ class Inflector * will remove non alphanumeric character from the word, so * "who's online" will be converted to "WhoSOnline" * - * @access static public - * @static * @see variablize * @param string $word Word to convert to camel case * @return string UpperCamelCasedWord */ - public static function camelize($word) + public function camelize($word) { return str_replace(' ', '', ucwords(preg_replace('/[^A-Z^a-z^0-9]+/', ' ', $word))); } @@ -208,12 +161,10 @@ class Inflector * * This can be really useful for creating friendly URLs. * - * @access static public - * @static * @param string $word Word to underscore * @return string Underscored word */ - public static function underscorize($word) + public function underscorize($word) { $regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1_\2', $word); $regex2 = preg_replace('/([a-zd])([A-Z])/', '\1_\2', $regex1); @@ -229,12 +180,10 @@ class Inflector * * This can be really useful for creating friendly URLs. * - * @access static public - * @static * @param string $word Word to hyphenate * @return string hyphenized word */ - public static function hyphenize($word) + public function hyphenize($word) { $regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1-\2', $word); $regex2 = preg_replace('/([a-zd])([A-Z])/', '\1-\2', $regex1); @@ -252,14 +201,12 @@ class Inflector * If you need to uppercase all the words you just have to * pass 'all' as a second parameter. * - * @access static public - * @static * @param string $word String to "humanize" * @param string $uppercase If set to 'all' it will uppercase all the words * instead of just the first one. * @return string Human-readable word */ - public static function humanize($word, $uppercase = '') + public function humanize($word, $uppercase = '') { $uppercase = $uppercase == 'all' ? 'ucwords' : 'ucfirst'; return $uppercase(str_replace('_', ' ', preg_replace('/_id$/', '', $word))); @@ -272,15 +219,13 @@ class Inflector * will remove non alphanumeric character from the word, so * "who's online" will be converted to "whoSOnline" * - * @access static public - * @static * @see camelize * @param string $word Word to lowerCamelCase * @return string Returns a lowerCamelCasedWord */ - public static function variablize($word) + public function variablize($word) { - $word = static::camelize($word); + $word = $this->camelize($word); return strtolower($word[0]).substr($word, 1); } @@ -290,15 +235,13 @@ class Inflector * * Converts "Person" to "people" * - * @access static public - * @static * @see classify * @param string $class_name Class name for getting related table_name. * @return string plural_table_name */ - public static function tableize($class_name) + public function tableize($class_name) { - return static::pluralize(static::underscore($class_name)); + return $this->pluralize($this->underscore($class_name)); } /** @@ -307,15 +250,13 @@ class Inflector * * Converts "people" to "Person" * - * @access static public - * @static * @see tableize * @param string $table_name Table name for getting related ClassName. * @return string SingularClassName */ - public static function classify($table_name) + public function classify($table_name) { - return static::camelize(static::singularize($table_name)); + return $this->camelize($this->singularize($table_name)); } /** @@ -323,34 +264,34 @@ class Inflector * * This method converts 13 to 13th, 2 to 2nd ... * - * @access static public - * @static * @param integer $number Number to get its ordinal value * @return string Ordinal representation of given string. */ - public static function ordinalize($number) + public function ordinalize($number) { + $this->init(); + if (in_array(($number % 100), range(11, 13))) { - return $number.'th'; + return $number.$this->ordinals['default']; } else { switch (($number % 10)) { case 1: - return $number.'st'; + return $number.$this->ordinals['first']; break; case 2: - return $number.'nd'; + return $number.$this->ordinals['second']; break; case 3: - return $number.'rd'; + return $number.$this->ordinals['third']; break; default: - return $number.'th'; + return $number.$this->ordinals['default']; break; } } } - public static function monthize($days) + public function monthize($days) { $now = new \DateTime(); $end = new \DateTime(); diff --git a/system/src/Grav/Common/Language/Language.php b/system/src/Grav/Common/Language/Language.php index 9c19f59b6..ea644e8af 100644 --- a/system/src/Grav/Common/Language/Language.php +++ b/system/src/Grav/Common/Language/Language.php @@ -276,13 +276,15 @@ class Language /** * Translate a key and possibly arguments into a string using current lang and fallbacks * - * @param $args first argument is the lookup key value - * other arguments can be passed and replaced in the translation with sprintf syntax + * @param $args first argument is the lookup key value + * other arguments can be passed and replaced in the translation with sprintf syntax * @param Array $languages + * @param bool $array_support + * @param bool $html_out * * @return string */ - public function translate($args, Array $languages = null) + public function translate($args, Array $languages = null, $array_support = false, $html_out = false) { if (is_array($args)) { $lookup = array_shift($args); @@ -293,9 +295,12 @@ class Language if ($this->config->get('system.languages.translations', true)) { - if ($this->enabled() && $lookup) { + + if (isset($this->grav['admin'])) { + $languages = ['en']; + } elseif ($this->enabled() && $lookup) { if (empty($languages)) { - if ($this->config->get('system.languages.translations.fallback', true)) { + if ($this->config->get('system.languages.translations_fallback', true)) { $languages = $this->getFallbackLanguages(); } else { $languages = (array)$this->getDefault(); @@ -306,7 +311,7 @@ class Language } foreach ((array)$languages as $lang) { - $translation = $this->getTranslation($lang, $lookup); + $translation = $this->getTranslation($lang, $lookup, $array_support); if ($translation) { if (count($args) >= 1) { @@ -318,7 +323,11 @@ class Language } } - return '' . $lookup . ''; + if ($html_out) { + return '' . $lookup . ''; + } else { + return $lookup; + } } /** @@ -327,15 +336,16 @@ class Language * @param $key * @param $index * @param null $languages + * @param bool $html_out * * @return string */ - public function translateArray($key, $index, $languages = null) + public function translateArray($key, $index, $languages = null, $html_out = false) { if ($this->config->get('system.languages.translations', true)) { if ($this->enabled() && $key) { if (empty($languages)) { - if ($this->config->get('system.languages.translations.fallback', true)) { + if ($this->config->get('system.languages.translations_fallback', true)) { $languages = $this->getFallbackLanguages(); } else { $languages = (array)$this->getDefault(); @@ -353,21 +363,26 @@ class Language } } - return '' . $key . '[' . $index . ']'; + if ($html_out) { + return '' . $key . '[' . $index . ']'; + } else { + return $key . '[' . $index . ']'; + } } /** * Lookup the translation text for a given lang and key * - * @param $lang lang code - * @param $key key to lookup with + * @param $lang lang code + * @param $key key to lookup with + * @param bool $array_support * * @return string */ - public function getTranslation($lang, $key) + public function getTranslation($lang, $key, $array_support = false) { $translation = $this->config->getLanguages()->get($lang . '.' . $key, null); - if (is_array($translation)) { + if (!$array_support && is_array($translation)) { return (string)array_shift($translation); } diff --git a/system/src/Grav/Common/Page/Medium/ImageFile.php b/system/src/Grav/Common/Page/Medium/ImageFile.php index 9514653f2..bb9ce59aa 100644 --- a/system/src/Grav/Common/Page/Medium/ImageFile.php +++ b/system/src/Grav/Common/Page/Medium/ImageFile.php @@ -2,6 +2,7 @@ namespace Grav\Common\Page\Medium; use Grav\Common\GravTrait; +use Gregwar\Image\Exceptions\GenerationError; use RocketTheme\Toolbox\Event\Event; class ImageFile extends \Gregwar\Image\Image diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php index 05f75da8d..17fd32151 100644 --- a/system/src/Grav/Common/Page/Medium/ImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -66,17 +66,23 @@ class ImageMedium extends Medium { parent::__construct($items, $blueprint); + $config = self::$grav['config']; + $image_info = getimagesize($this->get('filepath')); $this->def('width', $image_info[0]); $this->def('height', $image_info[1]); $this->def('mime', $image_info['mime']); - $this->def('debug', self::$grav['config']->get('system.images.debug')); + $this->def('debug', $config->get('system.images.debug')); $this->set('thumbnails.media', $this->get('filepath')); - $this->default_quality = self::$grav['config']->get('system.images.default_image_quality', 85); + $this->default_quality = $config->get('system.images.default_image_quality', 85); $this->reset(); + + if ($config->get('system.images.cache_all', false)) { + $this->cache(); + } } /** @@ -129,6 +135,19 @@ class ImageMedium extends Medium return self::$grav['base_url'] . $output . $this->querystring() . $this->urlHash(); } + /** + * Simply processes with no extra methods. Useful for triggering events. + * + * @return $this + */ + public function cache() + { + if (!$this->image) { + $this->image(); + } + return $this; + } + /** * Return srcset string for this Medium and its alternatives. diff --git a/system/src/Grav/Common/Page/Medium/VideoMedium.php b/system/src/Grav/Common/Page/Medium/VideoMedium.php index e67d3fbfc..acc967293 100644 --- a/system/src/Grav/Common/Page/Medium/VideoMedium.php +++ b/system/src/Grav/Common/Page/Medium/VideoMedium.php @@ -7,7 +7,6 @@ use Grav\Common\Grav; use Grav\Common\GravTrait; use Grav\Common\Data\Blueprint; use Grav\Common\Data\Data; -use Gregwar\Image\Image as ImageFile; class VideoMedium extends Medium { diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 7bf743fd3..8bcc84fa4 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -56,6 +56,7 @@ class Page protected $items; protected $header; protected $frontmatter; + protected $language; protected $content; protected $summary; protected $raw_content; @@ -127,6 +128,10 @@ class Page } else { $this->extension($extension); } + + // Exract page language from page extension + $language = trim(basename($this->extension(), 'md'), '.') ?: null; + $this->language($language); } /** @@ -217,6 +222,9 @@ class Page if (isset($this->header->title)) { $this->title = trim($this->header->title); } + if (isset($this->header->language)) { + $this->language = trim($this->header->language); + } if (isset($this->header->template)) { $this->template = trim($this->header->template); } @@ -281,6 +289,22 @@ class Page return $this->header; } + /** + * Get page language + * + * @param $var + * + * @return mixed + */ + public function language($var = null) + { + if ($var !== null) { + $this->language = $var; + } + + return $this->language; + } + /** * Modify a header value directly * @@ -539,7 +563,7 @@ class Page return preg_replace($regex, '', $this->folder); } if ($name == 'type') { - return basename($this->name(), '.md'); + return $this->template(); } if ($name == 'media') { return $this->media()->all(); @@ -605,22 +629,6 @@ class Page return null; } - /** - * Get page extension - * - * @param $var - * - * @return mixed - */ - public function extension($var = null) - { - if ($var !== null) { - $this->extension = $var; - } - - return $this->extension; - } - /** * Save page if there's a file assigned to it. * @param bool $reorder Internal use. @@ -658,9 +666,12 @@ class Page if ($parent->path()) { $clone->path($parent->path() . '/' . $clone->folder()); } + // TODO: make sure we always have the route. if ($parent->route()) { $clone->route($parent->route() . '/'. $clone->slug()); + } else { + $clone->route(self::getGrav()['pages']->root()->route() . '/'. $clone->slug()); } return $clone; @@ -693,7 +704,33 @@ class Page /** @var Pages $pages */ $pages = self::getGrav()['pages']; - return $pages->blueprints($this->template()); + $blueprint = $pages->blueprints($this->blueprintName()); + + $fields = $blueprint->fields(); + + // override if you only want 'normal' mode + if (empty($fields) && self::getGrav()['admin'] && self::getGrav()['config']->get('plugins.admin.edit_mode', 'auto') == 'normal') { + $blueprint = $pages->blueprints('default'); + } + + // override if you only want 'expert' mode + if (!empty($fields) && self::getGrav()['admin'] && self::getGrav()['config']->get('plugins.admin.edit_mode', 'auto') == 'expert') { + $blueprint = $pages->blueprints(''); + } + + return $blueprint; + } + + /** + * Get the blueprint name for this page. Use the blueprint form field if set + * + * @return string + */ + public function blueprintName() + { + $blueprint_name = filter_input(INPUT_POST, 'blueprint', FILTER_SANITIZE_STRING) ?: $this->template(); + + return $blueprint_name; } /** @@ -725,7 +762,7 @@ class Page public function extra() { $blueprints = $this->blueprints(); - return $blueprints->extra($this->toArray(), 'header.'); + return $blueprints->extra($this->toArray()['header'], 'header.'); } /** @@ -824,11 +861,28 @@ class Page $this->template = $var; } if (empty($this->template)) { - $this->template = ($this->modular() ? 'modular/' : '') . str_replace($this->extension, '', $this->name()); + $this->template = ($this->modular() ? 'modular/' : '') . str_replace($this->extension(), '', $this->name()); } return $this->template; } + /** + * Gets and sets the extension field. + * + * @param null $var + * @return null|string + */ + public function extension($var = null) + { + if ($var !== null) { + $this->extension = $var; + } + if (empty($this->extension)) { + $this->extension = '.' . pathinfo($this->name(), PATHINFO_EXTENSION); + } + return $this->extension; + } + /** * Gets and sets the expires field. If not set will return the default * @@ -1005,24 +1059,29 @@ class Page if (null === $this->metadata) { $header_tag_http_equivs = ['content-type', 'default-style', 'refresh']; $this->metadata = array(); - $page_header = $this->header; // Set the Generator tag $this->metadata['generator'] = array('name'=>'generator', 'content'=>'GravCMS ' . GRAV_VERSION); - // Safety check to ensure we have a header - if ($page_header) { + + if (isset($this->header->metadata)) { + $page_header = $this->header->metadata; + + + + + // Merge any site.metadata settings in with page metadata $defaults = (array) self::getGrav()['config']->get('site.metadata'); - if (isset($page_header->metadata)) { - $page_header->metadata = array_merge($defaults, $page_header->metadata); + if (isset($page_header)) { + $page_header = array_merge($defaults, $page_header); } else { - $page_header->metadata = $defaults; + $page_header = $defaults; } // Build an array of meta objects.. - foreach ((array)$page_header->metadata as $key => $value) { + foreach ((array)$page_header as $key => $value) { // If this is a property type metadata: "og", "twitter", "facebook" etc if (is_array($value)) { foreach ($value as $property => $prop_value) { diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index c8872c483..d8cad6e07 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -404,8 +404,8 @@ class Pages { if (!self::$types) { self::$types = new Types(); - self::$types->scanBlueprints('theme://blueprints/'); - self::$types->scanTemplates('theme://templates/'); + file_exists('theme://blueprints/') && self::$types->scanBlueprints('theme://blueprints/'); + file_exists('theme://templates/') && self::$types->scanTemplates('theme://templates/'); $event = new Event(); $event->types = self::$types; diff --git a/system/src/Grav/Common/Page/Types.php b/system/src/Grav/Common/Page/Types.php index 418faba9c..f6407cadd 100644 --- a/system/src/Grav/Common/Page/Types.php +++ b/system/src/Grav/Common/Page/Types.php @@ -13,27 +13,24 @@ class Types implements \ArrayAccess, \Iterator, \Countable use ArrayAccess, Constructor, Iterator, Countable, Export; protected $items; + protected $systemBlueprints; public function register($type, $blueprint = null) { + if (!$blueprint && $this->systemBlueprints && isset($this->systemBlueprints[$type])) { + $useBlueprint = $this->systemBlueprints[$type]; + } else { + $useBlueprint = $blueprint; + } + if ($blueprint || empty($this->items[$type])) { - $this->items[$type] = $blueprint; + $this->items[$type] = $useBlueprint; } } public function scanBlueprints($path) { - $options = [ - 'compare' => 'Filename', - 'pattern' => '|\.yaml$|', - 'filters' => [ - 'key' => '|\.yaml$|' - ], - 'key' => 'SubPathName', - 'value' => 'PathName', - ]; - - $this->items = Folder::all($path, $options) + $this->items; + $this->items = $this->findBlueprints($path) + $this->items; } public function scanTemplates($path) @@ -48,6 +45,10 @@ class Types implements \ArrayAccess, \Iterator, \Countable 'recursive' => false ]; + if (!$this->systemBlueprints) { + $this->systemBlueprints = $this->findBlueprints('blueprints://pages'); + } + foreach (Folder::all($path, $options) as $type) { $this->register($type); } @@ -78,9 +79,24 @@ class Types implements \ArrayAccess, \Iterator, \Countable if (strpos($name, 'modular/') !== 0) { continue; } - $list[basename($name)] = trim(ucfirst(strtr(basename($name), '_', ' '))); + $list[$name] = trim(ucfirst(strtr(basename($name), '_', ' '))); } ksort($list); return $list; } + + private function findBlueprints($path) + { + $options = [ + 'compare' => 'Filename', + 'pattern' => '|\.yaml$|', + 'filters' => [ + 'key' => '|\.yaml$|' + ], + 'key' => 'SubPathName', + 'value' => 'PathName', + ]; + + return Folder::all($path, $options); + } } diff --git a/system/src/Grav/Common/Plugins.php b/system/src/Grav/Common/Plugins.php index 77a2afa2e..dd5fb74a0 100644 --- a/system/src/Grav/Common/Plugins.php +++ b/system/src/Grav/Common/Plugins.php @@ -32,6 +32,8 @@ class Plugins extends Iterator $config = self::getGrav()['config']; $plugins = (array) $config->get('plugins'); + $inflector = self::getGrav()['inflector']; + /** @var EventDispatcher $events */ $events = self::getGrav()['events']; @@ -52,7 +54,7 @@ class Plugins extends Iterator $pluginClassFormat = [ 'Grav\\Plugin\\'.ucfirst($plugin).'Plugin', - 'Grav\\Plugin\\'.Inflector::camelize($plugin).'Plugin' + 'Grav\\Plugin\\'.$inflector->camelize($plugin).'Plugin' ]; $pluginClassName = false; diff --git a/system/src/Grav/Common/Session.php b/system/src/Grav/Common/Session.php index cdc2e9958..c587b6332 100644 --- a/system/src/Grav/Common/Session.php +++ b/system/src/Grav/Common/Session.php @@ -21,15 +21,27 @@ class Session extends \RocketTheme\Toolbox\Session\Session $config = $this->grav['config']; if ($config->get('system.session.enabled')) { + // Only activate admin if we're inside the admin path. + $is_admin = false; + $route = $config->get('plugins.admin.route'); + $base = '/' . trim($route, '/'); + if (substr($uri->route(), 0, strlen($base)) == $base) { + $is_admin = true; + } + + $session_timeout = $config->get('system.session.timeout', 1800); + $session_path = $config->get('system.session.path', '/' . ltrim($uri->rootUrl(false), '/')); + // Define session service. parent::__construct( - $config->get('system.session.timeout', 1800), - $config->get('system.session.path', '/' . ltrim($uri->rootUrl(false), '/')) + $session_timeout, + $session_path ); - $site_identifier = $config->get('site.title', 'unkown'); - $this->setName($config->get('system.session.name', 'grav_site') . '_' . substr(md5($site_identifier), 0, 7)); + $site_identifier = $config->get('site.title', 'unknown'); + $this->setName($config->get('system.session.name', 'grav_site') . '_' . substr(md5($site_identifier), 0, 7) . ($is_admin ? '_admin' : '')); $this->start(); + setcookie(session_name(), session_id(), time() + $session_timeout, $session_path); } } } diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php index b27a32c87..516a9d2fb 100644 --- a/system/src/Grav/Common/Themes.php +++ b/system/src/Grav/Common/Themes.php @@ -141,15 +141,16 @@ class Themes extends Iterator $locator = $grav['locator']; $file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php"); + $inflector = $grav['inflector']; + if ($file) { // Local variables available in the file: $grav, $config, $name, $file $class = include $file; if (!is_object($class)) { - $themeClassFormat = [ 'Grav\\Theme\\'.ucfirst($name), - 'Grav\\Theme\\'.Inflector::camelize($name) + 'Grav\\Theme\\'.$inflector->camelize($name) ]; $themeClassName = false; diff --git a/system/src/Grav/Common/Twig/Twig.php b/system/src/Grav/Common/Twig/Twig.php index 10d8c6539..d8f790962 100644 --- a/system/src/Grav/Common/Twig/Twig.php +++ b/system/src/Grav/Common/Twig/Twig.php @@ -301,13 +301,14 @@ class Twig $this->grav->fireEvent('onTwigSiteVariables'); $pages = $this->grav['pages']; $page = $this->grav['page']; + $content = $page->content(); $twig_vars = $this->twig_vars; $twig_vars['pages'] = $pages->root(); $twig_vars['page'] = $page; $twig_vars['header'] = $page->header(); - $twig_vars['content'] = $page->content(); + $twig_vars['content'] = $content; $ext = '.' . ($format ? $format : 'html') . TWIG_EXT; // determine if params are set, if so disable twig cache diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/TwigExtension.php index f2cba017f..7addcc223 100644 --- a/system/src/Grav/Common/Twig/TwigExtension.php +++ b/system/src/Grav/Common/Twig/TwigExtension.php @@ -78,6 +78,7 @@ class TwigExtension extends \Twig_Extension new \Twig_SimpleFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), new \Twig_SimpleFunction('gist', [$this, 'gistFunc']), new \Twig_simpleFunction('random_string', [$this, 'randomStringFunc']), + new \Twig_SimpleFunction('array', [$this, 'arrayFunc']), new \Twig_simpleFunction('t', [$this, 'translate']), new \Twig_simpleFunction('ta', [$this, 'translateArray']) ]; @@ -194,16 +195,18 @@ class TwigExtension extends \Twig_Extension // TODO: check this and fix the docblock if needed. $action = $action.'ize'; + $inflector = $this->grav['inflector']; + if (in_array( $action, ['titleize','camelize','underscorize','hyphenize', 'humanize','ordinalize','monthize'] )) { - return Inflector::$action($data); + return $inflector->$action($data); } elseif (in_array($action, ['pluralize','singularize'])) { if ($count) { - return Inflector::$action($data, $count); + return $inflector->$action($data, $count); } else { - return Inflector::$action($data); + return $inflector->$action($data); } } else { return $data; @@ -479,6 +482,11 @@ class TwigExtension extends \Twig_Extension return Utils::generateRandomString($count); } + public function arrayFunc($value) + { + return (array) $value; + } + public function translateFunc() { return $this->grav['language']->translate(func_get_args()); diff --git a/system/src/Grav/Common/User/User.php b/system/src/Grav/Common/User/User.php index b727fa19b..6528d9b0c 100644 --- a/system/src/Grav/Common/User/User.php +++ b/system/src/Grav/Common/User/User.php @@ -9,6 +9,9 @@ use Grav\Common\GravTrait; /** * User object * + * @property mixed authenticated + * @property mixed password + * @property bool|string hashed_password * @author RocketTheme * @license MIT */ @@ -57,14 +60,13 @@ class User extends Data // Plain-text is still stored if ($this->password) { - if ($password !== $this->password) { // Plain-text passwords do not match, we know we should fail but execute // verify to protect us from timing attacks and return false regardless of // the result Authentication::verify($password, self::getGrav()['config']->get('system.security.default_hash')); return false; - } else { + } else { // Plain-text does match, we can update the hash and proceed $save = true; @@ -97,6 +99,10 @@ class User extends Data */ public function authorise($action) { + if (empty($this->items)) { + return false; + } + return $this->get("access.{$action}") === true; } } diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 7262c498c..5c38f97a9 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -13,8 +13,9 @@ abstract class Utils use GravTrait; /** - * @param string $haystack - * @param string $needle + * @param string $haystack + * @param string $needle + * * @return bool */ public static function startsWith($haystack, $needle) @@ -27,14 +28,17 @@ abstract class Utils return $status; } } + return $status; } + return $needle === '' || strpos($haystack, $needle) === 0; } /** - * @param string $haystack - * @param string $needle + * @param string $haystack + * @param string $needle + * * @return bool */ public static function endsWith($haystack, $needle) @@ -47,14 +51,17 @@ abstract class Utils return $status; } } + return $status; } + return $needle === '' || substr($haystack, -strlen($needle)) === $needle; } /** - * @param string $haystack - * @param string $needle + * @param string $haystack + * @param string $needle + * * @return bool */ public static function contains($haystack, $needle) @@ -67,11 +74,12 @@ abstract class Utils * * @param object $obj1 * @param object $obj2 + * * @return object */ public static function mergeObjects($obj1, $obj2) { - return (object) array_merge((array) $obj1, (array) $obj2); + return (object)array_merge((array)$obj1, (array)$obj2); } /** @@ -82,6 +90,7 @@ abstract class Utils * @param string $ending * @param bool $exact * @param bool $considerHtml + * * @return string */ public static function truncateHtml($text, $length = 100, $ending = '...', $exact = false, $considerHtml = true) @@ -100,34 +109,41 @@ abstract class Utils // 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])) { + 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 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])); + } } - // 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) { + $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)) { + 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) { + if ($entity[1] + 1 - $entities_length <= $left) { $left--; $entities_length += strlen($entity[0]); } else { @@ -136,7 +152,7 @@ abstract class Utils } } } - $truncate .= substr($line_matchings[2], 0, $left+$entities_length); + $truncate .= substr($line_matchings[2], 0, $left + $entities_length); // maximum length is reached, so get off the loop break; } else { @@ -172,6 +188,7 @@ abstract class Utils $truncate .= ''; } } + return $truncate; } @@ -208,7 +225,7 @@ abstract class Utils if ($force_download) { header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); - header('Content-Disposition: attachment; filename='.$file_parts['basename']); + header('Content-Disposition: attachment; filename=' . $file_parts['basename']); header('Content-Transfer-Encoding: binary'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); @@ -243,14 +260,14 @@ abstract class Utils * Return the mimetype based on filename * * @param $extension Extension of file (eg .txt) + * * @return string */ public static function getMimeType($extension) { $extension = strtolower($extension); - switch($extension) - { + switch ($extension) { case "js": return "application/x-javascript"; @@ -348,6 +365,7 @@ abstract class Utils * Normalize path by processing relative `.` and `..` syntax and merging path * * @param $path + * * @return string */ public static function normalizePath($path) @@ -366,6 +384,7 @@ abstract class Utils array_push($ret, $segment); } } + return $root . implode('/', $ret); } @@ -383,10 +402,9 @@ abstract class Utils asort($offsets); $timezone_list = array(); - foreach( $offsets as $timezone => $offset ) - { + foreach ($offsets as $timezone => $offset) { $offset_prefix = $offset < 0 ? '-' : '+'; - $offset_formatted = gmdate( 'H:i', abs($offset) ); + $offset_formatted = gmdate('H:i', abs($offset)); $pretty_offset = "UTC${offset_prefix}${offset_formatted}"; @@ -396,4 +414,23 @@ abstract class Utils return $timezone_list; } + + public static function arrayFilterRecursive(Array $source, $fn) + { + $result = array(); + foreach ($source as $key => $value) + { + if (is_array($value)) + { + $result[$key] = static::arrayFilterRecursive($value, $fn); + continue; + } + if ($fn($key, $value)) + { + $result[$key] = $value; // KEEP + continue; + } + } + return $result; + } }