{{ page.title }}
{{ page.date_formatted }}
@@ -163,26 +161,25 @@ details. You can create themes for your Pico installation in the `themes` folder. Check out the default theme for an example. Pico uses [Twig][] for template -rendering. You can select your theme by setting the `$config['theme']` option -in `config/config.php` to the name of your theme folder. +rendering. You can select your theme by setting the `theme` option in +`config/config.yml` to the name of your theme folder. -All themes must include an `index.twig` (or `index.html`) file to define the -HTML structure of the theme. Below are the Twig variables that are available -to use in your theme. Please note that paths (e.g. `{{ base_dir }}`) and URLs +All themes must include an `index.twig` file to define the HTML structure of +the theme. Below are the Twig variables that are available to use in your +theme. Please note that paths (e.g. `{{ base_dir }}`) and URLs (e.g. `{{ base_url }}`) don't have a trailing slash. -* `{{ config }}` - Contains the values you set in `config/config.php` +* `{{ config }}` - Contains the values you set in `config/config.yml` (e.g. `{{ config.theme }}` becomes `default`) * `{{ base_dir }}` - The path to your Pico root directory -* `{{ base_url }}` - The URL to your Pico site; use Twigs `link` filter to +* `{{ base_url }}` - The URL to your Pico site; use Twig's `link` filter to specify internal links (e.g. `{{ "sub/page"|link }}`), this guarantees that your link works whether URL rewriting is enabled or not * `{{ theme_dir }}` - The path to the currently active theme * `{{ theme_url }}` - The URL to the currently active theme -* `{{ rewrite_url }}` - A boolean flag indicating enabled/disabled URL rewriting -* `{{ site_title }}` - Shortcut to the site title (see `config/config.php`) -* `{{ meta }}` - Contains the meta values from the current page +* `{{ site_title }}` - Shortcut to the site title (see `config/config.yml`) +* `{{ meta }}` - Contains the meta values of the current page * `{{ meta.title }}` * `{{ meta.description }}` * `{{ meta.author }}` @@ -191,55 +188,66 @@ to use in your theme. Please note that paths (e.g. `{{ base_dir }}`) and URLs * `{{ meta.time }}` * `{{ meta.robots }}` * ... -* `{{ content }}` - The content of the current page - (after it has been processed through Markdown) +* `{{ content }}` - The content of the current page after it has been processed + through Markdown * `{{ pages }}` - A collection of all the content pages in your site * `{{ page.id }}` - The relative path to the content file (unique ID) * `{{ page.url }}` - The URL to the page * `{{ page.title }}` - The title of the page (YAML header) * `{{ page.description }}` - The description of the page (YAML header) * `{{ page.author }}` - The author of the page (YAML header) - * `{{ page.time }}` - The timestamp derived from the `Date` header + * `{{ page.time }}` - The [Unix timestamp][UnixTimestamp] derived from + the `Date` header * `{{ page.date }}` - The date of the page (YAML header) - * `{{ page.date_formatted }}` - The formatted date of the page + * `{{ page.date_formatted }}` - The formatted date of the page as specified + by the `date_format` parameter in your + `config/config.yml` * `{{ page.raw_content }}` - The raw, not yet parsed contents of the page; - use Twigs `content` filter to get the parsed + use Twig's `content` filter to get the parsed contents of a page by passing its unique ID (e.g. `{{ "sub/page"|content }}`) * `{{ page.meta }}`- The meta values of the page * `{{ prev_page }}` - The data of the previous page (relative to `current_page`) * `{{ current_page }}` - The data of the current page * `{{ next_page }}` - The data of the next page (relative to `current_page`) -* `{{ is_front_page }}` - A boolean flag for the front page Pages can be used like the following: Additional to Twigs extensive list of filters, functions and tags, Pico also -provides some useful additional filters to make theming easier. You can parse -any Markdown string to HTML using the `markdown` filter. Arrays can be sorted -by one of its keys or a arbitrary deep sub-key using the `sort_by` filter -(e.g. `{% for page in pages|sort_by([ 'meta', 'nav' ]) %}...{% endfor %}` -iterates through all pages, ordered by the `nav` meta header; please note the -`[ 'meta', 'nav' ]` part of the example, it instructs Pico to sort by -`page.meta.nav`). You can return all values of a given key or key path of an -array using the `map` filter (e.g. `{{ pages|map("title") }}` returns all -page titles). +provides some useful additional filters to make theming easier. + +* Pass the unique ID of a page to the `link` filter to return the page's URL + (e.g. `{{ "sub/page"|link }}` gets `%base_url%?sub/page`). +* To get the parsed contents of a page, pass its unique ID to the `content` + filter (e.g. `{{ "sub/page"|content }}`). +* You can parse any Markdown string using the `markdown` filter (e.g. you can + use Markdown in the `description` meta variable and later parse it in your + theme using `{{ meta.description|markdown }}`). +* Arrays can be sorted by one of its keys using the `sort_by` filter + (e.g. `{% for page in pages|sort_by([ 'meta', 'nav' ]) %}...{% endfor %}` + iterates through all pages, ordered by the `nav` meta header; please note the + `[ 'meta', 'nav' ]` part of the example, it instructs Pico to sort by + `page.meta.nav`). +* You can return all values of a given array key using the `map` filter + (e.g. `{{ pages|map("title") }}` returns all page titles). You can use different templates for different content files by specifying the -`Template` meta header. Simply add e.g. `Template: blog-post` to a content file -and Pico will use the `blog-post.twig` file in your theme folder to render -the page. +`Template` meta header. Simply add e.g. `Template: blog` to the YAML header of +a content file and Pico will use the `blog.twig` template in your theme folder +to display the page. -You don't have to create your own theme if Pico's default theme isn't -sufficient for you, you can use one of the great themes third-party developers -and designers created in the past. As with plugins, you can find themes in -[our Wiki][WikiThemes] and on [our website][OfficialThemes]. +Pico's default theme isn't really intended to be used for a productive website, +it's rather a starting point for creating your own theme. If the default theme +isn't sufficient for you, and you don't want to create your own theme, you can +use one of the great themes third-party developers and designers created in the +past. As with plugins, you can find themes in [our Wiki][WikiThemes] and on +[our website][OfficialThemes]. ### Plugins @@ -255,61 +263,113 @@ Depending on the plugin you've installed, you may have to go through some more steps (e.g. specifying config variables), the plugin docs or `README` file will explain what to do. -Plugins which were written to work with Pico 1.0 can be enabled and disabled -through your `config/config.php`. If you want to e.g. disable the `PicoExcerpt` -plugin, add the following line to your `config/config.php`: -`$config['PicoExcerpt.enabled'] = false;`. To force the plugin to be enabled -replace `false` with `true`. +Plugins which were written to work with Pico 1.0 and later can be enabled and +disabled through your `config/config.yml`. If you want to e.g. disable the +`PicoDeprecated` plugin, add the following line to your `config/config.yml`: +`PicoDeprecated.enabled: false`. To force the plugin to be enabled, replace +`false` by `true`. #### Plugins for developers You're a plugin developer? We love you guys! You can find tons of information about how to develop plugins at http://picocms.org/development/. If you've -developed a plugin for Pico 0.9 or older, you probably want to upgrade it -to the brand new plugin system introduced with Pico 1.0. Please refer to the +developed a plugin before and want to upgrade it to Pico 2.0, refer to the [upgrade section of the docs][PluginUpgrade]. ## Config -You can override the default Pico settings (and add your own custom settings) -by editing `config/config.php` in the Pico directory. For a brief overview of -the available settings and their defaults see `config/config.php.template`. To -override a setting, copy `config/config.php.template` to `config/config.php`, -uncomment the setting and set your custom value. +Configuring Pico really is stupidly simple: Just create a `config/config.yml` +to override the default Pico settings (and add your own custom settings). Take +a look at the `config/config.yml.template` for a brief overview of the +available settings and their defaults. To override a setting, simply copy the +line from `config/config.yml.template` to `config/config.yml` and set your +custom value. + +But we didn't stop there. Rather than having just a single config file, you can +use a arbitrary number of config files. Simply create a `.yml` file in Pico's +`config` dir and you're good to go. This allows you to add some structure to +your config, like a separate config file for your theme (`config/my_theme.yml`). + +Please note that Pico loads config files in a special way you should be aware +of. First of all it loads the main config file `config/config.yml`, and then +any other `*.yml` file in Pico's `config` dir in alphabetical order. The file +order is crucial: Configiguration values which have been set already, cannot be +overwritten by a succeeding file. For example, if you set `site_title: Pico` in +`config/a.yml` and `site_title: My awesome site!` in `config/b.yml`, your site +title will be "Pico". + +Since YAML files are plain text files, users might read your Pico config by +navigating to `%base_url%/config/config.yml`. This is no problem in the first +place, but might get a problem if you use plugins that require you to store +security-relevant data in the config (like credentials). Thus you should +*always* make sure to configure your webserver to deny access to Pico's +`config` dir. Just refer to the "URL Rewriting" section below. By following the +instructions, you will not just enable URL rewriting, but also deny access to +Pico's `config` dir. ### URL Rewriting Pico's default URLs (e.g. %base_url%/?sub/page) already are very user-friendly. Additionally, Pico offers you a URL rewrite feature to make URLs even more -user-friendly (e.g. %base_url%/sub/page). +user-friendly (e.g. %base_url%/sub/page). Below you'll find some basic info +about how to configure your webserver proberly to enable URL rewriting. + +#### Apache If you're using the Apache web server, URL rewriting probably already is enabled - try it yourself, click on the [second URL](%base_url%/sub/page). If -you get an error message from your web server, please make sure to enable the -[`mod_rewrite` module][ModRewrite]. Assuming the second URL works, but Pico -still shows no rewritten URLs, force URL rewriting by setting -`$config['rewrite_url'] = true;` in your `config/config.php`. +URL rewriting doesn't work (you're getting `404 Not Found` error messages from +Apache), please make sure to enable the [`mod_rewrite` module][ModRewrite] and +to enable `.htaccess` overrides. You might have to set the +[`AllowOverride` directive][AllowOverride] to `AllowOverride All` in your +virtual host config file or global `httpd.conf`/`apache.conf`. Assuming +rewritten URLs work, but Pico still shows no rewritten URLs, force URL +rewriting by setting `rewrite_url: true` in your `config/config.yml`. If you +rather get a `500 Internal Server Error` no matter what you do, try removing +the `Options` directive from Pico's `.htaccess` file (it's the last line). -If you're using Nginx, you can use the following configuration to enable URL -rewriting (lines `5` to `8`) and denying access to Pico's internal files -(lines `1` to `3`). You'll need to adjust the path (`/pico` on lines `1`, `5` -and `7`) to match your installation directory. Additionally, you'll need to -enable URL rewriting by setting `$config['rewrite_url'] = true;` in your -`config/config.php`. The Nginx configuration should provide the *bare minimum* -you need for Pico. Nginx is a very extensive subject. If you have any trouble, -please read through our [Nginx configuration docs][NginxConfig]. +#### Nginx + +If you're using Nginx, you can use the following config to enable URL rewriting +(lines `5` to `8`) and denying access to Pico's internal files (lines `1` to +`3`). You'll need to adjust the path (`/pico` on lines `1`, `2`, `5` and `7`) +to match your installation directory. Additionally, you'll need to enable URL +rewriting by setting `rewrite_url: true` in your `config/config.yml`. The Nginx +config should provide the *bare minimum* you need for Pico. Nginx is a very +extensive subject. If you have any trouble, please read through our +[Nginx config docs][NginxConfig]. ``` -location ~ /pico/(\.htaccess|\.git|config|content|content-sample|lib|vendor|CHANGELOG\.md|composer\.(json|lock)) { - return 404; +location ~ ^/pico/((config|content|vendor|composer\.(json|lock|phar))(/|$)|(.+/)?\.(?!well-known(/|$))) { + try_files /pico/index.php$is_args$args; } -location ~ ^/pico(.*) { +location /pico/ { index index.php; - try_files $uri $uri/ /pico/index.php?$1&$args; + try_files $uri $uri/ /pico/index.php$is_args$args; } ``` +#### Lighttpd + +Pico runs smoothly on Lighttpd. You can use the following config to enable URL +rewriting (lines `6` to `9`) and denying access to Pico's internal files (lines +`1` to `4`). Make sure to adjust the path (`/pico` on lines `2`, `3` and `7`) +to match your installation directory, and let Pico know about available URL +rewriting by setting `rewrite_url: true` in your `config/config.yml`. The +config below should provide the *bare minimum* you need for Pico. + +``` +url.rewrite-once = ( + "^/pico/(config|content|vendor|composer\.(json|lock|phar))(/|$)" => "/pico/index.php", + "^/pico/(.+/)?\.(?!well-known(/|$))" => "/pico/index.php" +) + +url.rewrite-if-not-file = ( + "^/pico(/|$)" => "/pico/index.php" +) +``` + ## Documentation For more help have a look at the Pico documentation at http://picocms.org/docs. @@ -318,9 +378,11 @@ For more help have a look at the Pico documentation at http://picocms.org/docs. [MarkdownExtra]: https://michelf.ca/projects/php-markdown/extra/ [YAML]: https://en.wikipedia.org/wiki/YAML [Twig]: http://twig.sensiolabs.org/documentation +[UnixTimestamp]: https://en.wikipedia.org/wiki/Unix_timestamp [WikiThemes]: https://github.com/picocms/Pico/wiki/Pico-Themes [WikiPlugins]: https://github.com/picocms/Pico/wiki/Pico-Plugins [OfficialThemes]: http://picocms.org/themes/ [PluginUpgrade]: http://picocms.org/development/#upgrade [ModRewrite]: https://httpd.apache.org/docs/current/mod/mod_rewrite.html +[AllowOverride]: https://httpd.apache.org/docs/current/mod/core.html#allowoverride [NginxConfig]: http://picocms.org/in-depth/nginx/ diff --git a/content/.gitignore b/content/.gitignore new file mode 100644 index 0000000..24560b5 --- /dev/null +++ b/content/.gitignore @@ -0,0 +1,2 @@ +# This directory is meant to be empty +* diff --git a/index.php b/index.php index e582694..45ecdd2 100644 --- a/index.php +++ b/index.php @@ -1,4 +1,14 @@ + * + * SPDX-License-Identifier: MIT + * License-Filename: LICENSE + */ // load dependencies if (is_file(__DIR__ . '/vendor/autoload.php')) { @@ -8,7 +18,7 @@ if (is_file(__DIR__ . '/vendor/autoload.php')) { // composer dependency package require_once(__DIR__ . '/../../../vendor/autoload.php'); } else { - die("Cannot find `vendor/autoload.php`. Run `composer install`."); + die("Cannot find 'vendor/autoload.php'. Run `composer install`."); } // instance Pico diff --git a/index.php.dist b/index.php.dist index 78125ad..a1f981d 100644 --- a/index.php.dist +++ b/index.php.dist @@ -1,14 +1,24 @@ + * + * SPDX-License-Identifier: MIT + * License-Filename: LICENSE + */ // check PHP platform requirements if (PHP_VERSION_ID < 50306) { die('Pico requires PHP 5.3.6 or above to run'); } if (!extension_loaded('dom')) { - die('Pico requires the PHP extension "dom" to run'); + die("Pico requires the PHP extension 'dom' to run"); } if (!extension_loaded('mbstring')) { - die('Pico requires the PHP extension "mbstring" to run'); + die("Pico requires the PHP extension 'mbstring' to run"); } // load dependencies @@ -22,5 +32,8 @@ $pico = new Pico( 'themes/' // themes dir ); +// override configuration? +//$pico->setConfig(array()); + // run application echo $pico->run(); diff --git a/lib/AbstractPicoPlugin.php b/lib/AbstractPicoPlugin.php index fb8840e..23302f2 100644 --- a/lib/AbstractPicoPlugin.php +++ b/lib/AbstractPicoPlugin.php @@ -1,14 +1,27 @@ + * + * SPDX-License-Identifier: MIT + * License-Filename: LICENSE + */ /** * Abstract class to extend from when implementing a Pico plugin * + * Please refer to {@see PicoPluginInterface} for more information about how + * to develop a plugin for Pico. + * * @see PicoPluginInterface * * @author Daniel Rudolf * @link http://picocms.org * @license http://opensource.org/licenses/MIT The MIT License - * @version 1.0 + * @version 2.0 */ abstract class AbstractPicoPlugin implements PicoPluginInterface { @@ -21,22 +34,30 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface private $pico; /** - * Boolean indicating if this plugin is enabled (true) or disabled (false) + * Boolean indicating if this plugin is enabled (TRUE) or disabled (FALSE) * * @see PicoPluginInterface::isEnabled() * @see PicoPluginInterface::setEnabled() - * @var boolean + * @var bool|null */ - protected $enabled = true; + protected $enabled; /** * Boolean indicating if this plugin was ever enabled/disabled manually * * @see PicoPluginInterface::isStatusChanged() - * @var boolean + * @var bool */ protected $statusChanged = false; + /** + * Boolean indicating whether this plugin matches Pico's API version + * + * @see AbstractPicoPlugin::checkCompatibility() + * @var bool|null + */ + protected $nativePlugin; + /** * List of plugins which this plugin depends on * @@ -74,14 +95,16 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface if ($pluginEnabled !== null) { $this->setEnabled($pluginEnabled); } else { - $pluginConfig = $this->getConfig(get_called_class()); - if (is_array($pluginConfig) && isset($pluginConfig['enabled'])) { - $this->setEnabled($pluginConfig['enabled']); + $pluginEnabled = $this->getPluginConfig('enabled'); + if ($pluginEnabled !== null) { + $this->setEnabled($pluginEnabled); } elseif ($this->enabled) { + $this->setEnabled($this->enabled, true, true); + } elseif ($this->enabled === null) { // make sure dependencies are already fulfilled, // otherwise the plugin needs to be enabled manually try { - $this->checkDependencies(false); + $this->setEnabled(true, false, true); } catch (RuntimeException $e) { $this->enabled = false; } @@ -105,6 +128,7 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface $this->enabled = (bool) $enabled; if ($enabled) { + $this->checkCompatibility(); $this->checkDependencies($recursive); } else { $this->checkDependants($recursive); @@ -135,13 +159,39 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface return $this->pico; } + /** + * Returns either the value of the specified plugin config variable or + * the config array + * + * @param string $configName optional name of a config variable + * @param mixed $default optional default value to return when the + * named config variable doesn't exist + * + * @return mixed if no name of a config variable has been supplied, the + * plugin's config array is returned; otherwise it returns either the + * value of the named config variable, or, if the named config variable + * doesn't exist, the provided default value or NULL + */ + public function getPluginConfig($configName = null, $default = null) + { + $pluginConfig = $this->getConfig(get_called_class(), array()); + + if ($configName === null) { + return $pluginConfig; + } + + return isset($pluginConfig[$configName]) ? $pluginConfig[$configName] : $default; + } + /** * Passes all not satisfiable method calls to Pico * - * @see Pico - * @param string $methodName name of the method to call - * @param array $params parameters to pass - * @return mixed return value of the called method + * @see Pico + * + * @param string $methodName name of the method to call + * @param array $params parameters to pass + * + * @return mixed return value of the called method */ public function __call($methodName, array $params) { @@ -158,10 +208,13 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface /** * Enables all plugins which this plugin depends on * - * @see PicoPluginInterface::getDependencies() - * @param boolean $recursive enable required plugins automatically + * @see PicoPluginInterface::getDependencies() + * + * @param bool $recursive enable required plugins automatically + * * @return void - * @throws RuntimeException thrown when a dependency fails + * + * @throws RuntimeException thrown when a dependency fails */ protected function checkDependencies($recursive) { @@ -176,7 +229,7 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface } // plugins which don't implement PicoPluginInterface are always enabled - if (is_a($plugin, 'PicoPluginInterface') && !$plugin->isEnabled()) { + if (($plugin instanceof PicoPluginInterface) && !$plugin->isEnabled()) { if ($recursive) { if (!$plugin->isStatusChanged()) { $plugin->setEnabled(true, true, true); @@ -207,15 +260,18 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface /** * Disables all plugins which depend on this plugin * - * @see PicoPluginInterface::getDependants() - * @param boolean $recursive disabled dependant plugins automatically + * @see PicoPluginInterface::getDependants() + * + * @param bool $recursive disabled dependant plugins automatically + * * @return void - * @throws RuntimeException thrown when a dependency fails + * + * @throws RuntimeException thrown when a dependency fails */ protected function checkDependants($recursive) { $dependants = $this->getDependants(); - if (!empty($dependants)) { + if ($dependants) { if ($recursive) { foreach ($this->getDependants() as $pluginName => $plugin) { if ($plugin->isEnabled()) { @@ -230,8 +286,8 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface } } } else { - $dependantsList = 'plugin' . ((count($dependants) > 1) ? 's' : '') . ' '; - $dependantsList .= "'" . implode("', '", array_keys($dependants)) . "'"; + $dependantsList = 'plugin' . ((count($dependants) > 1) ? 's' : '') . ' ' + . "'" . implode("', '", array_keys($dependants)) . "'"; throw new RuntimeException( "Unable to disable plugin '" . get_called_class() . "': " . "Required by " . $dependantsList @@ -249,7 +305,7 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface $this->dependants = array(); foreach ($this->getPlugins() as $pluginName => $plugin) { // only plugins which implement PicoPluginInterface support dependencies - if (is_a($plugin, 'PicoPluginInterface')) { + if ($plugin instanceof PicoPluginInterface) { $dependencies = $plugin->getDependencies(); if (in_array(get_called_class(), $dependencies)) { $this->dependants[$pluginName] = $plugin; @@ -260,4 +316,37 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface return $this->dependants; } + + /** + * Checks compatibility with Pico's API version + * + * Pico automatically adds a dependency to {@see PicoDeprecated} when the + * plugin's API is older than Pico's API. {@see PicoDeprecated} furthermore + * throws a exception when it can't provide compatibility in such cases. + * However, we still have to decide whether this plugin is compatible to + * newer API versions, what requires some special (version specific) + * precaution and is therefore usually not the case. + * + * @return void + * + * @throws RuntimeException thrown when the plugin's and Pico's API aren't + * compatible + */ + protected function checkCompatibility() + { + if ($this->nativePlugin === null) { + $picoClassName = get_class($this->pico); + $picoApiVersion = defined($picoClassName . '::API_VERSION') ? $picoClassName::API_VERSION : 1; + $pluginApiVersion = defined('static::API_VERSION') ? static::API_VERSION : 1; + + $this->nativePlugin = ($pluginApiVersion === $picoApiVersion); + + if (!$this->nativePlugin && ($pluginApiVersion > $picoApiVersion)) { + throw new RuntimeException( + "Unable to enable plugin '" . get_called_class() . "': The plugin's API (version " + . $pluginApiVersion . ") isn't compatible with Pico's API (version " . $picoApiVersion . ")" + ); + } + } + } } diff --git a/lib/Pico.php b/lib/Pico.php index de2802d..4e515cc 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -1,4 +1,20 @@ + * + * The file has been renamed in the past; the version control history of the + * original file applies accordingly, available from the following original + * location: + * + *