From 56b2ed6c7db60405292c532b1d10ee81749cf897 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Mon, 29 Feb 2016 19:50:35 +0100 Subject: [PATCH 001/188] Allow manual plugin loading --- lib/Pico.php | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/Pico.php b/lib/Pico.php index 4ad1c3b..b49c7c5 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -383,6 +383,7 @@ class Pico * - 60 to 79: Plugins hooking into template or markdown parsing * - 80 to 99: Plugins using the `onPageRendered` event * + * @see Pico::loadPlugin() * @see Pico::getPlugin() * @see Pico::getPlugins() * @return void @@ -404,11 +405,52 @@ class Pico $this->plugins[$className] = $plugin; } else { // TODO: breaks backward compatibility - //throw new RuntimeException("Unable to load plugin '".$className."'"); + //throw new RuntimeException("Unable to load plugin '" . $className . "'"); } } } + /** + * Manually loads a plugin + * + * Manually loaded plugins must implement {@see PicoPluginInterface}. + * + * @see Pico::loadPlugins() + * @see Pico::getPlugin() + * @see Pico::getPlugins() + * @param PicoPluginInterface|string $plugin either the class name of a + * plugin to instantiate or a plugin instance + * @return PicoPluginInterface instance of the loaded plugin + * @throws RuntimeException thrown when a plugin couldn't + * be loaded + */ + public function loadPlugin($plugin) + { + if (!is_object($plugin)) { + $className = (string) $plugin; + if (class_exists($className)) { + $plugin = new $className($this); + } else { + throw new RuntimeException("Unable to load plugin '" . $className . "'"); + } + } + + $className = get_class($plugin); + if (!is_a($plugin, 'PicoPluginInterface')) { + throw new RuntimeException( + "Manually loaded plugins must implement 'PicoPluginInterface', " + . "'" . $className . "' given" + ); + } + + if ($this->plugins === null) { + $this->plugins = array(); + } + $this->plugins[$className] = $plugin; + + return $plugin; + } + /** * Returns the instance of a named plugin * From 5a9c02f7bf6466c02458311eb16545c17ebc945e Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Mon, 29 Feb 2016 19:51:06 +0100 Subject: [PATCH 002/188] Allow plugins to trigger events You MUST NOT trigger events of Pico's core through a plugin! --- lib/Pico.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Pico.php b/lib/Pico.php index b49c7c5..a0cb75f 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -1382,6 +1382,7 @@ class Pico * * Deprecated events (as used by plugins not implementing * {@link PicoPluginInterface}) are triggered by {@link PicoDeprecated}. + * You MUST NOT trigger events of Pico's core through a plugin! * * @see PicoPluginInterface * @see AbstractPicoPlugin @@ -1390,7 +1391,7 @@ class Pico * @param array $params optional parameters to pass * @return void */ - protected function triggerEvent($eventName, array $params = array()) + public function triggerEvent($eventName, array $params = array()) { if (!empty($this->plugins)) { foreach ($this->plugins as $plugin) { From 86614a3ab40b4ececa3dadba22de16184a44750b Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Wed, 2 Mar 2016 21:40:58 +0100 Subject: [PATCH 003/188] Default theme: Use flexbox to grow content div --- themes/default/style.css | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/themes/default/style.css b/themes/default/style.css index a8c0030..9073349 100644 --- a/themes/default/style.css +++ b/themes/default/style.css @@ -211,6 +211,17 @@ blockquote { /* Structure Styles /*---------------------------------------------*/ +body { + display: flex; + flex-direction: column; + min-height: 100vh; + height: 100%; +} +body > * { + flex: none; + width: 100%; +} + .inner { width: 850px; margin: 0 auto; @@ -262,6 +273,9 @@ blockquote { display: inline-block; float: left; } +#content { + flex: 1 0 auto; +} #footer { background: #707070; padding: 60px 0; From a3fa373119c12cae834bcfab758a11b036234c76 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Wed, 2 Mar 2016 21:44:38 +0100 Subject: [PATCH 004/188] Default theme: Move elements into Twig blocks Allows plugins to extend the default template --- themes/default/index.twig | 90 +++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/themes/default/index.twig b/themes/default/index.twig index 37f8f16..1b9f2ce 100644 --- a/themes/default/index.twig +++ b/themes/default/index.twig @@ -1,51 +1,67 @@ - + - {% if meta.title %}{{ meta.title }} | {% endif %}{{ site_title }} - {% if meta.description %} - - {% endif %}{% if meta.robots %} - - {% endif %} + {% block documentHeader %} + {% block title %}{% if meta.title %}{{ meta.title }} | {% endif %}{{ site_title }}{% endblock %} + {% if meta.description %} + + {% endif %}{% if meta.robots %} + + {% endif %} + {% endblock %} - - + {% block stylesheets %} + + + {% endblock %} - + {% block javascript %} + + {% endblock %} - + {% block pageHeader %} + + {% endblock %} -
-
- {{ content }} -
-
+ {% block pageContent %} +
+
+ {% block content content %} +
+
+ {% endblock %} - + {% block pageFooter %} + + {% endblock %} From 1709b920d17ba685e200405dcc8b9cb221a35e61 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Wed, 2 Mar 2016 21:46:35 +0100 Subject: [PATCH 005/188] Add AbstractPicoPlugin::getPluginConfig() method --- lib/AbstractPicoPlugin.php | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/AbstractPicoPlugin.php b/lib/AbstractPicoPlugin.php index 4ee337e..68587f0 100644 --- a/lib/AbstractPicoPlugin.php +++ b/lib/AbstractPicoPlugin.php @@ -74,9 +74,9 @@ 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) { // make sure dependencies are already fulfilled, // otherwise the plugin needs to be enabled manually @@ -135,6 +135,29 @@ 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 + * @return mixed returns either the value of the named plugin + * config variable, null if the config variable doesn't exist or the + * plugin's config array if no config name was supplied + */ + protected function getPluginConfig($configName = null) + { + $pluginConfig = $this->getConfig(get_called_class()); + if ($pluginConfig) { + if ($configName === null) { + return $pluginConfig; + } elseif (isset($pluginConfig[$configName])) { + return $pluginConfig[$configName]; + } + } + + return null; + } + /** * Passes all not satisfiable method calls to Pico * From 2a3e2fa5769cd901ff9057433924836be83b3586 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Wed, 2 Mar 2016 22:10:49 +0100 Subject: [PATCH 006/188] Fix typos in class docs/exception messages --- lib/Pico.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index a0cb75f..00f9e9e 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -405,7 +405,13 @@ class Pico $this->plugins[$className] = $plugin; } else { // TODO: breaks backward compatibility - //throw new RuntimeException("Unable to load plugin '" . $className . "'"); + /* + $pluginFileName = substr($pluginFile, strlen($this->getPluginsDir())); + throw new RuntimeException( + "Unable to load plugin '" . $className . "' " + . "from '" . $pluginFileName . "'" + ); + */ } } } @@ -431,15 +437,15 @@ class Pico if (class_exists($className)) { $plugin = new $className($this); } else { - throw new RuntimeException("Unable to load plugin '" . $className . "'"); + throw new RuntimeException("Unable to load plugin '" . $className . "': Class not found"); } } $className = get_class($plugin); if (!is_a($plugin, 'PicoPluginInterface')) { throw new RuntimeException( - "Manually loaded plugins must implement 'PicoPluginInterface', " - . "'" . $className . "' given" + "Unable to load plugin '" . $className . "': " + . "Manually loaded plugins must implement 'PicoPluginInterface'" ); } @@ -1382,7 +1388,7 @@ class Pico * * Deprecated events (as used by plugins not implementing * {@link PicoPluginInterface}) are triggered by {@link PicoDeprecated}. - * You MUST NOT trigger events of Pico's core through a plugin! + * You MUST NOT trigger events of Pico's core with a plugin! * * @see PicoPluginInterface * @see AbstractPicoPlugin From 43705d0f76eb87cb91072a527983f0ce28b39da6 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 6 Mar 2016 00:29:40 +0100 Subject: [PATCH 007/188] Minor code refactoring --- lib/Pico.php | 54 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index ea4c157..025b537 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -657,8 +657,11 @@ class Pico */ protected function discoverRequestFile() { + $contentDir = $this->getConfig('content_dir'); + $contentExt = $this->getConfig('content_ext'); + if (empty($this->requestUrl)) { - $this->requestFile = $this->getConfig('content_dir') . 'index' . $this->getConfig('content_ext'); + $this->requestFile = $contentDir . 'index' . $contentExt; } else { // prevent content_dir breakouts using malicious request URLs // we don't use realpath() here because we neither want to check for file existance @@ -680,24 +683,24 @@ class Pico } if (empty($requestFileParts)) { - $this->requestFile = $this->getConfig('content_dir') . 'index' . $this->getConfig('content_ext'); + $this->requestFile = $contentDir . 'index' . $contentExt; return; } // discover the content file to serve // Note: $requestFileParts neither contains a trailing nor a leading slash - $this->requestFile = $this->getConfig('content_dir') . implode('/', $requestFileParts); + $this->requestFile = $contentDir . implode('/', $requestFileParts); if (is_dir($this->requestFile)) { // if no index file is found, try a accordingly named file in the previous dir // if this file doesn't exist either, show the 404 page, but assume the index // file as being requested (maintains backward compatibility to Pico < 1.0) - $indexFile = $this->requestFile . '/index' . $this->getConfig('content_ext'); - if (file_exists($indexFile) || !file_exists($this->requestFile . $this->getConfig('content_ext'))) { + $indexFile = $this->requestFile . '/index' . $contentExt; + if (file_exists($indexFile) || !file_exists($this->requestFile . $contentExt)) { $this->requestFile = $indexFile; return; } } - $this->requestFile .= $this->getConfig('content_ext'); + $this->requestFile .= $contentExt; } } @@ -737,25 +740,26 @@ class Pico { $contentDir = $this->getConfig('content_dir'); $contentDirLength = strlen($contentDir); + $contentExt = $this->getConfig('content_ext'); if (substr($file, 0, $contentDirLength) === $contentDir) { $errorFileDir = substr($file, $contentDirLength); while ($errorFileDir !== '.') { $errorFileDir = dirname($errorFileDir); - $errorFile = $errorFileDir . '/404' . $this->getConfig('content_ext'); + $errorFile = $errorFileDir . '/404' . $contentExt; - if (file_exists($this->getConfig('content_dir') . $errorFile)) { - return $this->loadFileContent($this->getConfig('content_dir') . $errorFile); + if (file_exists($contentDir . $errorFile)) { + return $this->loadFileContent($contentDir . $errorFile); } } - } elseif (file_exists($this->getConfig('content_dir') . '404' . $this->getConfig('content_ext'))) { + } elseif (file_exists($contentDir . '404' . $contentExt)) { // provided that the requested file is not in the regular // content directory, fallback to Pico's global `404.md` - return $this->loadFileContent($this->getConfig('content_dir') . '404' . $this->getConfig('content_ext')); + return $this->loadFileContent($contentDir . '404' . $contentExt); } - $errorFile = $this->getConfig('content_dir') . '404' . $this->getConfig('content_ext'); + $errorFile = $contentDir . '404' . $contentExt; throw new RuntimeException('Required "' . $errorFile . '" not found'); } @@ -989,25 +993,30 @@ class Pico */ protected function readPages() { + $contentDir = $this->getConfig('content_dir'); + $contentDirLength = strlen($contentDir); + $contentExt = $this->getConfig('content_ext'); + $contentExtLength = strlen($contentExt); + $this->pages = array(); - $files = $this->getFiles($this->getConfig('content_dir'), $this->getConfig('content_ext'), Pico::SORT_NONE); + $files = $this->getFiles($contentDir, $contentExt, Pico::SORT_NONE); foreach ($files as $i => $file) { // skip 404 page - if (basename($file) === '404' . $this->getConfig('content_ext')) { + if (basename($file) === '404' . $contentExt) { unset($files[$i]); continue; } - $id = substr($file, strlen($this->getConfig('content_dir')), -strlen($this->getConfig('content_ext'))); + $id = substr($file, $contentDirLength, -$contentExtLength); // drop inaccessible pages (e.g. drop "sub.md" if "sub/index.md" exists) - $conflictFile = $this->getConfig('content_dir') . $id . '/index' . $this->getConfig('content_ext'); + $conflictFile = $contentDir . $id . '/index' . $contentExt; if (in_array($conflictFile, $files, true)) { continue; } $url = $this->getPageUrl($id); - if ($file != $this->requestFile) { + if ($file !== $this->requestFile) { $rawContent = file_get_contents($file); $headers = $this->getMetaHeaders(); @@ -1126,8 +1135,7 @@ class Pico return; } - $contentExt = $this->getConfig('content_ext'); - $currentPageId = substr($this->requestFile, $contentDirLength, -strlen($contentExt)); + $currentPageId = substr($this->requestFile, $contentDirLength, -strlen($this->getConfig('content_ext'))); $currentPageIndex = array_search($currentPageId, $pageIds); if ($currentPageIndex !== false) { $this->currentPage = &$this->pages[$currentPageId]; @@ -1287,7 +1295,7 @@ class Pico $protocol . "://" . $_SERVER['HTTP_HOST'] . rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/'; - return $this->getConfig('base_url'); + return $this->config['base_url']; } /** @@ -1303,7 +1311,7 @@ class Pico } $this->config['rewrite_url'] = (isset($_SERVER['PICO_URL_REWRITING']) && $_SERVER['PICO_URL_REWRITING']); - return $this->getConfig('rewrite_url'); + return $this->config['rewrite_url']; } /** @@ -1363,7 +1371,7 @@ class Pico foreach ($files as $file) { // exclude hidden files/dirs starting with a .; this also excludes the special dirs . and .. // exclude files ending with a ~ (vim/nano backup) or # (emacs backup) - if ((substr($file, 0, 1) === '.') || in_array(substr($file, -1), array('~', '#'))) { + if (($file[0] === '.') || in_array(substr($file, -1), array('~', '#'))) { continue; } @@ -1394,7 +1402,7 @@ class Pico $path = $this->getRootDir() . $path; } } else { - if (substr($path, 0, 1) !== '/') { + if ($path[0] !== '/') { $path = $this->getRootDir() . $path; } } From 479926eeb46bb6eedc0f908d05456e6557400b90 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 6 Mar 2016 00:38:51 +0100 Subject: [PATCH 008/188] Add Pico::VERSION constant --- content-sample/index.md | 4 ++-- lib/Pico.php | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/content-sample/index.md b/content-sample/index.md index 209369b..b1c8529 100644 --- a/content-sample/index.md +++ b/content-sample/index.md @@ -5,8 +5,8 @@ Description: Pico is a stupidly simple, blazing fast, flat file CMS. ## Welcome to Pico -Congratulations, you have successfully installed [Pico](http://picocms.org/). -%meta.description% +Congratulations, you have successfully installed [Pico](http://picocms.org/) +%version%. %meta.description% ## Creating Content diff --git a/lib/Pico.php b/lib/Pico.php index 025b537..df494d4 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -26,6 +26,13 @@ */ class Pico { + /** + * Pico version + * + * @var string + */ + const VERSION = '1.1.0-dev'; + /** * Sort files in alphabetical ascending order * @@ -907,6 +914,9 @@ class Pico . "(?:(.*?)(?:\r)?\n)?(?(2)\*\/|---)[[:blank:]]*(?:(?:\r)?\n|$)/s"; $content = preg_replace($metaHeaderPattern, '', $rawContent, 1); + // replace %version% + $content = str_replace('%version%', static::VERSION, $content); + // replace %site_title% $content = str_replace('%site_title%', $this->getConfig('site_title'), $content); @@ -1267,6 +1277,7 @@ class Pico 'current_page' => $this->currentPage, 'next_page' => $this->nextPage, 'is_front_page' => ($this->requestFile === $frontPage), + 'version' => static::VERSION ); } From 245cd15770fc678c3b1990741ba93d01e9beb0f3 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 6 Mar 2016 00:49:45 +0100 Subject: [PATCH 009/188] Refactor Pico::prepareFileContent() for better performance --- lib/Pico.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index df494d4..1f4d0eb 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -915,38 +915,36 @@ class Pico $content = preg_replace($metaHeaderPattern, '', $rawContent, 1); // replace %version% - $content = str_replace('%version%', static::VERSION, $content); + $variables['%version%'] = static::VERSION; // replace %site_title% - $content = str_replace('%site_title%', $this->getConfig('site_title'), $content); + $variables['%site_title%'] = $this->getConfig('site_title'); // replace %base_url% if ($this->isUrlRewritingEnabled()) { // always use `%base_url%?sub/page` syntax for internal links // we'll replace the links accordingly, depending on enabled rewriting - $content = str_replace('%base_url%?', $this->getBaseUrl(), $content); + $variables['%base_url%?'] = $this->getBaseUrl(); } else { // actually not necessary, but makes the URL look a little nicer - $content = str_replace('%base_url%?', $this->getBaseUrl() . '?', $content); + $variables['%base_url%?'] = $this->getBaseUrl() . '?'; } - $content = str_replace('%base_url%', rtrim($this->getBaseUrl(), '/'), $content); + $variables['%base_url%'] = rtrim($this->getBaseUrl(), '/'); // replace %theme_url% $themeUrl = $this->getBaseUrl() . basename($this->getThemesDir()) . '/' . $this->getConfig('theme'); - $content = str_replace('%theme_url%', $themeUrl, $content); + $variables['%theme_url%'] = $themeUrl; // replace %meta.*% if (!empty($meta)) { - $metaKeys = $metaValues = array(); foreach ($meta as $metaKey => $metaValue) { if (is_scalar($metaValue) || ($metaValue === null)) { - $metaKeys[] = '%meta.' . $metaKey . '%'; - $metaValues[] = strval($metaValue); + $variables['%meta.' . $metaKey . '%'] = strval($metaValue); } } - $content = str_replace($metaKeys, $metaValues, $content); } + $content = str_replace(array_keys($variables), $variables, $content); return $content; } From 5bb1c325ff1229f0114dca3796a214c61bebbfc4 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 6 Mar 2016 00:54:36 +0100 Subject: [PATCH 010/188] Add onSinglePageLoading event; allow skipping pages in onSinglePageLoaded --- lib/Pico.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index 1f4d0eb..5ab4e7f 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -1017,6 +1017,9 @@ class Pico $id = substr($file, $contentDirLength, -$contentExtLength); + // trigger onSinglePageLoading event + $this->triggerEvent('onSinglePageLoading', array(&$id)); + // drop inaccessible pages (e.g. drop "sub.md" if "sub/index.md" exists) $conflictFile = $contentDir . $id . '/index' . $contentExt; if (in_array($conflictFile, $files, true)) { @@ -1061,10 +1064,12 @@ class Pico unset($rawContent, $meta); - // trigger event + // trigger onSinglePageLoaded event $this->triggerEvent('onSinglePageLoaded', array(&$page)); - $this->pages[$id] = $page; + if ($page !== null) { + $this->pages[$id] = $page; + } } } From 75d5081bfbe6c9c056e859e2b455c9b44c2e5d6c Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 6 Mar 2016 20:06:24 +0100 Subject: [PATCH 011/188] Use scope isolated includes for plugins & config --- lib/Pico.php | 20 ++++++++++++++++++-- plugins/00-PicoDeprecated.php | 10 +++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index 5ab4e7f..10465f2 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -398,10 +398,18 @@ class Pico */ protected function loadPlugins() { + // scope isolated require_once() + $includeClosure = function ($pluginFile) { + require_once($pluginFile); + }; + if (PHP_VERSION_ID >= 50400) { + $includeClosure = $includeClosure->bindTo(null); + } + $this->plugins = array(); $pluginFiles = $this->getFiles($this->getPluginsDir(), '.php'); foreach ($pluginFiles as $pluginFile) { - require_once($pluginFile); + $includeClosure($pluginFile); $className = preg_replace('/^[0-9]+-/', '', basename($pluginFile, '.php')); if (class_exists($className)) { @@ -508,7 +516,15 @@ class Pico { $config = null; if (file_exists($this->getConfigDir() . 'config.php')) { - require($this->getConfigDir() . 'config.php'); + // scope isolated require() + $includeClosure = function ($configFile) { + require($configFile); + }; + if (PHP_VERSION_ID >= 50400) { + $includeClosure = $includeClosure->bindTo(null); + } + + $includeClosure($this->getConfigDir() . 'config.php'); } $defaultConfig = array( diff --git a/plugins/00-PicoDeprecated.php b/plugins/00-PicoDeprecated.php index 1077800..c5d9850 100644 --- a/plugins/00-PicoDeprecated.php +++ b/plugins/00-PicoDeprecated.php @@ -165,10 +165,18 @@ class PicoDeprecated extends AbstractPicoPlugin protected function loadRootDirConfig(array &$realConfig) { if (file_exists($this->getRootDir() . 'config.php')) { + // scope isolated require() + $includeClosure = function ($configFile) { + require($configFile); + }; + if (PHP_VERSION_ID >= 50400) { + $includeClosure = $includeClosure->bindTo(null); + } + // config.php in Pico::$rootDir is deprecated // use config.php in Pico::$configDir instead $config = null; - require($this->getRootDir() . 'config.php'); + $includeClosure($this->getRootDir() . 'config.php'); if (is_array($config)) { if (isset($config['base_url'])) { From cd74b681f5dea400bfb28f196230e07560f21af7 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 6 Mar 2016 20:47:25 +0100 Subject: [PATCH 012/188] Fix scope isolated config includes --- lib/Pico.php | 2 +- plugins/00-PicoDeprecated.php | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index 10465f2..59e4eae 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -517,7 +517,7 @@ class Pico $config = null; if (file_exists($this->getConfigDir() . 'config.php')) { // scope isolated require() - $includeClosure = function ($configFile) { + $includeClosure = function ($configFile) use (&$config) { require($configFile); }; if (PHP_VERSION_ID >= 50400) { diff --git a/plugins/00-PicoDeprecated.php b/plugins/00-PicoDeprecated.php index c5d9850..59600b4 100644 --- a/plugins/00-PicoDeprecated.php +++ b/plugins/00-PicoDeprecated.php @@ -165,8 +165,10 @@ class PicoDeprecated extends AbstractPicoPlugin protected function loadRootDirConfig(array &$realConfig) { if (file_exists($this->getRootDir() . 'config.php')) { + $config = null; + // scope isolated require() - $includeClosure = function ($configFile) { + $includeClosure = function ($configFile) use (&$config) { require($configFile); }; if (PHP_VERSION_ID >= 50400) { @@ -175,7 +177,6 @@ class PicoDeprecated extends AbstractPicoPlugin // config.php in Pico::$rootDir is deprecated // use config.php in Pico::$configDir instead - $config = null; $includeClosure($this->getRootDir() . 'config.php'); if (is_array($config)) { From 988a23fd024b065811185fca517939e59d561473 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 6 Mar 2016 20:54:58 +0100 Subject: [PATCH 013/188] Modular config: Load config from any config/*.config.php Resolves #330 After loading the `config/config.php`, Pico proceeds with any existing `config/*.config.php` in alphabetical order. The file order is crucial: Config values which has been set already, cannot be overwritten by a succeeding file. This is also true for arrays, i.e. when specifying `$config['test'] = array('foo' => 'bar')` in `config/a.config.php` and `$config['test'] = array('baz' => 42)` in `config/b.config.php`, `$config['test']['baz']` will be undefined --- lib/Pico.php | 50 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index 59e4eae..25e4938 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -506,7 +506,15 @@ class Pico } /** - * Loads the config.php from Pico::$configDir + * Loads the config.php and any *.config.php from Pico::$configDir + * + * After loading the {@path "config/config.php"}, Pico proceeds with any + * existing {@path "config/*.config.php"} in alphabetical order. The file + * order is crucial: Config values which has been set already, cannot be + * overwritten by a succeeding file. This is also true for arrays, + * i.e. when specifying `$config['test'] = array('foo' => 'bar')` in + * `config/a.config.php` and `$config['test'] = array('baz' => 42)` in + * `config/b.config.php`, `$config['test']['baz']` will be undefined! * * @see Pico::setConfig() * @see Pico::getConfig() @@ -514,20 +522,33 @@ class Pico */ protected function loadConfig() { - $config = null; - if (file_exists($this->getConfigDir() . 'config.php')) { - // scope isolated require() - $includeClosure = function ($configFile) use (&$config) { - require($configFile); - }; - if (PHP_VERSION_ID >= 50400) { - $includeClosure = $includeClosure->bindTo(null); - } - - $includeClosure($this->getConfigDir() . 'config.php'); + // scope isolated require() + $includeClosure = function ($configFile) { + require($configFile); + return (isset($config) && is_array($config)) ? $config : array(); + }; + if (PHP_VERSION_ID >= 50400) { + $includeClosure = $includeClosure->bindTo(null); } - $defaultConfig = array( + // load main config file (config/config.php) + $this->config = is_array($this->config) ? $this->config : array(); + if (file_exists($this->getConfigDir() . 'config.php')) { + $this->config += $includeClosure($this->getConfigDir() . 'config.php'); + } + + // merge $config of config/*.config.php files + $configFiles = glob($this->getConfigDir() . '?*.config.php', GLOB_MARK); + if ($configFiles) { + foreach ($configFiles as $configFile) { + if (substr($configFile, -1) !== '/') { + $this->config += $includeClosure($configFile); + } + } + } + + // merge default config + $this->config += array( 'site_title' => 'Pico', 'base_url' => '', 'rewrite_url' => null, @@ -541,9 +562,6 @@ class Pico 'timezone' => '' ); - $this->config = is_array($this->config) ? $this->config : array(); - $this->config += is_array($config) ? $config + $defaultConfig : $defaultConfig; - if (empty($this->config['base_url'])) { $this->config['base_url'] = $this->getBaseUrl(); } else { From dc621b24cd182bf245e99b99272fd26a5832ba1c Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 6 Mar 2016 21:00:00 +0100 Subject: [PATCH 014/188] Improve class docs of Pico::loadConfig() --- lib/Pico.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index 25e4938..12bd993 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -509,12 +509,12 @@ class Pico * Loads the config.php and any *.config.php from Pico::$configDir * * After loading the {@path "config/config.php"}, Pico proceeds with any - * existing {@path "config/*.config.php"} in alphabetical order. The file - * order is crucial: Config values which has been set already, cannot be - * overwritten by a succeeding file. This is also true for arrays, - * i.e. when specifying `$config['test'] = array('foo' => 'bar')` in - * `config/a.config.php` and `$config['test'] = array('baz' => 42)` in - * `config/b.config.php`, `$config['test']['baz']` will be undefined! + * existing `config/*.config.php` in alphabetical order. The file order is + * crucial: Config values which has been set already, cannot be overwritten + * by a succeeding file. This is also true for arrays, i.e. when specifying + * `$config['test'] = array('foo' => 'bar')` in `config/a.config.php` and + * `$config['test'] = array('baz' => 42)` in `config/b.config.php`, + * `$config['test']['baz']` will be undefined! * * @see Pico::setConfig() * @see Pico::getConfig() From 3d11b8a9799df2546d492044aa97dd1b16f7a1f5 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Fri, 11 Mar 2016 19:07:45 +0100 Subject: [PATCH 015/188] Replace is_a() function calls with instanceof operator --- lib/AbstractPicoPlugin.php | 4 ++-- lib/Pico.php | 4 ++-- lib/PicoTwigExtension.php | 8 ++++---- plugins/00-PicoDeprecated.php | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/AbstractPicoPlugin.php b/lib/AbstractPicoPlugin.php index 68587f0..2b7a56c 100644 --- a/lib/AbstractPicoPlugin.php +++ b/lib/AbstractPicoPlugin.php @@ -199,7 +199,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); @@ -272,7 +272,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; diff --git a/lib/Pico.php b/lib/Pico.php index 12bd993..eada6de 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -457,7 +457,7 @@ class Pico } $className = get_class($plugin); - if (!is_a($plugin, 'PicoPluginInterface')) { + if (!($plugin instanceof PicoPluginInterface)) { throw new RuntimeException( "Unable to load plugin '" . $className . "': " . "Manually loaded plugins must implement 'PicoPluginInterface'" @@ -1477,7 +1477,7 @@ class Pico foreach ($this->plugins as $plugin) { // only trigger events for plugins that implement PicoPluginInterface // deprecated events (plugins for Pico 0.9 and older) will be triggered by `PicoDeprecated` - if (is_a($plugin, 'PicoPluginInterface')) { + if ($plugin instanceof PicoPluginInterface) { $plugin->handleEvent($eventName, $params); } } diff --git a/lib/PicoTwigExtension.php b/lib/PicoTwigExtension.php index c249ff4..383330a 100644 --- a/lib/PicoTwigExtension.php +++ b/lib/PicoTwigExtension.php @@ -102,7 +102,7 @@ class PicoTwigExtension extends Twig_Extension */ public function mapFilter($var, $mapKeyPath) { - if (!is_array($var) && (!is_object($var) || !is_a($var, 'Traversable'))) { + if (!is_array($var) && (!is_object($var) || !($var instanceof Traversable))) { throw new Twig_Error_Runtime(sprintf( 'The map filter only works with arrays or "Traversable", got "%s"', is_object($var) ? get_class($var) : gettype($var) @@ -141,7 +141,7 @@ class PicoTwigExtension extends Twig_Extension */ public function sortByFilter($var, $sortKeyPath, $fallback = 'bottom') { - if (is_object($var) && is_a($var, 'Traversable')) { + if (is_object($var) && ($var instanceof Traversable)) { $var = iterator_to_array($var, true); } elseif (!is_array($var)) { throw new Twig_Error_Runtime(sprintf( @@ -204,9 +204,9 @@ class PicoTwigExtension extends Twig_Extension foreach ($keyPath as $key) { if (is_object($var)) { - if (is_a($var, 'ArrayAccess')) { + if ($var instanceof ArrayAccess) { // use ArrayAccess, see below - } elseif (is_a($var, 'Traversable')) { + } elseif ($var instanceof Traversable) { $var = iterator_to_array($var); } elseif (isset($var->{$key})) { $var = $var->{$key}; diff --git a/plugins/00-PicoDeprecated.php b/plugins/00-PicoDeprecated.php index 59600b4..e2db93a 100644 --- a/plugins/00-PicoDeprecated.php +++ b/plugins/00-PicoDeprecated.php @@ -68,7 +68,7 @@ class PicoDeprecated extends AbstractPicoPlugin { if (!empty($plugins)) { foreach ($plugins as $plugin) { - if (!is_a($plugin, 'PicoPluginInterface')) { + if (!($plugin instanceof PicoPluginInterface)) { // the plugin doesn't implement PicoPluginInterface; it uses deprecated events // enable PicoDeprecated if it hasn't be explicitly enabled/disabled yet if (!$this->isStatusChanged()) { From d56d3f8c8ca7d178d0861668a7258b64e30642db Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sat, 12 Mar 2016 00:18:49 +0100 Subject: [PATCH 016/188] Revert "Default theme: Move elements into Twig blocks" This reverts commit a3fa373119c12cae834bcfab758a11b036234c76. At first glance this adds flexibility, but at the moment it is impossible with Twig to ensure the existance of a block. As a result, custom themes may break the plugin. A custom theme should overwrite a plugin's template explicitly. --- themes/default/index.twig | 90 ++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 53 deletions(-) diff --git a/themes/default/index.twig b/themes/default/index.twig index 1b9f2ce..37f8f16 100644 --- a/themes/default/index.twig +++ b/themes/default/index.twig @@ -1,67 +1,51 @@ - + - {% block documentHeader %} - {% block title %}{% if meta.title %}{{ meta.title }} | {% endif %}{{ site_title }}{% endblock %} - {% if meta.description %} - - {% endif %}{% if meta.robots %} - - {% endif %} - {% endblock %} + {% if meta.title %}{{ meta.title }} | {% endif %}{{ site_title }} + {% if meta.description %} + + {% endif %}{% if meta.robots %} + + {% endif %} - {% block stylesheets %} - - - {% endblock %} + + - {% block javascript %} - - {% endblock %} + - {% block pageHeader %} - - {% endblock %} + - {% block pageContent %} -
-
- {% block content content %} -
-
- {% endblock %} +
+
+ {{ content }} +
+
- {% block pageFooter %} - - {% endblock %} + From b133f6dae5db4cab49178ea4e1e9267e01e8ad88 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Fri, 22 Apr 2016 14:23:46 +0200 Subject: [PATCH 017/188] Add Pico::VERSION_ID (like PHP_VERSION_ID) --- lib/Pico.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/Pico.php b/lib/Pico.php index eada6de..652570d 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -32,6 +32,13 @@ class Pico * @var string */ const VERSION = '1.1.0-dev'; + + /** + * Pico version ID + * + * @var int + */ + const VERSION_ID = 10100; /** * Sort files in alphabetical ascending order From a11912249787713cf769692d5d8032eef1ff1725 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Fri, 22 Apr 2016 14:31:14 +0200 Subject: [PATCH 018/188] Fix coding standard violation --- lib/Pico.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index 652570d..3767081 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -32,10 +32,10 @@ class Pico * @var string */ const VERSION = '1.1.0-dev'; - + /** * Pico version ID - * + * * @var int */ const VERSION_ID = 10100; From 1b3ef7516d5c6bee125bda4e955ea3452f0b5766 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sat, 23 Apr 2016 21:40:30 +0200 Subject: [PATCH 019/188] Drop the "index" part of URLs Closes #347. Thanks @Robby- --- lib/Pico.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index 3767081..d0985dd 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -1387,9 +1387,18 @@ class Pico . (is_object($queryData) ? get_class($queryData) : gettype($queryData)) . ' given' ); } + + // drop "index" + if ($page === 'index') { + $page = ''; + } elseif (($pagePathLength = strrpos($page, '/')) !== false) { + if (substr($page, $pagePathLength + 1) === 'index') { + $page = substr($page, 0, $pagePathLength); + } + } + if (!empty($queryData)) { - $page = !empty($page) ? $page : 'index'; - $queryData = $this->isUrlRewritingEnabled() ? '?' . $queryData : '&' . $queryData; + $queryData = ($this->isUrlRewritingEnabled() || empty($page)) ? '?' . $queryData : '&' . $queryData; } if (empty($page)) { From d19621a908f110dbc603dfd747e9b9508b7bb286 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 24 Apr 2016 01:22:43 +0200 Subject: [PATCH 020/188] Improve themes dir guessing; add $config['theme_url'] config --- config/config.php.template | 6 ++--- lib/Pico.php | 49 ++++++++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/config/config.php.template b/config/config.php.template index 0d44037..f04a4bc 100644 --- a/config/config.php.template +++ b/config/config.php.template @@ -10,23 +10,23 @@ * {@path "config/config.php"}, uncomment the line, then make and * save your changes. * - * @author Gilbert Pellegrom * @link http://picocms.org * @license http://opensource.org/licenses/MIT - * @version 1.0 + * @version 1.1 */ /* * BASIC */ // $config['site_title'] = 'Pico'; // Site title -// $config['base_url'] = ''; // Override base URL (e.g. http://example.com) +// $config['base_url'] = ''; // Override base URL (e.g. http://example.com/pico/) // $config['rewrite_url'] = null; // A boolean indicating forced URL rewriting /* * THEME */ // $config['theme'] = 'default'; // Set the theme (defaults to "default") +// $config['theme_url'] = ''; // Override the base URL of the themes folder (e.g. http://example.com/pico/themes/) // $config['twig_config'] = array( // Twig settings // 'cache' => false, // To enable Twig caching change this to a path to a writable directory // 'autoescape' => false, // Auto-escape Twig vars diff --git a/lib/Pico.php b/lib/Pico.php index d0985dd..d19b159 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -22,7 +22,7 @@ * @author Daniel Rudolf * @link * @license The MIT License - * @version 1.0 + * @version 1.1 */ class Pico { @@ -590,6 +590,14 @@ class Pico $this->config['content_dir'] = $this->getAbsolutePath($this->config['content_dir']); } + if (empty($this->config['theme_url'])) { + $this->config['theme_url'] = $this->getBaseThemeUrl(); + } elseif (preg_match('#^[A-Za-z][A-Za-z0-9+\-.]*://#', $this->config['theme_url'])) { + $this->config['theme_url'] = rtrim($this->config['theme_url'], '/') . '/'; + } else { + $this->config['theme_url'] = $this->getBaseUrl() . rtrim($this->config['theme_url'], '/') . '/'; + } + if (empty($this->config['timezone'])) { // explicitly set a default timezone to prevent a E_NOTICE // when no timezone is set; the `date_default_timezone_get()` @@ -973,8 +981,7 @@ class Pico $variables['%base_url%'] = rtrim($this->getBaseUrl(), '/'); // replace %theme_url% - $themeUrl = $this->getBaseUrl() . basename($this->getThemesDir()) . '/' . $this->getConfig('theme'); - $variables['%theme_url%'] = $themeUrl; + $variables['%theme_url%'] = $this->getBaseThemeUrl() . $this->getConfig('theme'); // replace %meta.*% if (!empty($meta)) { @@ -1311,7 +1318,7 @@ class Pico 'base_dir' => rtrim($this->getRootDir(), '/'), 'base_url' => rtrim($this->getBaseUrl(), '/'), 'theme_dir' => $this->getThemesDir() . $this->getConfig('theme'), - 'theme_url' => $this->getBaseUrl() . basename($this->getThemesDir()) . '/' . $this->getConfig('theme'), + 'theme_url' => $this->getBaseThemeUrl() . $this->getConfig('theme'), 'rewrite_url' => $this->isUrlRewritingEnabled(), 'site_title' => $this->getConfig('site_title'), 'meta' => $this->meta, @@ -1410,6 +1417,40 @@ class Pico } } + /** + * Returns the URL of the themes folder of this Pico instance + * + * We assume that the themes folder is a arbitrary deep sub folder of the + * script's base path (i.e. the directory {@path "index.php"} is in resp. + * the `httpdocs` directory). Usually the script's base path is identical + * to {@link Pico::$rootDir}, but this may aberrate when Pico got installed + * as a composer dependency. However, ultimately it allows us to use + * {@link Pico::getBaseUrl()} as origin of the theme URL. Otherwise Pico + * falls back to the basename of {@link Pico::$themesDir} (i.e. assuming + * that `Pico::$themesDir` is `foo/bar/baz`, the base URL of the themes + * folder will be `baz/`; this ensures BC to Pico < 1.1). Pico's base URL + * always gets prepended appropriately. + * + * @return string the URL of the themes folder + */ + public function getBaseThemeUrl() + { + $themeUrl = $this->getConfig('theme_url'); + if (!empty($themeUrl)) { + return $themeUrl; + } + + $basePath = dirname($_SERVER['SCRIPT_FILENAME']) . '/'; + $basePathLength = strlen($basePath); + if (substr($this->getThemesDir(), 0, $basePathLength) === $basePath) { + $this->config['theme_url'] = $this->getBaseUrl() . substr($this->getThemesDir(), $basePathLength); + } else { + $this->config['theme_url'] = $this->getBaseUrl() . basename($this->getThemesDir()) . '/'; + } + + return $this->config['theme_url']; + } + /** * Recursively walks through a directory and returns all containing files * matching the specified file extension From 6465c2b0a99b5262f25083af70b94668c1cac6af Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 24 Apr 2016 20:11:05 +0200 Subject: [PATCH 021/188] Support REQUEST_URI routing method With Pico 1.0 you had to setup URL rewriting (e.g. using `mod_rewrite` on Apache) in a way that rewritten URLs follow the `QUERY_STRING` principles. Starting with version 1.1, Pico additionally supports the `REQUEST_URI` routing method, what allows you to simply rewrite all requests to just `index.php`. Pico then reads the requested page from the `REQUEST_URI` environment variable provided by the webserver. Please note that `QUERY_STRING` takes precedence over `REQUEST_URI`. --- .htaccess | 11 +++++++--- lib/Pico.php | 58 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/.htaccess b/.htaccess index 192f3a4..875db33 100644 --- a/.htaccess +++ b/.htaccess @@ -1,13 +1,18 @@ RewriteEngine On - #May be required to access sub-directories + # May be required to access sub-directories #RewriteBase / + + # Deny access to internal dirs by passing the URL to Pico + RewriteRule ^(.git|config|content|content-sample|lib|vendor)(/|$) index.php [L] + + # Enable URL rewriting RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule ^(.*)$ index.php?$1 [L,QSA] - RewriteRule ^(config|content|content-sample|lib|vendor)/.* - [R=404,L] + RewriteRule ^ index.php [L] + # Let Pico know about available URL rewriting SetEnv PICO_URL_REWRITING 1 diff --git a/lib/Pico.php b/lib/Pico.php index d19b159..c814ea1 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -656,13 +656,19 @@ class Pico /** * Evaluates the requested URL * - * Pico 1.0 uses the `QUERY_STRING` routing method (e.g. `/pico/?sub/page`) + * Pico uses the `QUERY_STRING` routing method (e.g. `/pico/?sub/page`) * to support SEO-like URLs out-of-the-box with any webserver. You can - * still setup URL rewriting (e.g. using `mod_rewrite` on Apache) to - * basically remove the `?` from URLs, but your rewritten URLs must follow - * the new `QUERY_STRING` principles. URL rewriting requires some special - * configuration on your webserver, but this should be "basic work" for - * any webmaster... + * still setup URL rewriting to basically remove the `?` from URLs. + * However, URL rewriting requires some special configuration of your + * webserver, but this should be "basic work" for any webmaster... + * + * With Pico 1.0 you had to setup URL rewriting (e.g. using `mod_rewrite` + * on Apache) in a way that rewritten URLs follow the `QUERY_STRING` + * principles. Starting with version 1.1, Pico additionally supports the + * `REQUEST_URI` routing method, what allows you to simply rewrite all + * requests to just `index.php`. Pico then reads the requested page from + * the `REQUEST_URI` environment variable provided by the webserver. + * Please note that `QUERY_STRING` takes precedence over `REQUEST_URI`. * * Pico 0.9 and older required Apache with `mod_rewrite` enabled, thus old * plugins, templates and contents may require you to enable URL rewriting @@ -675,23 +681,43 @@ class Pico * enabled URL rewriting. In content files you can use the `%base_url%` * variable; e.g. `%base_url%?sub/page` will be replaced accordingly. * + * Heads up! Pico always interprets the first parameter as name of the + * requested page (provided that the parameter has no value). According to + * that you MUST NOT call Pico with a parameter without value as first + * parameter (e.g. http://example.com/pico/?someBooleanParam), otherwise + * Pico interprets `someBooleanParam` as name of the requested page. Use + * `/pico/?someBooleanParam=` or `/pico/?index&someBooleanParam` instead. + * * @see Pico::getRequestUrl() * @return void */ protected function evaluateRequestUrl() { // use QUERY_STRING; e.g. /pico/?sub/page - // if you want to use rewriting, you MUST make your rules to - // rewrite the URLs to follow the QUERY_STRING method - // - // Note: you MUST NOT call the index page with /pico/?someBooleanParameter; - // use /pico/?someBooleanParameter= or /pico/?index&someBooleanParameter instead - $pathComponent = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; - if (($pathComponentLength = strpos($pathComponent, '&')) !== false) { - $pathComponent = substr($pathComponent, 0, $pathComponentLength); + $pathComponent = $_SERVER['QUERY_STRING']; + if (!empty($pathComponent)) { + if (($pathComponentLength = strpos($pathComponent, '&')) !== false) { + $pathComponent = substr($pathComponent, 0, $pathComponentLength); + } + if (strpos($pathComponent, '=') === false) { + $this->requestUrl = trim(rawurldecode($pathComponent), '/'); + } + } + + // use REQUEST_URI (requires URL rewriting); e.g. /pico/sub/page + if (($this->requestUrl === null) && $this->isUrlRewritingEnabled()) { + $basePath = dirname($_SERVER['SCRIPT_NAME']) . '/'; + $basePathLength = strlen($basePath); + + $requestUri = $_SERVER['REQUEST_URI']; + if (substr($requestUri, 0, $basePathLength) === $basePath) { + $requestUri = substr($requestUri, $basePathLength); + if (($requestUriLength = strpos($requestUri, '?')) !== false) { + $requestUri = substr($requestUri, 0, $requestUriLength); + } + $this->requestUrl = rtrim(rawurldecode($requestUri), '/'); + } } - $this->requestUrl = (strpos($pathComponent, '=') === false) ? rawurldecode($pathComponent) : ''; - $this->requestUrl = trim($this->requestUrl, '/'); } /** From 6c4f69c1079ce9cf219b450b4f3913ac0fc5bac7 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 24 Apr 2016 20:49:06 +0200 Subject: [PATCH 022/188] Update inline user docs to reflect 6465c2b0a99b5262f25083af70b94668c1cac6af --- content-sample/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content-sample/index.md b/content-sample/index.md index 086c56c..b498bc0 100644 --- a/content-sample/index.md +++ b/content-sample/index.md @@ -284,13 +284,13 @@ still shows no rewritten URLs, force URL rewriting by setting `$config['rewrite_url'] = true;` in your `config/config.php`. If you're using Nginx, you can use the following configuration to enable -URL rewriting. Don't forget to adjust the path (`/pico`; line `1` and `4`) +URL rewriting. Don't forget to adjust the path (`/pico/`; line `1` and `3`) to match your installation directory. You can then enable URL rewriting by setting `$config['rewrite_url'] = true;` in your `config/config.php`. - location ~ ^/pico(.*) { + location /pico/ { index index.php; - try_files $uri $uri/ /pico/?$1&$args; + try_files $uri $uri/ /pico/$is_args$args; } ## Documentation From e01044319ad52ae3da541ba80ed8bb55c2921dcb Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Sun, 24 Apr 2016 21:13:47 +0200 Subject: [PATCH 023/188] Build system: Use dynamic phpDoc title --- .phpdoc.xml | 2 +- _build/deploy-phpdoc-branch.sh | 6 +++++- _build/deploy-phpdoc-release.sh | 6 +++++- lib/Pico.php | 2 ++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.phpdoc.xml b/.phpdoc.xml index c46b6da..94996fb 100644 --- a/.phpdoc.xml +++ b/.phpdoc.xml @@ -1,6 +1,6 @@ - <![CDATA[Pico 1.0 API Documentation]]> + <![CDATA[Pico API Documentation]]> _build/phpdoc.cache diff --git a/_build/deploy-phpdoc-branch.sh b/_build/deploy-phpdoc-branch.sh index c4a94a9..720ee15 100755 --- a/_build/deploy-phpdoc-branch.sh +++ b/_build/deploy-phpdoc-branch.sh @@ -18,6 +18,10 @@ fi DEPLOYMENT_ID="${TRAVIS_BRANCH//\//_}" DEPLOYMENT_DIR="$TRAVIS_BUILD_DIR/_build/deploy-$DEPLOYMENT_ID.git" +# get current Pico milestone +VERSION="$(php -r 'require_once(__DIR__ . "/lib/Pico.php"); echo Pico::VERSION;')" +MILESTONE="Pico$([[ "$VERSION" =~ ^([0-9]+\.[0-9]+)\. ]] && echo " ${BASH_REMATCH[1]}")" + # clone repo echo "Cloning repo..." git clone --branch="gh-pages" "https://github.com/$TRAVIS_REPO_SLUG.git" "$DEPLOYMENT_DIR" @@ -33,7 +37,7 @@ github-setup.sh generate-phpdoc.sh \ "$TRAVIS_BUILD_DIR/.phpdoc.xml" \ "$DEPLOYMENT_DIR/phpDoc/$DEPLOYMENT_ID.cache" "$DEPLOYMENT_DIR/phpDoc/$DEPLOYMENT_ID" \ - "Pico 1.0 API Documentation ($TRAVIS_BRANCH branch)" + "$MILESTONE API Documentation ($TRAVIS_BRANCH branch)" [ $? -eq 0 ] || exit 1 [ -n "$(git status --porcelain "$DEPLOYMENT_DIR/phpDoc/$DEPLOYMENT_ID.cache")" ] || exit 0 diff --git a/_build/deploy-phpdoc-release.sh b/_build/deploy-phpdoc-release.sh index 8d3410e..6ab1042 100755 --- a/_build/deploy-phpdoc-release.sh +++ b/_build/deploy-phpdoc-release.sh @@ -29,10 +29,14 @@ github-setup.sh # generate phpDocs if [ "$DEPLOY_PHPDOC_RELEASES" == "true" ]; then + # get current Pico milestone + MILESTONE="Pico$([[ "$TRAVIS_TAG" =~ ^v([0-9]+\.[0-9]+)\. ]] && echo " ${BASH_REMATCH[1]}")" + + # generate phpDocs generate-phpdoc.sh \ "$TRAVIS_BUILD_DIR/.phpdoc.xml" \ "-" "$DEPLOYMENT_DIR/phpDoc/$DEPLOYMENT_ID" \ - "Pico 1.0 API Documentation ($TRAVIS_TAG)" + "$MILESTONE API Documentation ($TRAVIS_TAG)" [ $? -eq 0 ] || exit 1 # commit phpDocs diff --git a/lib/Pico.php b/lib/Pico.php index b73400e..156d0fe 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -4,6 +4,7 @@ * Pico * * Pico is a stupidly simple, blazing fast, flat file CMS. + * * - Stupidly Simple: Pico makes creating and maintaining a * website as simple as editing text files. * - Blazing Fast: Pico is seriously lightweight and doesn't @@ -16,6 +17,7 @@ * for powerful and flexible themes. * - Open Source: Pico is completely free and open source, * released under the MIT license. + * * See for more info. * * @author Gilbert Pellegrom From 0f8deda6a310da3f48bc72fe0010212653a359ac Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Wed, 27 Apr 2016 21:07:59 +0200 Subject: [PATCH 024/188] Update .htaccess Sync with Pico 1.0.3; see ee5b4f0 --- .htaccess | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.htaccess b/.htaccess index 875db33..54cbc9b 100644 --- a/.htaccess +++ b/.htaccess @@ -3,8 +3,9 @@ # May be required to access sub-directories #RewriteBase / - # Deny access to internal dirs by passing the URL to Pico - RewriteRule ^(.git|config|content|content-sample|lib|vendor)(/|$) index.php [L] + # Deny access to internal dirs and files by passing the URL to Pico + RewriteRule ^(\.git|config|content|content-sample|lib|vendor)(/|$) index.php [L] + RewriteRule ^(CHANGELOG.md|composer.(json|lock)) index.php [L] # Enable URL rewriting RewriteCond %{REQUEST_FILENAME} !-f From eeb43e131faa9b2bc64853e74506baf22c99ab7b Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Fri, 6 May 2016 23:19:40 +0200 Subject: [PATCH 025/188] Pico::prepareFileContent(): Declare $variables variable --- lib/Pico.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Pico.php b/lib/Pico.php index 156d0fe..bcea0f2 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -997,6 +997,8 @@ class Pico */ public function prepareFileContent($rawContent, array $meta) { + $variables = array(); + // remove meta header $metaHeaderPattern = "/^(\/(\*)|---)[[:blank:]]*(?:\r)?\n" . "(?:(.*?)(?:\r)?\n)?(?(2)\*\/|---)[[:blank:]]*(?:(?:\r)?\n|$)/s"; From f251bc83ec995d05e0e33140df22a451c0cfe4af Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Mon, 20 Jun 2016 22:38:24 +0200 Subject: [PATCH 026/188] Update composer.json: Use Symfony YAML 3.1 and later --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5894970..0a354a4 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "php": ">=5.3.6", "twig/twig": "^1.18", "erusev/parsedown-extra": "^0.7", - "symfony/yaml" : "^2.3" + "symfony/yaml" : "^3.1" }, "require-dev" : { "phpdocumentor/phpdocumentor": "^2.8", From 81dddc94cf0c8d08ec3def69a628197a4c81aea8 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Mon, 20 Jun 2016 22:46:57 +0200 Subject: [PATCH 027/188] Revert "Update composer.json: Use Symfony YAML 3.1 and later" Symfony YAML >= 3.0 requires PHP >= 5.5.9 and we're currently not planning to raise Pico's PHP requirement. This reverts commit f251bc83ec995d05e0e33140df22a451c0cfe4af. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0a354a4..5894970 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "php": ">=5.3.6", "twig/twig": "^1.18", "erusev/parsedown-extra": "^0.7", - "symfony/yaml" : "^3.1" + "symfony/yaml" : "^2.3" }, "require-dev" : { "phpdocumentor/phpdocumentor": "^2.8", From 848e28b7e695e76e0021216c66e4c26d2e1e74ae Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Thu, 14 Jul 2016 00:20:22 +0200 Subject: [PATCH 028/188] Declare Pico::getFiles() public This might be a useful helper method for plugins (e.g. PicoAdmin) --- lib/Pico.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Pico.php b/lib/Pico.php index 86c2c1b..1f129b4 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -1506,7 +1506,7 @@ class Pico * or Pico::SORT_NONE to leave the result unsorted * @return array list of found files */ - protected function getFiles($directory, $fileExtension = '', $order = self::SORT_ASC) + public function getFiles($directory, $fileExtension = '', $order = self::SORT_ASC) { $directory = rtrim($directory, '/'); $result = array(); From aa1bc077a785986889e737d4b2c2715284571fff Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Thu, 14 Jul 2016 00:24:06 +0200 Subject: [PATCH 029/188] Add $dropIndex parameter to Pico::getPageUrl() method This allows one to prevent Pico from removing the last "index" path component. Example use case: Pico's official admin plugin. We must distinguish between "content/sub.md" and "content/sub/index.md", otherwise it wouldn't be possible to edit both pages. --- lib/Pico.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index 1f129b4..b2f04a6 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -1424,9 +1424,11 @@ class Pico * @param string $page identifier of the page to link to * @param array|string $queryData either an array containing properties to * create a URL-encoded query string from, or a already encoded string + * @param boolean $dropIndex when the last path component is "index", + * then passing TRUE (default) leads to removing this path component * @return string URL */ - public function getPageUrl($page, $queryData = null) + public function getPageUrl($page, $queryData = null, $dropIndex = true) { if (is_array($queryData)) { $queryData = http_build_query($queryData, '', '&'); @@ -1438,11 +1440,13 @@ class Pico } // drop "index" - if ($page === 'index') { - $page = ''; - } elseif (($pagePathLength = strrpos($page, '/')) !== false) { - if (substr($page, $pagePathLength + 1) === 'index') { - $page = substr($page, 0, $pagePathLength); + if ($dropIndex) { + if ($page === 'index') { + $page = ''; + } elseif (($pagePathLength = strrpos($page, '/')) !== false) { + if (substr($page, $pagePathLength + 1) === 'index') { + $page = substr($page, 0, $pagePathLength); + } } } From 21bd18bcf02b3a415aaffcf813c085d113e3fc99 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Wed, 20 Jul 2016 19:23:19 +0200 Subject: [PATCH 030/188] Replace Pico::discoverRequestFile() with public Pico::resolveFilePath() This allows plugins (e.g. PicoAdmin) to safely resolve file paths without the need of re-implementing the method. --- lib/Pico.php | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/Pico.php b/lib/Pico.php index 11d3ade..5e5cf83 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -308,7 +308,7 @@ class Pico $this->triggerEvent('onRequestUrl', array(&$this->requestUrl)); // discover requested file - $this->discoverRequestFile(); + $this->requestFile = $this->resolveFilePath($this->requestUrl); $this->triggerEvent('onRequestFile', array(&$this->requestFile)); // load raw file content @@ -735,24 +735,29 @@ class Pico } /** - * Uses the request URL to discover the content file to serve + * Resolves a given file path to its corresponding content file + * + * This method also prevents `content_dir` breakouts using malicious + * request URLs. We don't use `realpath()`, because we neither want to + * check for file existance, nor prohibit symlinks which intentionally + * point to somewhere outside the `content_dir` folder. It is STRONGLY + * RECOMMENDED to use PHP's `open_basedir` feature - always, not just + * with Pico! * * @see Pico::getRequestFile() - * @return void + * @param string $requestUrl path name (likely from a URL) to resolve + * @return string path to the resolved content file */ - protected function discoverRequestFile() + public function resolveFilePath($requestUrl) { $contentDir = $this->getConfig('content_dir'); $contentExt = $this->getConfig('content_ext'); - if (empty($this->requestUrl)) { - $this->requestFile = $contentDir . 'index' . $contentExt; + if (empty($requestUrl)) { + return $contentDir . 'index' . $contentExt; } else { - // prevent content_dir breakouts using malicious request URLs - // we don't use realpath() here because we neither want to check for file existance - // nor prohibit symlinks which intentionally point to somewhere outside the content_dir - // it is STRONGLY RECOMMENDED to use open_basedir - always, not just with Pico! - $requestUrl = str_replace('\\', '/', $this->requestUrl); + // prevent content_dir breakouts + $requestUrl = str_replace('\\', '/', $requestUrl); $requestUrlParts = explode('/', $requestUrl); $requestFileParts = array(); @@ -768,31 +773,29 @@ class Pico } if (empty($requestFileParts)) { - $this->requestFile = $contentDir . 'index' . $contentExt; - return; + return $contentDir . 'index' . $contentExt; } // discover the content file to serve // Note: $requestFileParts neither contains a trailing nor a leading slash - $this->requestFile = $contentDir . implode('/', $requestFileParts); - if (is_dir($this->requestFile)) { + $requestFile = $contentDir . implode('/', $requestFileParts); + if (is_dir($requestFile)) { // if no index file is found, try a accordingly named file in the previous dir // if this file doesn't exist either, show the 404 page, but assume the index // file as being requested (maintains backward compatibility to Pico < 1.0) - $indexFile = $this->requestFile . '/index' . $contentExt; - if (file_exists($indexFile) || !file_exists($this->requestFile . $contentExt)) { - $this->requestFile = $indexFile; - return; + $indexFile = $requestFile . '/index' . $contentExt; + if (file_exists($indexFile) || !file_exists($requestFile . $contentExt)) { + return $indexFile; } } - $this->requestFile .= $contentExt; + return $requestFile . $contentExt; } } /** * Returns the absolute path to the content file to serve * - * @see Pico::discoverRequestFile() + * @see Pico::resolveFilePath() * @return string|null file path */ public function getRequestFile() From 6a13915f15a753f2b8dd56c761fbc0b7d5a0a553 Mon Sep 17 00:00:00 2001 From: Daniel Rudolf Date: Tue, 2 Aug 2016 02:31:20 +0200 Subject: [PATCH 031/188] Refactor default theme --- config/config.php.template | 3 + themes/default/font/LICENSE.txt | 1 + themes/default/font/fontello.eot | Bin 5864 -> 6116 bytes themes/default/font/fontello.svg | 6 +- themes/default/font/fontello.ttf | Bin 5696 -> 5948 bytes themes/default/font/fontello.woff | Bin 3428 -> 3564 bytes themes/default/font/fontello.woff2 | Bin 2888 -> 2956 bytes themes/default/fontello.css | 5 +- themes/default/index.twig | 54 +- themes/default/js/pico.js | 67 ++ themes/default/js/utils.js | 161 +++++ themes/default/scripts/modernizr-2.6.1.min.js | 4 - themes/default/style.css | 573 +++++++----------- 13 files changed, 504 insertions(+), 370 deletions(-) create mode 100644 themes/default/js/pico.js create mode 100644 themes/default/js/utils.js delete mode 100644 themes/default/scripts/modernizr-2.6.1.min.js diff --git a/config/config.php.template b/config/config.php.template index bd8c182..34b3063 100644 --- a/config/config.php.template +++ b/config/config.php.template @@ -27,6 +27,9 @@ */ // $config['theme'] = 'default'; // Set the theme (defaults to "default") // $config['theme_url'] = ''; // Override the base URL of the themes folder (e.g. http://example.com/pico/themes/) +// $config['theme_config'] = array( // Settings of the theme; depends on the theme used +// 'widescreen' => false // Default theme: Allocate more horicontal space (i.e. make the site container wider) +// ); // $config['twig_config'] = array( // Twig settings // 'cache' => false, // To enable Twig caching change this to a path to a writable directory // 'autoescape' => false, // Auto-escape Twig vars diff --git a/themes/default/font/LICENSE.txt b/themes/default/font/LICENSE.txt index bf29f26..270e046 100644 --- a/themes/default/font/LICENSE.txt +++ b/themes/default/font/LICENSE.txt @@ -20,3 +20,4 @@ Font license info License: SIL OFL 1.1 Homepage: http://fortawesome.github.com/Font-Awesome/ + Icons: menu, github-circled ("octocat"), twitter ("birdy"), chat diff --git a/themes/default/font/fontello.eot b/themes/default/font/fontello.eot index b3ae4d971c5489002cad31e996c07441da3e2515..0d93cf8c5321162fae67e2f60cff9f4b4e351b01 100644 GIT binary patch delta 807 zcmZvaO-NKx6vzMf-uEGI%x5xTJxfB!dJCEi#IHjiVFJ8z18^8v85TB3RmJ{n=nInEe@*;l2jc6as z3J{1=JTl|V+P9Y$Cy9qioLOtZA%8Wk>&RBjrgAe+ADtct#3ev6k_l_txEsC+=t+wA zB$J7}r5_jP$Wb6iG-Jg8q%AwK@u2P? z?WF+xoio4SzB%#hF<^Y4_=uB9I9~@_Ujw0+#J9jm8|XkEc*=6qsHz!0IXZzbsN5fv z;H|Kuf{B9njrFs2uZ=ff&iI8u3c_Etku~T>GE_G}bp|TH!HX!}E8Q=xm&?Gv>)qB= zY3xlZrFyrXM#`3?`0EU3>W0CqfOm3k7;VSwedhu)9V|Wn!CGtV)dON zLEOB@wR`+0?(<}@dgr^D53*2|*l&iIkN>CX5mN?y&8kmTQL^c4w(BEJE;^Dsy)nI|Rh4PJ6v)y8l;V#?23Fvz-&n;s#PUO3 o7g3ebZ7_wYOoI|nTCRHaT4p+@&&OSQW3FN+($|;QjKh_`0p|Xmc>n+a delta 536 zcmYjNJ!lj`7=80|d+Tm6XJIM{3sE8%Ih1fVFPQl3zTo zCRljF!u~Kat=(_PV7=#7nr+s7Sr(c8!p({NR*WC+SL$EX-kcu=@9ypE2Z}v_+>DzG z=KQ$33GCt{Fc!}T@%OPqFMz$zsc#?~<)9x!5U0k^PQXrHiz5ZTO+8Dt#R;*NjL5-S znEQY!sGJQkJ2ZvRsBnVKC4mmM?rg1fyE_@XH0BOFXOd?jtaM#UQ37wt;AiBak`L-Y zTF_NBkX?*e-e|5)_Ky64t2&wG - + - + + + \ No newline at end of file diff --git a/themes/default/font/fontello.ttf b/themes/default/font/fontello.ttf index f9b65ca25d0b21287c7f9631d7ab6e44dc8b4643..fb114894e16899384d5df3c0de7d6eecbe1482bf 100644 GIT binary patch delta 804 zcmZvaO=uHA6vzK>XLnOJHHjrgC{@#%en5+@HEl22FM|;*SV<~Y6uYKrBh4mDx=QOM z9>jxqsOBUh1Vz-F81YoWLp>Dqq=I)9a?)HBl|p>8t?0oS-kbTo`Oll3xAU@Rbzcz# zz>g~su;YoT@$ilIWsYpHC66b1`-j6PegHJdd@^O3_Rz@l1?ER=Pg9qiu>QWT0^SI( zhv)2h>-x==Y33N4J#Q}B91rlkg~x{Z?DE{>d&d%hI0L9gCT$jHja$u^0Z)bngBgx^ z4v8t|GIJzjIZI7#JcDPAT`-$ZnM$9~m_IN#TIQ0Cs`n%FC+2X@w9>Edw(RD!R6u!S z=NFw9hkrc)jCZ^qw+m_e^H}#P5PHUZ4J4Br=)*9Stm)+7iyfIrq8Ys1ZCss_kM;`^ zYmoNj?RJT;81ISNHBM9@y;#& zv+nYAFtG8}Ukp;nC${@T;UT3x(NbPd?@F6LVB6ox51sW$bPI4MC%p1ONa4 delta 537 zcmYk2K}Z!*6o&shcbs|7NKY3n0+9(5qaK`o+Zw{TYS7?r-uo*}Lb5g|(OU4S-z(80)Gur3d=l zb)cG$K#!U%s6RdDSAml&}PA*3|aYZaePepS!NTvjg za7yv*8G46B0Zep|Q~V5I3wzJ^mMWFw5qDXbRHTx~;8_SOI&B<87mVsO8>Mj`iOJ}k zY%1TC-Lf&hh+6nkdNQ28ybo7y#U(%9Y^MMFXu-bJY{vOSujx7b#1;j_D@O5OOk`n{ lcWqmGrZ9vQrl^9M4}5Lw2jf~7CavPQ4yH$d@+r5m<_I`ua*qH2 diff --git a/themes/default/font/fontello.woff b/themes/default/font/fontello.woff index a2ef51165dccc7da3d5f0bbce731ab5950b5bb12..d37a53602bdff4bb0f1e9525bec8c58ac30ea3a8 100644 GIT binary patch delta 1567 zcmXYxcU03?9K~M}V$dW684d`R9hpWD0Wst-l_f<4p(_5uULh$Wm>`5;3MvYT0a<0J zs3-`=$gn^O9+rrMDg^|U0xAktSu#qc59>SUbKbl6+DjRACh_9`t*Fq4J!G>Nx)RAz%V@a=#3W|@@ z1OV*_J!OdIn%)(;P<9v-dVT<4g#h5$XGKE)Fvc<1#f`&^{C5@Bq;YhIT^sG&w9? zWyglVG7$o}khC{;MvW(a4MKtuWH^wD+zvvzT#O$C4Ile?W-UV)h%i!saJO0Ec8WPo z#K*K_x+EzP3Ywv(PNRW=KXxzp)(wzZ@%cG?{*AK{Bn-xZBnUkzLpnRk#t4xmd3#jyKP)Iv<5$~*l~qZUMN)7mAZ)NL>7^w@oeZ~wkU zYkAD#^E37W(4lTB6xk`r^~F@};2{yo*ZLc&;=PvK$leLg@L0|S2NYh@rlOdv z_ugM-(VmB%$dnwShWmPqG@I*f&>HDdGxTjNFG>7_rS7EC@w(8`d}5)aYV>c?;Mz=S zOuR?*WL#5&X=Gqw>-lHTUEEx}4*6_7U|s6iN)s4v6dF4DkZ9})x}t6-c^Y>>*nEki zK>H%MM<7sXj}a;2?nEw&qDCbe7AJ5R51(LKx~ z3FlC#JwQ%JvppPlx}S!$rd2dOj%~ZeX7oONE*V+cFaD`*PJ7$AE2bGW@#!_{6GRw+SG9y>k@4(ai@~Y*!_EEH7<7MCu{u=&1U)ZvZrvggI{+rD(VKx zw_VIvyi;oSk*rf}AAAz*?_4i$>MT2xrEz&ZHTcR@@({|g{`v-LWAtE7MBU9ZynHp+ zM`p*TDLGjmIrIK@gAqu-uQSJMQyqp9<7RU09lmGeT?ajRaeB~g@za(giFpmdV>2&p z0+KETj5l+QzgV)Ij6@uh;B?8Sp@pvl0a;01wQQPxKId+iv2XvYBg@gAes3RDe;$py zm*9_f|6p@HJv-HAbg9cJtU%e_fcLOZXs%YCrlXilEWeH5Wc6w|*`k&_kYMc3xhYc{ zlP1Ig{Y&Kmb9jz{dt2&#*`b!FpV+<6`{&E}#T6EL`Wg}Wda-36nkN>C(U*-n+}#5< z#Pyh|>KiIH1*QF6Vrwx=>T2<+>-^x`<5yexnd+L&9cc7_(r_^MEmLI5<5m?LUiW@4 z=cedc;jKG#Gxss#%j0rqXIRylQyCjsk!6&UR_q^cbLX!nOM6voi?cb1WdS5jzyj>n2C%4a|Ouv@lCdq_6?{jw(89owG zisT694W81H;G^ac*s#KDvpXW{jB@d+H1X@I;)Jx90W+otN<xH`eMe$~-`fuyh#hd4N+OEq)4~8h|{~sPfz(_)7?###{^LI10c{4am zJ#1T0lu^3ELXum$iDR);D&G;sRw5L9A1(?@C0M%dQ7UzL1+UB3-kPEc(#x=Ua0J0X za*IqxxQ|bIK$W7gV0EZtfj7WN4e|LXlN`|*QNHLGQI6<5U(Z@c kBCs%!ZoPVeM!%;^x0VR>EG??D`6!UMEMEwKc${$ZKX`eFX#fBK delta 1392 zcmXYwdo&@D!f{ z0NMh2>JW-jzXBV=WpkkC3;-?*03tU{oOOiFIthF6FJMOb8)SC)=@94*!>k+t45REY z!jXmsvw{F1G{BG<#ES5}+V7#zgx)CBx)6Q5EygF5!xzBIu~3uOP`wX8ctjw~5jCJD ztbsN~0S-$52NNk!|Id5k$S*8TF!b!(F)#vOd^8SKg~MD0NGWIF%YrR$|xr?9j=^3?S407oJv!pKb4O%ueK5DE?Pa3 zi}^>`oEr6d)23|mkW16=10=duA1qV4tWsSZ-idB;(IRrPp~tp>G@+ck!AoRlSUp0- zPWJAcLp||hVEQ-a_zWChH&T-SG{fStd4WQqz@V2FHz8LMrjpg=*GJnJ2ZVk>OPfM_bL%~(L*lP!2(og60@b%3dZ?DUel6@`XmL)?ckb>I& z*P+kGY5G@bQ?9$+cga)qpWC}wVvR=0*mUuJ8?1x&h0i=Z`Ojj;ZRV?}3iZ`WJ%gXG zlX{x=*myJQ#*v;TQT4#>=Bo1qU!MFzo3tuJG$de`f2e#-P-3(yiN3YIe%Kl7Fc^v z{;*SMex__CtB)Hit?`=MfxVm86od}q(JvToQ0C0cSqgvB2o#gWtWSl987cowE`qC!oboqok+ zZ&SCUe?z?4-A|cAqm>fcQ)-7}R9VhZevzOfm-eZfc&dHazo28d*UjkfDn2#)k<{?s zwo(KU&#}+CY4%j@?!hC9Pm|>D+7(`b58`q)UeX2z#+PMD)%>b(ld?H#zhCHL;fM3( zygE!d;rL*%#Qn!Hn;Gs4^O9rfinrh&rf4EX`&TH<{uC`WwW26g5elD$W-tiE|UE*4uB2+NC!2QvJphW7~TDQj63C6gWD7 K56kAN#Qg`EnKpL- diff --git a/themes/default/font/fontello.woff2 b/themes/default/font/fontello.woff2 index 9821cb2e2aeb516162585f03b10f70828e88a329..38039ce15ec297de54c53f8d7ce8ce6d383be50d 100644 GIT binary patch literal 2956 zcmV;73v={$Pew8T0RR9101J!&4*&oF02e#}01Gq#0RR9100000000000000000000 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 C7oh?G literal 2888 zcmV-O3%B%lPew8T0RR9101HR}4*&oF02V+101EK{0RR9100000000000000000000 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 diff --git a/themes/default/fontello.css b/themes/default/fontello.css index 8fd6a40..e9a005e 100644 --- a/themes/default/fontello.css +++ b/themes/default/fontello.css @@ -55,6 +55,7 @@ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ } -.icon-chat:before { content: '\e800'; } /* '' */ -.icon-birdy:before { content: '\f099'; } /* '' */ +.icon-menu:before { content: '\f0c9'; } /* '' */ .icon-octocat:before { content: '\f09b'; } /* '' */ +.icon-birdy:before { content: '\f099'; } /* '' */ +.icon-chat:before { content: '\e800'; } /* '' */ diff --git a/themes/default/index.twig b/themes/default/index.twig index 8d6bc03..ec76b84 100644 --- a/themes/default/index.twig +++ b/themes/default/index.twig @@ -1,28 +1,32 @@ - + + {% if meta.title %}{{ meta.title }} | {% endif %}{{ site_title }} {% if meta.description %} - + {% endif %}{% if meta.robots %} - + {% endif %} - + - - - + -