mikuli.cz

:)
git clone https://git.sr.ht/~ashymad/mikuli.cz
Log | Files | Refs

commit 13c4e575bcffedb47db66d87b74509f41277bea0
parent 9d74e8013739a2c92a0bfd7f94e4c9d4e13bbd83
Author: Szymon Mikulicz <szymon.mikulicz@posteo.net>
Date:   Thu, 21 Dec 2023 14:58:52 +0100

Merge branch 'main' of https://github.com/datenstrom/yellow

Diffstat:
M.gitattributes | 9+++------
A.github/workflows/system-tests.yml | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
D.gitignore | 2--
M.htaccess | 2+-
D.travis.yml | 15---------------
Mcontent/1-home/page.md | 2--
Acontent/9-about/page.md | 10++++++++++
Mmedia/downloads/yellow.pdf | 0
Rmedia/images/picture.jpg -> media/images/photo.jpg | 0
Rmedia/thumbnails/picture-100x40.jpg -> media/thumbnails/photo-100x40.jpg | 0
Mrobots.txt | 2+-
Dsystem/extensions/bundle.php | 1962-------------------------------------------------------------------------------
Dsystem/extensions/command.php | 623-------------------------------------------------------------------------------
Msystem/extensions/core.php | 5706++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Asystem/extensions/edit-stack.svg | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msystem/extensions/edit.css | 207+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msystem/extensions/edit.js | 204++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msystem/extensions/edit.php | 1330++++++++++++++++++++++++++++++++++++++++---------------------------------------
Dsystem/extensions/edit.woff | 0
Asystem/extensions/generate.php | 456+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msystem/extensions/image.php | 126++++++++++++++++++++++++++++++++++++-------------------------------------------
Asystem/extensions/install-blog.bin | 0
Asystem/extensions/install-language.bin | 0
Asystem/extensions/install-wiki.bin | 0
Msystem/extensions/markdown.php | 69++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Dsystem/extensions/meta.php | 68--------------------------------------------------------------------
Asystem/extensions/serve.php | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msystem/extensions/stockholm.php | 14+++++---------
Asystem/extensions/update-current.ini | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/extensions/update-latest.ini | 775+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msystem/extensions/update.php | 1222+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Asystem/extensions/yellow-language.ini | 5+++++
Rsystem/settings/system.ini -> system/extensions/yellow-system.ini | 0
Rsystem/settings/user.ini -> system/extensions/yellow-user.ini | 0
Msystem/layouts/default.html | 2+-
Msystem/layouts/error.html | 2+-
Msystem/layouts/footer.html | 5++---
Msystem/layouts/header.html | 7++-----
Msystem/layouts/navigation.html | 2+-
Msystem/layouts/pagination.html | 6+++---
Dsystem/resources/stockholm-opensans-license.txt | 202-------------------------------------------------------------------------------
Dsystem/resources/stockholm.css | 386-------------------------------------------------------------------------------
Dsystem/settings/text.ini | 5-----
Rsystem/resources/bundle-4ae45fbba2.min.js -> system/themes/bundle-4ae45fbba2.min.js | 0
Rsystem/resources/stockholm-opensans-bold.woff -> system/themes/stockholm-opensans-bold.woff | 0
Rsystem/resources/stockholm-opensans-light.woff -> system/themes/stockholm-opensans-light.woff | 0
Rsystem/resources/stockholm-opensans-regular.woff -> system/themes/stockholm-opensans-regular.woff | 0
Asystem/themes/stockholm.css | 410+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsystem/resources/stockholm-icon.png -> system/themes/stockholm.png | 0
Myellow.php | 6++----
50 files changed, 6865 insertions(+), 7282 deletions(-)

diff --git a/.gitattributes b/.gitattributes @@ -1,8 +1,5 @@ -.gitignore export-ignore +.github export-ignore .gitattributes export-ignore -.travis.yml export-ignore -CONTRIBUTING.md export-ignore LICENSE.md export-ignore -README-de.md export-ignore -README-sv.md export-ignore -README.md export-ignore +README*.md export-ignore +SCREENSHOT*.png export-ignore diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml @@ -0,0 +1,51 @@ +# Datenstrom Yellow system tests + +name: System tests +on: [push, pull_request] +jobs: + extension-tests: + name: Extensions + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: latest + extensions: curl, gd, mbstring, zip + ini-file: development + coverage: none + tools: none + - name: Set up problem matcher + run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" + - name: Set up test environment + run: | + php yellow.php skip installation maximal + echo "Generate:exclude" > content/contact/page.md + echo "Generate:exclude" > content/search/page.md + - name: Run tests + run: php yellow.php generate tests + php-tests: + name: PHP ${{ matrix.php }} + strategy: + matrix: + php: [8.3, 8.2, 8.1, 8.0, 7.4, 7.3, 7.2, 7.1, 7.0] + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, gd, mbstring, zip + ini-file: development + coverage: none + tools: none + - name: Set up problem matcher + run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" + - name: Set up test environment + run: php yellow.php skip installation minimal + - name: Run tests + run: php yellow.php generate tests diff --git a/.gitignore b/.gitignore @@ -1,2 +0,0 @@ -.DS_Store -*.min.* diff --git a/.htaccess b/.htaccess @@ -1,7 +1,7 @@ <IfModule mod_rewrite.c> RewriteEngine on DirectoryIndex index.html yellow.php -RewriteRule ^(cache|content|system)/ error [L] +RewriteRule ^(content|system)/ error [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ yellow.php [L] diff --git a/.travis.yml b/.travis.yml @@ -1,15 +0,0 @@ -# Datenstrom Yellow tests, https://travis-ci.org/datenstrom - -language: php -php: - - 7.4 - - 7.3 - - 7.2 - - 7.1 - - 7.0 - - 5.6 -before_script: - - echo "CoreStaticUrl:http://website/" >> system/settings/system.ini - - php yellow.php about -script: - - php yellow.php build test diff --git a/content/1-home/page.md b/content/1-home/page.md @@ -28,5 +28,3 @@ Checkout what I do except typesetting: [design](design), [program](program), appear here someday. Hope you like what you see. Want to contact me? Direct your mail to szymon.mikulicz at posteo.net - - diff --git a/content/9-about/page.md b/content/9-about/page.md @@ -0,0 +1,10 @@ +--- +Title: About +--- +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut +labore et dolore magna pizza. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris +nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit +esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +[Made with Datenstrom Yellow](https://datenstrom.se/yellow/). diff --git a/media/downloads/yellow.pdf b/media/downloads/yellow.pdf Binary files differ. diff --git a/media/images/picture.jpg b/media/images/photo.jpg Binary files differ. diff --git a/media/thumbnails/picture-100x40.jpg b/media/thumbnails/photo-100x40.jpg Binary files differ. diff --git a/robots.txt b/robots.txt @@ -1,5 +1,5 @@ User-agent: * Disallow: /harming/humans Disallow: /harming/machines -Disallow: /risking/own/existence +Disallow: /risking/your/own/existence Disallow: /edit/ diff --git a/system/extensions/bundle.php b/system/extensions/bundle.php @@ -1,1962 +0,0 @@ -<?php -// Bundle extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/bundle -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowBundle { - const VERSION = "0.8.12"; - const TYPE = "feature"; - public $yellow; //access to API - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - } - - // Handle page output data - public function onParsePageOutput($page, $text) { - $output = null; - if ($text && preg_match("/^(.*<head>[\r\n]+)(.*)(<\/head>.*)$/s", $text, $matches)) { - $output = $matches[1].$this->normaliseHead($matches[2]).$matches[3]; - } - return $output; - } - - // Handle command - public function onCommand($command, $text) { - switch ($command) { - case "clean": $statusCode = $this->processCommandClean($command, $text); break; - default: $statusCode = 0; - } - return $statusCode; - } - - // Process command to clean bundles - public function processCommandClean($command, $text) { - $statusCode = 0; - if ($command=="clean" && $text=="all") { - $path = $this->yellow->system->get("coreResourceDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/bundle-.*/", false, false) as $entry) { - if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; - } - if ($statusCode==500) echo "ERROR cleaning bundles: Can't delete files in directory '$path'!\n"; - } - return $statusCode; - } - - // Normalise page head - public function normaliseHead($text) { - $dataMeta = $dataLink = $dataCss = $dataScriptDefer = $dataScriptNow = $dataOther = array(); - foreach ($this->yellow->toolbox->getTextLines($text) as $line) { - if (preg_match("/^<meta (.*?)>$/i", $line) || preg_match("/^<title>(.*?)<\/title>$/i", $line)) { - array_push($dataMeta, $line); - } elseif (preg_match("/^<link (.*?)href=\"([^\"]+)\"(.*?)>$/i", $line, $matches)) { - if (preg_match("/\"stylesheet\"/i", $line)) { - if (!isset($dataCss[$matches[2]])) $dataCss[$matches[2]] = $line; - } else { - array_push($dataLink, $line); - } - } elseif (preg_match("/^<script (.*?)src=\"([^\"]+)\"(.*?)><\/script>$/i", $line, $matches)) { - if (preg_match("/\"defer\"/i", $line)) { - if (!isset($dataScriptDefer[$matches[2]])) $dataScriptDefer[$matches[2]] = $line; - } else { - if (!isset($dataScriptNow[$matches[2]])) $dataScriptNow[$matches[2]] = $line; - } - } else { - array_push($dataOther, $line); - } - } - if (!defined("DEBUG") || DEBUG==0) { - $dataCss = $this->processBundle($dataCss, "css"); - $dataScriptDefer = $this->processBundle($dataScriptDefer, "js", "defer"); - $dataScriptNow = $this->processBundle($dataScriptNow, "js"); - } - $output = implode($dataMeta).implode($dataLink).implode($dataCss). - implode($dataScriptDefer).implode($dataScriptNow).implode($dataOther); - return $output; - } - - // Process bundle, create file on demand - public function processBundle($data, $type, $attribute = "") { - $fileNames = array(); - $modified = 0; - $scheme = $this->yellow->system->get("coreServerScheme"); - $address = $this->yellow->system->get("coreServerAddress"); - $base = $this->yellow->system->get("coreServerBase"); - foreach ($data as $key=>$value) { - if (preg_match("/^\w+:/", $key)) continue; - if (preg_match("/data-bundle=\"exclude\"/i", $value)) continue; - if (substru($key, 0, strlenu($base))!=$base) continue; - $location = substru($key, strlenu($base)); - $fileName = $this->yellow->lookup->findFileFromSystem($location); - $modified = max($modified, $this->yellow->toolbox->getFileModified($fileName)); - if (is_readable($fileName)) { - array_push($fileNames, $fileName); - unset($data[$key]); - } - } - if (!empty($fileNames)) { - $autoVersioning = intval($modified/(60*60*24)); - $id = substru(md5($autoVersioning.$base.implode($fileNames)), 0, 10); - $fileNameBundle = $this->yellow->system->get("coreResourceDirectory")."bundle-$id.min.$type"; - $locationBundle = $base.$this->yellow->system->get("coreResourceLocation")."bundle-$id.min.$type"; - $rawDataAttribute = $attribute=="defer" ? "defer=\"defer\" " : ""; - if ($type=="css") { - $data[$locationBundle] = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"".htmlspecialchars($locationBundle)."\" />\n"; - } else { - $data[$locationBundle] = "<script type=\"text/javascript\" ${rawDataAttribute}src=\"".htmlspecialchars($locationBundle)."\"></script>\n"; - } - if ($this->yellow->toolbox->getFileModified($fileNameBundle)!=$modified) { - $fileDataBundle = ""; - foreach ($fileNames as $fileName) { - $fileData = $this->yellow->toolbox->readFile($fileName); - $fileData = $this->processBundleConvert($scheme, $address, $base, $fileData, $fileName, $type); - $fileData = $this->processBundleMinify($scheme, $address, $base, $fileData, $fileName, $type); - if (substrb($fileData, 0, 3)=="\xEF\xBB\xBF") $fileData = substrb($fileData, 3); - if (substrb($fileData, 0, 13)=="\"use strict\";" || substrb($fileData, 0, 13)=="'use strict';") $fileData = substrb($fileData, 13); - if (!empty($fileDataBundle)) $fileDataBundle .= "\n\n"; - $fileDataBundle .= "/* ".basename($fileName)." */\n"; - $fileDataBundle .= $fileData; - } - if (is_file($fileNameBundle)) $this->yellow->toolbox->deleteFile($fileNameBundle); - if (!$this->yellow->toolbox->createFile($fileNameBundle, $fileDataBundle) || - !$this->yellow->toolbox->modifyFile($fileNameBundle, $modified)) { - $this->yellow->page->error(500, "Can't write file '$fileNameBundle'!"); - } - } - } - return $data; - } - - // Process bundle, convert URLs - public function processBundleConvert($scheme, $address, $base, $fileData, $fileName, $type) { - if ($type=="css") { - $extensionDirectoryLength = strlenu($this->yellow->system->get("coreExtensionDirectory")); - if (substru($fileName, 0, $extensionDirectoryLength) == $this->yellow->system->get("coreExtensionDirectory")) { - $base .= $this->yellow->system->get("coreExtensionLocation"); - } else { - $base .= $this->yellow->system->get("coreResourceLocation"); - } - $thisCompatible = $this; - $callback = function ($matches) use ($thisCompatible, $scheme, $address, $base) { - $url = $thisCompatible->yellow->lookup->normaliseUrl($scheme, $address, $base, $matches[1], false); - $url = strreplaceu("$scheme://$address", "", $url); - return "url(\"$url\")"; - }; - $fileData = preg_replace_callback("/url\([\'\"]?(.*?)[\'\"]?\)/", $callback, $fileData); - } - return $fileData; - } - - // Process bundle, minify data - public function processBundleMinify($scheme, $address, $base, $fileData, $fileName, $type) { - $minifier = $type=="css" ? new MinifyCss() : new MinifyJavaScript(); - if (preg_match("/\.min/", $fileName)) $minifier = new MinifyBasic(); - $minifier->add($fileData); - return $minifier->minify(); - } -} - -/** - * Abstract minifier class. - * - * Please report bugs on https://github.com/matthiasmullie/minify/issues - * - * @package Minify - * @author Matthias Mullie <minify@mullie.eu> - * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved - * @license MIT License - */ -abstract class Minify -{ - /** - * The data to be minified. - * - * @var string[] - */ - protected $data = array(); - - /** - * Array of patterns to match. - * - * @var string[] - */ - protected $patterns = array(); - - /** - * This array will hold content of strings and regular expressions that have - * been extracted from the JS source code, so we can reliably match "code", - * without having to worry about potential "code-like" characters inside. - * - * @var string[] - */ - public $extracted = array(); - - /** - * Init the minify class - optionally, code may be passed along already. - */ - public function __construct(/* $data = null, ... */) - { - // it's possible to add the source through the constructor as well ;) - if (func_num_args()) { - call_user_func_array(array($this, 'add'), func_get_args()); - } - } - - /** - * Add a file or straight-up code to be minified. - * - * @param string|string[] $data - * - * @return static - */ - public function add($data /* $data = null, ... */) - { - // bogus "usage" of parameter $data: scrutinizer warns this variable is - // not used (we're using func_get_args instead to support overloading), - // but it still needs to be defined because it makes no sense to have - // this function without argument :) - $args = array($data) + func_get_args(); - - // this method can be overloaded - foreach ($args as $data) { - if (is_array($data)) { - call_user_func_array(array($this, 'add'), $data); - continue; - } - - // redefine var - $data = (string) $data; - - // load data - $value = $this->load($data); - $key = ($data != $value) ? $data : count($this->data); - - // replace CR linefeeds etc. - // @see https://github.com/matthiasmullie/minify/pull/139 - $value = str_replace(array("\r\n", "\r"), "\n", $value); - - // store data - $this->data[$key] = $value; - } - - return $this; - } - - /** - * Minify the data & (optionally) saves it to a file. - * - * @param string[optional] $path Path to write the data to - * - * @return string The minified data - */ - public function minify($path = null) - { - $content = $this->execute($path); - - // save to path - if ($path !== null) { - $this->save($content, $path); - } - - return $content; - } - - /** - * Minify & gzip the data & (optionally) saves it to a file. - * - * @param string[optional] $path Path to write the data to - * @param int[optional] $level Compression level, from 0 to 9 - * - * @return string The minified & gzipped data - */ - public function gzip($path = null, $level = 9) - { - $content = $this->execute($path); - $content = gzencode($content, $level, FORCE_GZIP); - - // save to path - if ($path !== null) { - $this->save($content, $path); - } - - return $content; - } - - /** - * Minify the data & write it to a CacheItemInterface object. - * - * @param CacheItemInterface $item Cache item to write the data to - * - * @return CacheItemInterface Cache item with the minifier data - */ - public function cache(CacheItemInterface $item) - { - $content = $this->execute(); - $item->set($content); - - return $item; - } - - /** - * Minify the data. - * - * @param string[optional] $path Path to write the data to - * - * @return string The minified data - */ - abstract public function execute($path = null); - - /** - * Load data. - * - * @param string $data Either a path to a file or the content itself - * - * @return string - */ - protected function load($data) - { - // check if the data is a file - if ($this->canImportFile($data)) { - $data = file_get_contents($data); - - // strip BOM, if any - if (substr($data, 0, 3) == "\xef\xbb\xbf") { - $data = substr($data, 3); - } - } - - return $data; - } - - /** - * Save to file. - * - * @param string $content The minified data - * @param string $path The path to save the minified data to - * - * @throws IOException - */ - protected function save($content, $path) - { - $handler = $this->openFileForWriting($path); - - $this->writeToFile($handler, $content); - - @fclose($handler); - } - - /** - * Register a pattern to execute against the source content. - * - * @param string $pattern PCRE pattern - * @param string|callable $replacement Replacement value for matched pattern - */ - protected function registerPattern($pattern, $replacement = '') - { - // study the pattern, we'll execute it more than once - $pattern .= 'S'; - - $this->patterns[] = array($pattern, $replacement); - } - - /** - * We can't "just" run some regular expressions against JavaScript: it's a - * complex language. E.g. having an occurrence of // xyz would be a comment, - * unless it's used within a string. Of you could have something that looks - * like a 'string', but inside a comment. - * The only way to accurately replace these pieces is to traverse the JS one - * character at a time and try to find whatever starts first. - * - * @param string $content The content to replace patterns in - * - * @return string The (manipulated) content - */ - protected function replace($content) - { - $processed = ''; - $positions = array_fill(0, count($this->patterns), -1); - $matches = array(); - - while ($content) { - // find first match for all patterns - foreach ($this->patterns as $i => $pattern) { - list($pattern, $replacement) = $pattern; - - // we can safely ignore patterns for positions we've unset earlier, - // because we know these won't show up anymore - if (array_key_exists($i, $positions) == false) { - continue; - } - - // no need to re-run matches that are still in the part of the - // content that hasn't been processed - if ($positions[$i] >= 0) { - continue; - } - - $match = null; - if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE)) { - $matches[$i] = $match; - - // we'll store the match position as well; that way, we - // don't have to redo all preg_matches after changing only - // the first (we'll still know where those others are) - $positions[$i] = $match[0][1]; - } else { - // if the pattern couldn't be matched, there's no point in - // executing it again in later runs on this same content; - // ignore this one until we reach end of content - unset($matches[$i], $positions[$i]); - } - } - - // no more matches to find: everything's been processed, break out - if (!$matches) { - $processed .= $content; - break; - } - - // see which of the patterns actually found the first thing (we'll - // only want to execute that one, since we're unsure if what the - // other found was not inside what the first found) - $discardLength = min($positions); - $firstPattern = array_search($discardLength, $positions); - $match = $matches[$firstPattern][0][0]; - - // execute the pattern that matches earliest in the content string - list($pattern, $replacement) = $this->patterns[$firstPattern]; - $replacement = $this->replacePattern($pattern, $replacement, $content); - - // figure out which part of the string was unmatched; that's the - // part we'll execute the patterns on again next - $content = (string) substr($content, $discardLength); - $unmatched = (string) substr($content, strpos($content, $match) + strlen($match)); - - // move the replaced part to $processed and prepare $content to - // again match batch of patterns against - $processed .= substr($replacement, 0, strlen($replacement) - strlen($unmatched)); - $content = $unmatched; - - // first match has been replaced & that content is to be left alone, - // the next matches will start after this replacement, so we should - // fix their offsets - foreach ($positions as $i => $position) { - $positions[$i] -= $discardLength + strlen($match); - } - } - - return $processed; - } - - /** - * This is where a pattern is matched against $content and the matches - * are replaced by their respective value. - * This function will be called plenty of times, where $content will always - * move up 1 character. - * - * @param string $pattern Pattern to match - * @param string|callable $replacement Replacement value - * @param string $content Content to match pattern against - * - * @return string - */ - protected function replacePattern($pattern, $replacement, $content) - { - if (is_callable($replacement)) { - return preg_replace_callback($pattern, $replacement, $content, 1, $count); - } else { - return preg_replace($pattern, $replacement, $content, 1, $count); - } - } - - /** - * Strings are a pattern we need to match, in order to ignore potential - * code-like content inside them, but we just want all of the string - * content to remain untouched. - * - * This method will replace all string content with simple STRING# - * placeholder text, so we've rid all strings from characters that may be - * misinterpreted. Original string content will be saved in $this->extracted - * and after doing all other minifying, we can restore the original content - * via restoreStrings(). - * - * @param string[optional] $chars - * @param string[optional] $placeholderPrefix - */ - protected function extractStrings($chars = '\'"', $placeholderPrefix = '') - { - // PHP only supports $this inside anonymous functions since 5.4 - $minifier = $this; - $callback = function ($match) use ($minifier, $placeholderPrefix) { - // check the second index here, because the first always contains a quote - if ($match[2] === '') { - /* - * Empty strings need no placeholder; they can't be confused for - * anything else anyway. - * But we still needed to match them, for the extraction routine - * to skip over this particular string. - */ - return $match[0]; - } - - $count = count($minifier->extracted); - $placeholder = $match[1].$placeholderPrefix.$count.$match[1]; - $minifier->extracted[$placeholder] = $match[1].$match[2].$match[1]; - - return $placeholder; - }; - - /* - * The \\ messiness explained: - * * Don't count ' or " as end-of-string if it's escaped (has backslash - * in front of it) - * * Unless... that backslash itself is escaped (another leading slash), - * in which case it's no longer escaping the ' or " - * * So there can be either no backslash, or an even number - * * multiply all of that times 4, to account for the escaping that has - * to be done to pass the backslash into the PHP string without it being - * considered as escape-char (times 2) and to get it in the regex, - * escaped (times 2) - */ - $this->registerPattern('/(['.$chars.'])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback); - } - - /** - * This method will restore all extracted data (strings, regexes) that were - * replaced with placeholder text in extract*(). The original content was - * saved in $this->extracted. - * - * @param string $content - * - * @return string - */ - protected function restoreExtractedData($content) - { - if (!$this->extracted) { - // nothing was extracted, nothing to restore - return $content; - } - - $content = strtr($content, $this->extracted); - - $this->extracted = array(); - - return $content; - } - - /** - * Check if the path is a regular file and can be read. - * - * @param string $path - * - * @return bool - */ - protected function canImportFile($path) - { - $parsed = parse_url($path); - if ( - // file is elsewhere - isset($parsed['host']) || - // file responds to queries (may change, or need to bypass cache) - isset($parsed['query']) - ) { - return false; - } - - return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path); - } - - /** - * Attempts to open file specified by $path for writing. - * - * @param string $path The path to the file - * - * @return resource Specifier for the target file - * - * @throws IOException - */ - protected function openFileForWriting($path) - { - if (($handler = @fopen($path, 'w')) === false) { - throw new IOException('The file "'.$path.'" could not be opened for writing. Check if PHP has enough permissions.'); - } - - return $handler; - } - - /** - * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions. - * - * @param resource $handler The resource to write to - * @param string $content The content to write - * @param string $path The path to the file (for exception printing only) - * - * @throws IOException - */ - protected function writeToFile($handler, $content, $path = '') - { - if (($result = @fwrite($handler, $content)) === false || ($result < strlen($content))) { - throw new IOException('The file "'.$path.'" could not be written to. Check your disk space and file permissions.'); - } - } -} - -class CSS extends Minify -{ - /** - * @var int maximum inport size in kB - */ - protected $maxImportSize = 5; - - /** - * @var string[] valid import extensions - */ - protected $importExtensions = array( - 'gif' => 'data:image/gif', - 'png' => 'data:image/png', - 'jpe' => 'data:image/jpeg', - 'jpg' => 'data:image/jpeg', - 'jpeg' => 'data:image/jpeg', - 'svg' => 'data:image/svg+xml', - 'woff' => 'data:application/x-font-woff', - 'tif' => 'image/tiff', - 'tiff' => 'image/tiff', - 'xbm' => 'image/x-xbitmap', - ); - - /** - * Set the maximum size if files to be imported. - * - * Files larger than this size (in kB) will not be imported into the CSS. - * Importing files into the CSS as data-uri will save you some connections, - * but we should only import relatively small decorative images so that our - * CSS file doesn't get too bulky. - * - * @param int $size Size in kB - */ - public function setMaxImportSize($size) - { - $this->maxImportSize = $size; - } - - /** - * Set the type of extensions to be imported into the CSS (to save network - * connections). - * Keys of the array should be the file extensions & respective values - * should be the data type. - * - * @param string[] $extensions Array of file extensions - */ - public function setImportExtensions(array $extensions) - { - $this->importExtensions = $extensions; - } - - /** - * Move any import statements to the top. - * - * @param string $content Nearly finished CSS content - * - * @return string - */ - protected function moveImportsToTop($content) - { - if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) { - // remove from content - foreach ($matches[0] as $import) { - $content = str_replace($import, '', $content); - } - - // add to top - $content = implode(';', $matches[2]).';'.trim($content, ';'); - } - - return $content; - } - - /** - * Combine CSS from import statements. - * - * @import's will be loaded and their content merged into the original file, - * to save HTTP requests. - * - * @param string $source The file to combine imports for - * @param string $content The CSS content to combine imports for - * @param string[] $parents Parent paths, for circular reference checks - * - * @return string - * - * @throws FileImportException - */ - protected function combineImports($source, $content, $parents) - { - $importRegexes = array( - // @import url(xxx) - '/ - # import statement - @import - - # whitespace - \s+ - - # open url() - url\( - - # (optional) open path enclosure - (?P<quotes>["\']?) - - # fetch path - (?P<path>.+?) - - # (optional) close path enclosure - (?P=quotes) - - # close url() - \) - - # (optional) trailing whitespace - \s* - - # (optional) media statement(s) - (?P<media>[^;]*) - - # (optional) trailing whitespace - \s* - - # (optional) closing semi-colon - ;? - - /ix', - - // @import 'xxx' - '/ - - # import statement - @import - - # whitespace - \s+ - - # open path enclosure - (?P<quotes>["\']) - - # fetch path - (?P<path>.+?) - - # close path enclosure - (?P=quotes) - - # (optional) trailing whitespace - \s* - - # (optional) media statement(s) - (?P<media>[^;]*) - - # (optional) trailing whitespace - \s* - - # (optional) closing semi-colon - ;? - - /ix', - ); - - // find all relative imports in css - $matches = array(); - foreach ($importRegexes as $importRegex) { - if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) { - $matches = array_merge($matches, $regexMatches); - } - } - - $search = array(); - $replace = array(); - - // loop the matches - foreach ($matches as $match) { - // get the path for the file that will be imported - $importPath = dirname($source).'/'.$match['path']; - - // only replace the import with the content if we can grab the - // content of the file - if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) { - continue; - } - - // check if current file was not imported previously in the same - // import chain. - if (in_array($importPath, $parents)) { - throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.'); - } - - // grab referenced file & minify it (which may include importing - // yet other @import statements recursively) - $minifier = new static($importPath); - $minifier->setMaxImportSize($this->maxImportSize); - $minifier->setImportExtensions($this->importExtensions); - $importContent = $minifier->execute($source, $parents); - - // check if this is only valid for certain media - if (!empty($match['media'])) { - $importContent = '@media '.$match['media'].'{'.$importContent.'}'; - } - - // add to replacement array - $search[] = $match[0]; - $replace[] = $importContent; - } - - // replace the import statements - return str_replace($search, $replace, $content); - } - - /** - * Import files into the CSS, base64-ized. - * - * @url(image.jpg) images will be loaded and their content merged into the - * original file, to save HTTP requests. - * - * @param string $source The file to import files for - * @param string $content The CSS content to import files for - * - * @return string - */ - protected function importFiles($source, $content) - { - $regex = '/url\((["\']?)(.+?)\\1\)/i'; - if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { - $search = array(); - $replace = array(); - - // loop the matches - foreach ($matches as $match) { - $extension = substr(strrchr($match[2], '.'), 1); - if ($extension && !array_key_exists($extension, $this->importExtensions)) { - continue; - } - - // get the path for the file that will be imported - $path = $match[2]; - $path = dirname($source).'/'.$path; - - // only replace the import with the content if we're able to get - // the content of the file, and it's relatively small - if ($this->canImportFile($path) && $this->canImportBySize($path)) { - // grab content && base64-ize - $importContent = $this->load($path); - $importContent = base64_encode($importContent); - - // build replacement - $search[] = $match[0]; - $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')'; - } - } - - // replace the import statements - $content = str_replace($search, $replace, $content); - } - - return $content; - } - - /** - * Minify the data. - * Perform CSS optimizations. - * - * @param string[optional] $path Path to write the data to - * @param string[] $parents Parent paths, for circular reference checks - * - * @return string The minified data - */ - public function execute($path = null, $parents = array()) - { - $content = ''; - - // loop CSS data (raw data and files) - foreach ($this->data as $source => $css) { - /* - * Let's first take out strings & comments, since we can't just - * remove whitespace anywhere. If whitespace occurs inside a string, - * we should leave it alone. E.g.: - * p { content: "a test" } - */ - $this->extractStrings(); - $this->stripComments(); - $this->extractCalcs(); - $css = $this->replace($css); - - $css = $this->stripWhitespace($css); - $css = $this->shortenColors($css); - $css = $this->shortenZeroes($css); - $css = $this->shortenFontWeights($css); - $css = $this->stripEmptyTags($css); - - // restore the string we've extracted earlier - $css = $this->restoreExtractedData($css); - - $source = is_int($source) ? '' : $source; - $parents = $source ? array_merge($parents, array($source)) : $parents; - $css = $this->combineImports($source, $css, $parents); - $css = $this->importFiles($source, $css); - - /* - * If we'll save to a new path, we'll have to fix the relative paths - * to be relative no longer to the source file, but to the new path. - * If we don't write to a file, fall back to same path so no - * conversion happens (because we still want it to go through most - * of the move code, which also addresses url() & @import syntax...) - */ - $converter = $this->getPathConverter($source, $path ?: $source); - $css = $this->move($converter, $css); - - // combine css - $content .= $css; - } - - $content = $this->moveImportsToTop($content); - - return $content; - } - - /** - * Moving a css file should update all relative urls. - * Relative references (e.g. ../images/image.gif) in a certain css file, - * will have to be updated when a file is being saved at another location - * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper). - * - * @param ConverterInterface $converter Relative path converter - * @param string $content The CSS content to update relative urls for - * - * @return string - */ - protected function move(ConverterInterface $converter, $content) - { - /* - * Relative path references will usually be enclosed by url(). @import - * is an exception, where url() is not necessary around the path (but is - * allowed). - * This *could* be 1 regular expression, where both regular expressions - * in this array are on different sides of a |. But we're using named - * patterns in both regexes, the same name on both regexes. This is only - * possible with a (?J) modifier, but that only works after a fairly - * recent PCRE version. That's why I'm doing 2 separate regular - * expressions & combining the matches after executing of both. - */ - $relativeRegexes = array( - // url(xxx) - '/ - # open url() - url\( - - \s* - - # open path enclosure - (?P<quotes>["\'])? - - # fetch path - (?P<path>.+?) - - # close path enclosure - (?(quotes)(?P=quotes)) - - \s* - - # close url() - \) - - /ix', - - // @import "xxx" - '/ - # import statement - @import - - # whitespace - \s+ - - # we don\'t have to check for @import url(), because the - # condition above will already catch these - - # open path enclosure - (?P<quotes>["\']) - - # fetch path - (?P<path>.+?) - - # close path enclosure - (?P=quotes) - - /ix', - ); - - // find all relative urls in css - $matches = array(); - foreach ($relativeRegexes as $relativeRegex) { - if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) { - $matches = array_merge($matches, $regexMatches); - } - } - - $search = array(); - $replace = array(); - - // loop all urls - foreach ($matches as $match) { - // determine if it's a url() or an @import match - $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url'); - - $url = $match['path']; - if ($this->canImportByPath($url)) { - // attempting to interpret GET-params makes no sense, so let's discard them for awhile - $params = strrchr($url, '?'); - $url = $params ? substr($url, 0, -strlen($params)) : $url; - - // fix relative url - $url = $converter->convert($url); - - // now that the path has been converted, re-apply GET-params - $url .= $params; - } - - /* - * Urls with control characters above 0x7e should be quoted. - * According to Mozilla's parser, whitespace is only allowed at the - * end of unquoted urls. - * Urls with `)` (as could happen with data: uris) should also be - * quoted to avoid being confused for the url() closing parentheses. - * And urls with a # have also been reported to cause issues. - * Urls with quotes inside should also remain escaped. - * - * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation - * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378 - * @see https://github.com/matthiasmullie/minify/issues/193 - */ - $url = trim($url); - if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) { - $url = $match['quotes'] . $url . $match['quotes']; - } - - // build replacement - $search[] = $match[0]; - if ($type === 'url') { - $replace[] = 'url('.$url.')'; - } elseif ($type === 'import') { - $replace[] = '@import "'.$url.'"'; - } - } - - // replace urls - return str_replace($search, $replace, $content); - } - - /** - * Shorthand hex color codes. - * #FF0000 -> #F00. - * - * @param string $content The CSS content to shorten the hex color codes for - * - * @return string - */ - protected function shortenColors($content) - { - $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?:([0-9a-z])\\4)?(?=[; }])/i', '#$1$2$3$4', $content); - - // remove alpha channel if it's pointless... - $content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content); - $content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content); - - $colors = array( - // we can shorten some even more by replacing them with their color name - '#F0FFFF' => 'azure', - '#F5F5DC' => 'beige', - '#A52A2A' => 'brown', - '#FF7F50' => 'coral', - '#FFD700' => 'gold', - '#808080' => 'gray', - '#008000' => 'green', - '#4B0082' => 'indigo', - '#FFFFF0' => 'ivory', - '#F0E68C' => 'khaki', - '#FAF0E6' => 'linen', - '#800000' => 'maroon', - '#000080' => 'navy', - '#808000' => 'olive', - '#CD853F' => 'peru', - '#FFC0CB' => 'pink', - '#DDA0DD' => 'plum', - '#800080' => 'purple', - '#F00' => 'red', - '#FA8072' => 'salmon', - '#A0522D' => 'sienna', - '#C0C0C0' => 'silver', - '#FFFAFA' => 'snow', - '#D2B48C' => 'tan', - '#FF6347' => 'tomato', - '#EE82EE' => 'violet', - '#F5DEB3' => 'wheat', - // or the other way around - 'WHITE' => '#fff', - 'BLACK' => '#000', - ); - - return preg_replace_callback( - '/(?<=[: ])('.implode('|', array_keys($colors)).')(?=[; }])/i', - function ($match) use ($colors) { - return $colors[strtoupper($match[0])]; - }, - $content - ); - } - - /** - * Shorten CSS font weights. - * - * @param string $content The CSS content to shorten the font weights for - * - * @return string - */ - protected function shortenFontWeights($content) - { - $weights = array( - 'normal' => 400, - 'bold' => 700, - ); - - $callback = function ($match) use ($weights) { - return $match[1].$weights[$match[2]]; - }; - - return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content); - } - - /** - * Shorthand 0 values to plain 0, instead of e.g. -0em. - * - * @param string $content The CSS content to shorten the zero values for - * - * @return string - */ - protected function shortenZeroes($content) - { - // we don't want to strip units in `calc()` expressions: - // `5px - 0px` is valid, but `5px - 0` is not - // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but - // `10 * 0` is invalid - // we've extracted calcs earlier, so we don't need to worry about this - - // reusable bits of code throughout these regexes: - // before & after are used to make sure we don't match lose unintended - // 0-like values (e.g. in #000, or in http://url/1.0) - // units can be stripped from 0 values, or used to recognize non 0 - // values (where wa may be able to strip a .0 suffix) - $before = '(?<=[:(, ])'; - $after = '(?=[ ,);}])'; - $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)'; - - // strip units after zeroes (0px -> 0) - // NOTE: it should be safe to remove all units for a 0 value, but in - // practice, Webkit (especially Safari) seems to stumble over at least - // 0%, potentially other units as well. Only stripping 'px' for now. - // @see https://github.com/matthiasmullie/minify/issues/60 - $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content); - - // strip 0-digits (.0 -> 0) - $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content); - // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px - $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content); - // strip trailing 0: 50.00 -> 50, 50.00px -> 50px - $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content); - // strip leading 0: 0.1 -> .1, 01.1 -> 1.1 - $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content); - - // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0) - $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content); - - // IE doesn't seem to understand a unitless flex-basis value (correct - - // it goes against the spec), so let's add it in again (make it `%`, - // which is only 1 char: 0%, 0px, 0 anything, it's all just the same) - // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex - $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content); - $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content); - - return $content; - } - - /** - * Strip empty tags from source code. - * - * @param string $content - * - * @return string - */ - protected function stripEmptyTags($content) - { - $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content); - $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content); - - return $content; - } - - /** - * Strip comments from source code. - */ - protected function stripComments() - { - // PHP only supports $this inside anonymous functions since 5.4 - $minifier = $this; - $callback = function ($match) use ($minifier) { - $count = count($minifier->extracted); - $placeholder = '/*'.$count.'*/'; - $minifier->extracted[$placeholder] = $match[0]; - - return $placeholder; - }; - $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback); - - $this->registerPattern('/\/\*.*?\*\//s', ''); - } - - /** - * Strip whitespace. - * - * @param string $content The CSS content to strip the whitespace for - * - * @return string - */ - protected function stripWhitespace($content) - { - // remove leading & trailing whitespace - $content = preg_replace('/^\s*/m', '', $content); - $content = preg_replace('/\s*$/m', '', $content); - - // replace newlines with a single space - $content = preg_replace('/\s+/', ' ', $content); - - // remove whitespace around meta characters - // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex - $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content); - $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content); - $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content); - $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content); - - // whitespace around + and - can only be stripped inside some pseudo- - // classes, like `:nth-child(3+2n)` - // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or - // selectors like `div.weird- p` - $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type'); - $content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content); - - // remove semicolon/whitespace followed by closing bracket - $content = str_replace(';}', '}', $content); - - return trim($content); - } - - /** - * Replace all `calc()` occurrences. - */ - protected function extractCalcs() - { - // PHP only supports $this inside anonymous functions since 5.4 - $minifier = $this; - $callback = function ($match) use ($minifier) { - $length = strlen($match[1]); - $expr = ''; - $opened = 0; - - for ($i = 0; $i < $length; $i++) { - $char = $match[1][$i]; - $expr .= $char; - if ($char === '(') { - $opened++; - } elseif ($char === ')' && --$opened === 0) { - break; - } - } - $rest = str_replace($expr, '', $match[1]); - $expr = trim(substr($expr, 1, -1)); - - $count = count($minifier->extracted); - $placeholder = 'calc('.$count.')'; - $minifier->extracted[$placeholder] = 'calc('.$expr.')'; - - return $placeholder.$rest; - }; - - $this->registerPattern('/calc(\(.+?)(?=$|;|calc\()/', $callback); - } - - /** - * Check if file is small enough to be imported. - * - * @param string $path The path to the file - * - * @return bool - */ - protected function canImportBySize($path) - { - return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024; - } - - /** - * Check if file a file can be imported, going by the path. - * - * @param string $path - * - * @return bool - */ - protected function canImportByPath($path) - { - return preg_match('/^(data:|https?:|\\/)/', $path) === 0; - } - - /** - * Return a converter to update relative paths to be relative to the new - * destination. - * - * @param string $source - * @param string $target - * - * @return ConverterInterface - */ - protected function getPathConverter($source, $target) - { - return new Converter($source, $target); - } -} - -class JS extends Minify -{ - /** - * Var-matching regex based on http://stackoverflow.com/a/9337047/802993. - * - * Note that regular expressions using that bit must have the PCRE_UTF8 - * pattern modifier (/u) set. - * - * @var string - */ - const REGEX_VARIABLE = '\b[$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}][$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}0-9\x{0300}-\x{036f}\x{0483}-\x{0487}\x{0591}-\x{05bd}\x{05bf}\x{05c1}\x{05c2}\x{05c4}\x{05c5}\x{05c7}\x{0610}-\x{061a}\x{064b}-\x{0669}\x{0670}\x{06d6}-\x{06dc}\x{06df}-\x{06e4}\x{06e7}\x{06e8}\x{06ea}-\x{06ed}\x{06f0}-\x{06f9}\x{0711}\x{0730}-\x{074a}\x{07a6}-\x{07b0}\x{07c0}-\x{07c9}\x{07eb}-\x{07f3}\x{0816}-\x{0819}\x{081b}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082d}\x{0859}-\x{085b}\x{08e4}-\x{08fe}\x{0900}-\x{0903}\x{093a}-\x{093c}\x{093e}-\x{094f}\x{0951}-\x{0957}\x{0962}\x{0963}\x{0966}-\x{096f}\x{0981}-\x{0983}\x{09bc}\x{09be}-\x{09c4}\x{09c7}\x{09c8}\x{09cb}-\x{09cd}\x{09d7}\x{09e2}\x{09e3}\x{09e6}-\x{09ef}\x{0a01}-\x{0a03}\x{0a3c}\x{0a3e}-\x{0a42}\x{0a47}\x{0a48}\x{0a4b}-\x{0a4d}\x{0a51}\x{0a66}-\x{0a71}\x{0a75}\x{0a81}-\x{0a83}\x{0abc}\x{0abe}-\x{0ac5}\x{0ac7}-\x{0ac9}\x{0acb}-\x{0acd}\x{0ae2}\x{0ae3}\x{0ae6}-\x{0aef}\x{0b01}-\x{0b03}\x{0b3c}\x{0b3e}-\x{0b44}\x{0b47}\x{0b48}\x{0b4b}-\x{0b4d}\x{0b56}\x{0b57}\x{0b62}\x{0b63}\x{0b66}-\x{0b6f}\x{0b82}\x{0bbe}-\x{0bc2}\x{0bc6}-\x{0bc8}\x{0bca}-\x{0bcd}\x{0bd7}\x{0be6}-\x{0bef}\x{0c01}-\x{0c03}\x{0c3e}-\x{0c44}\x{0c46}-\x{0c48}\x{0c4a}-\x{0c4d}\x{0c55}\x{0c56}\x{0c62}\x{0c63}\x{0c66}-\x{0c6f}\x{0c82}\x{0c83}\x{0cbc}\x{0cbe}-\x{0cc4}\x{0cc6}-\x{0cc8}\x{0cca}-\x{0ccd}\x{0cd5}\x{0cd6}\x{0ce2}\x{0ce3}\x{0ce6}-\x{0cef}\x{0d02}\x{0d03}\x{0d3e}-\x{0d44}\x{0d46}-\x{0d48}\x{0d4a}-\x{0d4d}\x{0d57}\x{0d62}\x{0d63}\x{0d66}-\x{0d6f}\x{0d82}\x{0d83}\x{0dca}\x{0dcf}-\x{0dd4}\x{0dd6}\x{0dd8}-\x{0ddf}\x{0df2}\x{0df3}\x{0e31}\x{0e34}-\x{0e3a}\x{0e47}-\x{0e4e}\x{0e50}-\x{0e59}\x{0eb1}\x{0eb4}-\x{0eb9}\x{0ebb}\x{0ebc}\x{0ec8}-\x{0ecd}\x{0ed0}-\x{0ed9}\x{0f18}\x{0f19}\x{0f20}-\x{0f29}\x{0f35}\x{0f37}\x{0f39}\x{0f3e}\x{0f3f}\x{0f71}-\x{0f84}\x{0f86}\x{0f87}\x{0f8d}-\x{0f97}\x{0f99}-\x{0fbc}\x{0fc6}\x{102b}-\x{103e}\x{1040}-\x{1049}\x{1056}-\x{1059}\x{105e}-\x{1060}\x{1062}-\x{1064}\x{1067}-\x{106d}\x{1071}-\x{1074}\x{1082}-\x{108d}\x{108f}-\x{109d}\x{135d}-\x{135f}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}\x{1753}\x{1772}\x{1773}\x{17b4}-\x{17d3}\x{17dd}\x{17e0}-\x{17e9}\x{180b}-\x{180d}\x{1810}-\x{1819}\x{18a9}\x{1920}-\x{192b}\x{1930}-\x{193b}\x{1946}-\x{194f}\x{19b0}-\x{19c0}\x{19c8}\x{19c9}\x{19d0}-\x{19d9}\x{1a17}-\x{1a1b}\x{1a55}-\x{1a5e}\x{1a60}-\x{1a7c}\x{1a7f}-\x{1a89}\x{1a90}-\x{1a99}\x{1b00}-\x{1b04}\x{1b34}-\x{1b44}\x{1b50}-\x{1b59}\x{1b6b}-\x{1b73}\x{1b80}-\x{1b82}\x{1ba1}-\x{1bad}\x{1bb0}-\x{1bb9}\x{1be6}-\x{1bf3}\x{1c24}-\x{1c37}\x{1c40}-\x{1c49}\x{1c50}-\x{1c59}\x{1cd0}-\x{1cd2}\x{1cd4}-\x{1ce8}\x{1ced}\x{1cf2}-\x{1cf4}\x{1dc0}-\x{1de6}\x{1dfc}-\x{1dff}\x{200c}\x{200d}\x{203f}\x{2040}\x{2054}\x{20d0}-\x{20dc}\x{20e1}\x{20e5}-\x{20f0}\x{2cef}-\x{2cf1}\x{2d7f}\x{2de0}-\x{2dff}\x{302a}-\x{302f}\x{3099}\x{309a}\x{a620}-\x{a629}\x{a66f}\x{a674}-\x{a67d}\x{a69f}\x{a6f0}\x{a6f1}\x{a802}\x{a806}\x{a80b}\x{a823}-\x{a827}\x{a880}\x{a881}\x{a8b4}-\x{a8c4}\x{a8d0}-\x{a8d9}\x{a8e0}-\x{a8f1}\x{a900}-\x{a909}\x{a926}-\x{a92d}\x{a947}-\x{a953}\x{a980}-\x{a983}\x{a9b3}-\x{a9c0}\x{a9d0}-\x{a9d9}\x{aa29}-\x{aa36}\x{aa43}\x{aa4c}\x{aa4d}\x{aa50}-\x{aa59}\x{aa7b}\x{aab0}\x{aab2}-\x{aab4}\x{aab7}\x{aab8}\x{aabe}\x{aabf}\x{aac1}\x{aaeb}-\x{aaef}\x{aaf5}\x{aaf6}\x{abe3}-\x{abea}\x{abec}\x{abed}\x{abf0}-\x{abf9}\x{fb1e}\x{fe00}-\x{fe0f}\x{fe20}-\x{fe26}\x{fe33}\x{fe34}\x{fe4d}-\x{fe4f}\x{ff10}-\x{ff19}\x{ff3f}]*\b'; - - /** - * Full list of JavaScript reserved words. - * Will be loaded from /data/js/keywords_reserved.txt. - * - * @see https://mathiasbynens.be/notes/reserved-keywords - * - * @var string[] - */ - protected $keywordsReserved = array(); - - /** - * List of JavaScript reserved words that accept a <variable, value, ...> - * after them. Some end of lines are not the end of a statement, like with - * these keywords. - * - * E.g.: we shouldn't insert a ; after this else - * else - * console.log('this is quite fine') - * - * Will be loaded from /data/js/keywords_before.txt - * - * @var string[] - */ - protected $keywordsBefore = array(); - - /** - * List of JavaScript reserved words that accept a <variable, value, ...> - * before them. Some end of lines are not the end of a statement, like when - * continued by one of these keywords on the newline. - * - * E.g.: we shouldn't insert a ; before this instanceof - * variable - * instanceof String - * - * Will be loaded from /data/js/keywords_after.txt - * - * @var string[] - */ - protected $keywordsAfter = array(); - - /** - * List of all JavaScript operators. - * - * Will be loaded from /data/js/operators.txt - * - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators - * - * @var string[] - */ - protected $operators = array(); - - /** - * List of JavaScript operators that accept a <variable, value, ...> after - * them. Some end of lines are not the end of a statement, like with these - * operators. - * - * Note: Most operators are fine, we've only removed ++ and --. - * ++ & -- have to be joined with the value they're in-/decrementing. - * - * Will be loaded from /data/js/operators_before.txt - * - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators - * - * @var string[] - */ - protected $operatorsBefore = array(); - - /** - * List of JavaScript operators that accept a <variable, value, ...> before - * them. Some end of lines are not the end of a statement, like when - * continued by one of these operators on the newline. - * - * Note: Most operators are fine, we've only removed ), ], ++, --, ! and ~. - * There can't be a newline separating ! or ~ and whatever it is negating. - * ++ & -- have to be joined with the value they're in-/decrementing. - * ) & ] are "special" in that they have lots or usecases. () for example - * is used for function calls, for grouping, in if () and for (), ... - * - * Will be loaded from /data/js/operators_after.txt - * - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators - * - * @var string[] - */ - protected $operatorsAfter = array(); - - /** - * {@inheritdoc} - */ - public function __construct() - { - call_user_func_array(array('parent', '__construct'), func_get_args()); - - $dataDir = __DIR__.'/../data/js/'; - $options = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES; - $this->keywordsReserved = file($dataDir.'keywords_reserved.txt', $options); - $this->keywordsBefore = file($dataDir.'keywords_before.txt', $options); - $this->keywordsAfter = file($dataDir.'keywords_after.txt', $options); - $this->operators = file($dataDir.'operators.txt', $options); - $this->operatorsBefore = file($dataDir.'operators_before.txt', $options); - $this->operatorsAfter = file($dataDir.'operators_after.txt', $options); - } - - /** - * Minify the data. - * Perform JS optimizations. - * - * @param string[optional] $path Path to write the data to - * - * @return string The minified data - */ - public function execute($path = null) - { - $content = ''; - - /* - * Let's first take out strings, comments and regular expressions. - * All of these can contain JS code-like characters, and we should make - * sure any further magic ignores anything inside of these. - * - * Consider this example, where we should not strip any whitespace: - * var str = "a test"; - * - * Comments will be removed altogether, strings and regular expressions - * will be replaced by placeholder text, which we'll restore later. - */ - $this->extractStrings('\'"`'); - $this->stripComments(); - $this->extractRegex(); - - // loop files - foreach ($this->data as $source => $js) { - // take out strings, comments & regex (for which we've registered - // the regexes just a few lines earlier) - $js = $this->replace($js); - - $js = $this->propertyNotation($js); - $js = $this->shortenBools($js); - $js = $this->stripWhitespace($js); - - // combine js: separating the scripts by a ; - $content .= $js.";"; - } - - // clean up leftover `;`s from the combination of multiple scripts - $content = ltrim($content, ';'); - $content = (string) substr($content, 0, -1); - - /* - * Earlier, we extracted strings & regular expressions and replaced them - * with placeholder text. This will restore them. - */ - $content = $this->restoreExtractedData($content); - - return $content; - } - - /** - * Strip comments from source code. - */ - protected function stripComments() - { - // PHP only supports $this inside anonymous functions since 5.4 - $minifier = $this; - $callback = function ($match) use ($minifier) { - $count = count($minifier->extracted); - $placeholder = '/*'.$count.'*/'; - $minifier->extracted[$placeholder] = $match[0]; - - return $placeholder; - }; - // multi-line comments - $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback); - $this->registerPattern('/\/\*.*?\*\//s', ''); - - // single-line comments - $this->registerPattern('/\/\/.*$/m', ''); - } - - /** - * JS can have /-delimited regular expressions, like: /ab+c/.match(string). - * - * The content inside the regex can contain characters that may be confused - * for JS code: e.g. it could contain whitespace it needs to match & we - * don't want to strip whitespace in there. - * - * The regex can be pretty simple: we don't have to care about comments, - * (which also use slashes) because stripComments() will have stripped those - * already. - * - * This method will replace all string content with simple REGEX# - * placeholder text, so we've rid all regular expressions from characters - * that may be misinterpreted. Original regex content will be saved in - * $this->extracted and after doing all other minifying, we can restore the - * original content via restoreRegex() - */ - protected function extractRegex() - { - // PHP only supports $this inside anonymous functions since 5.4 - $minifier = $this; - $callback = function ($match) use ($minifier) { - $count = count($minifier->extracted); - $placeholder = '"'.$count.'"'; - $minifier->extracted[$placeholder] = $match[0]; - - return $placeholder; - }; - - // match all chars except `/` and `\` - // `\` is allowed though, along with whatever char follows (which is the - // one being escaped) - // this should allow all chars, except for an unescaped `/` (= the one - // closing the regex) - // then also ignore bare `/` inside `[]`, where they don't need to be - // escaped: anything inside `[]` can be ignored safely - $pattern = '\\/(?!\*)(?:[^\\[\\/\\\\\n\r]++|(?:\\\\.)++|(?:\\[(?:[^\\]\\\\\n\r]++|(?:\\\\.)++)++\\])++)++\\/[gimuy]*'; - - // a regular expression can only be followed by a few operators or some - // of the RegExp methods (a `\` followed by a variable or value is - // likely part of a division, not a regex) - $keywords = array('do', 'in', 'new', 'else', 'throw', 'yield', 'delete', 'return', 'typeof'); - $before = '([=:,;\+\-\*\/\}\(\{\[&\|!]|^|'.implode('|', $keywords).')\s*'; - $propertiesAndMethods = array( - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Properties_2 - 'constructor', - 'flags', - 'global', - 'ignoreCase', - 'multiline', - 'source', - 'sticky', - 'unicode', - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Methods_2 - 'compile(', - 'exec(', - 'test(', - 'toSource(', - 'toString(', - ); - $delimiters = array_fill(0, count($propertiesAndMethods), '/'); - $propertiesAndMethods = array_map('preg_quote', $propertiesAndMethods, $delimiters); - $after = '(?=\s*([\.,;\)\}&\|+]|\/\/|$|\.('.implode('|', $propertiesAndMethods).')))'; - $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback); - - // regular expressions following a `)` are rather annoying to detect... - // quite often, `/` after `)` is a division operator & if it happens to - // be followed by another one (or a comment), it is likely to be - // confused for a regular expression - // however, it's perfectly possible for a regex to follow a `)`: after - // a single-line `if()`, `while()`, ... statement, for example - // since, when they occur like that, they're always the start of a - // statement, there's only a limited amount of ways they can be useful: - // by calling the regex methods directly - // if a regex following `)` is not followed by `.<property or method>`, - // it's quite likely not a regex - $before = '\)\s*'; - $after = '(?=\s*\.('.implode('|', $propertiesAndMethods).'))'; - $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback); - - // 1 more edge case: a regex can be followed by a lot more operators or - // keywords if there's a newline (ASI) in between, where the operator - // actually starts a new statement - // (https://github.com/matthiasmullie/minify/issues/56) - $operators = $this->getOperatorsForRegex($this->operatorsBefore, '/'); - $operators += $this->getOperatorsForRegex($this->keywordsReserved, '/'); - $after = '(?=\s*\n\s*('.implode('|', $operators).'))'; - $this->registerPattern('/'.$pattern.$after.'/', $callback); - } - - /** - * Strip whitespace. - * - * We won't strip *all* whitespace, but as much as possible. The thing that - * we'll preserve are newlines we're unsure about. - * JavaScript doesn't require statements to be terminated with a semicolon. - * It will automatically fix missing semicolons with ASI (automatic semi- - * colon insertion) at the end of line causing errors (without semicolon.) - * - * Because it's sometimes hard to tell if a newline is part of a statement - * that should be terminated or not, we'll just leave some of them alone. - * - * @param string $content The content to strip the whitespace for - * - * @return string - */ - protected function stripWhitespace($content) - { - // uniform line endings, make them all line feed - $content = str_replace(array("\r\n", "\r"), "\n", $content); - - // collapse all non-line feed whitespace into a single space - $content = preg_replace('/[^\S\n]+/', ' ', $content); - - // strip leading & trailing whitespace - $content = str_replace(array(" \n", "\n "), "\n", $content); - - // collapse consecutive line feeds into just 1 - $content = preg_replace('/\n+/', "\n", $content); - - $operatorsBefore = $this->getOperatorsForRegex($this->operatorsBefore, '/'); - $operatorsAfter = $this->getOperatorsForRegex($this->operatorsAfter, '/'); - $operators = $this->getOperatorsForRegex($this->operators, '/'); - $keywordsBefore = $this->getKeywordsForRegex($this->keywordsBefore, '/'); - $keywordsAfter = $this->getKeywordsForRegex($this->keywordsAfter, '/'); - - // strip whitespace that ends in (or next line begin with) an operator - // that allows statements to be broken up over multiple lines - unset($operatorsBefore['+'], $operatorsBefore['-'], $operatorsAfter['+'], $operatorsAfter['-']); - $content = preg_replace( - array( - '/('.implode('|', $operatorsBefore).')\s+/', - '/\s+('.implode('|', $operatorsAfter).')/', - ), - '\\1', - $content - ); - - // make sure + and - can't be mistaken for, or joined into ++ and -- - $content = preg_replace( - array( - '/(?<![\+\-])\s*([\+\-])(?![\+\-])/', - '/(?<![\+\-])([\+\-])\s*(?![\+\-])/', - ), - '\\1', - $content - ); - - // collapse whitespace around reserved words into single space - $content = preg_replace('/(^|[;\}\s])\K('.implode('|', $keywordsBefore).')\s+/', '\\2 ', $content); - $content = preg_replace('/\s+('.implode('|', $keywordsAfter).')(?=([;\{\s]|$))/', ' \\1', $content); - - /* - * We didn't strip whitespace after a couple of operators because they - * could be used in different contexts and we can't be sure it's ok to - * strip the newlines. However, we can safely strip any non-line feed - * whitespace that follows them. - */ - $operatorsDiffBefore = array_diff($operators, $operatorsBefore); - $operatorsDiffAfter = array_diff($operators, $operatorsAfter); - $content = preg_replace('/('.implode('|', $operatorsDiffBefore).')[^\S\n]+/', '\\1', $content); - $content = preg_replace('/[^\S\n]+('.implode('|', $operatorsDiffAfter).')/', '\\1', $content); - - /* - * Whitespace after `return` can be omitted in a few occasions - * (such as when followed by a string or regex) - * Same for whitespace in between `)` and `{`, or between `{` and some - * keywords. - */ - $content = preg_replace('/\breturn\s+(["\'\/\+\-])/', 'return$1', $content); - $content = preg_replace('/\)\s+\{/', '){', $content); - $content = preg_replace('/}\n(else|catch|finally)\b/', '}$1', $content); - - /* - * Get rid of double semicolons, except where they can be used like: - * "for(v=1,_=b;;)", "for(v=1;;v++)" or "for(;;ja||(ja=true))". - * I'll safeguard these double semicolons inside for-loops by - * temporarily replacing them with an invalid condition: they won't have - * a double semicolon and will be easy to spot to restore afterwards. - */ - $content = preg_replace('/\bfor\(([^;]*);;([^;]*)\)/', 'for(\\1;-;\\2)', $content); - $content = preg_replace('/;+/', ';', $content); - $content = preg_replace('/\bfor\(([^;]*);-;([^;]*)\)/', 'for(\\1;;\\2)', $content); - - /* - * Next, we'll be removing all semicolons where ASI kicks in. - * for-loops however, can have an empty body (ending in only a - * semicolon), like: `for(i=1;i<3;i++);`, of `for(i in list);` - * Here, nothing happens during the loop; it's just used to keep - * increasing `i`. With that ; omitted, the next line would be expected - * to be the for-loop's body... Same goes for while loops. - * I'm going to double that semicolon (if any) so after the next line, - * which strips semicolons here & there, we're still left with this one. - */ - $content = preg_replace('/(for\([^;\{]*;[^;\{]*;[^;\{]*\));(\}|$)/s', '\\1;;\\2', $content); - $content = preg_replace('/(for\([^;\{]+\s+in\s+[^;\{]+\));(\}|$)/s', '\\1;;\\2', $content); - /* - * Below will also keep `;` after a `do{}while();` along with `while();` - * While these could be stripped after do-while, detecting this - * distinction is cumbersome, so I'll play it safe and make sure `;` - * after any kind of `while` is kept. - */ - $content = preg_replace('/(while\([^;\{]+\));(\}|$)/s', '\\1;;\\2', $content); - - /* - * We also can't strip empty else-statements. Even though they're - * useless and probably shouldn't be in the code in the first place, we - * shouldn't be stripping the `;` that follows it as it breaks the code. - * We can just remove those useless else-statements completely. - * - * @see https://github.com/matthiasmullie/minify/issues/91 - */ - $content = preg_replace('/else;/s', '', $content); - - /* - * We also don't really want to terminate statements followed by closing - * curly braces (which we've ignored completely up until now) or end-of- - * script: ASI will kick in here & we're all about minifying. - * Semicolons at beginning of the file don't make any sense either. - */ - $content = preg_replace('/;(\}|$)/s', '\\1', $content); - $content = ltrim($content, ';'); - - // get rid of remaining whitespace af beginning/end - return trim($content); - } - - /** - * We'll strip whitespace around certain operators with regular expressions. - * This will prepare the given array by escaping all characters. - * - * @param string[] $operators - * @param string $delimiter - * - * @return string[] - */ - protected function getOperatorsForRegex(array $operators, $delimiter = '/') - { - // escape operators for use in regex - $delimiters = array_fill(0, count($operators), $delimiter); - $escaped = array_map('preg_quote', $operators, $delimiters); - - $operators = array_combine($operators, $escaped); - - // ignore + & - for now, they'll get special treatment - unset($operators['+'], $operators['-']); - - // dot can not just immediately follow a number; it can be confused for - // decimal point, or calling a method on it, e.g. 42 .toString() - $operators['.'] = '(?<![0-9]\s)\.'; - - // don't confuse = with other assignment shortcuts (e.g. +=) - $chars = preg_quote('+-*\=<>%&|', $delimiter); - $operators['='] = '(?<!['.$chars.'])\='; - - return $operators; - } - - /** - * We'll strip whitespace around certain keywords with regular expressions. - * This will prepare the given array by escaping all characters. - * - * @param string[] $keywords - * @param string $delimiter - * - * @return string[] - */ - protected function getKeywordsForRegex(array $keywords, $delimiter = '/') - { - // escape keywords for use in regex - $delimiter = array_fill(0, count($keywords), $delimiter); - $escaped = array_map('preg_quote', $keywords, $delimiter); - - // add word boundaries - array_walk($keywords, function ($value) { - return '\b'.$value.'\b'; - }); - - $keywords = array_combine($keywords, $escaped); - - return $keywords; - } - - /** - * Replaces all occurrences of array['key'] by array.key. - * - * @param string $content - * - * @return string - */ - protected function propertyNotation($content) - { - // PHP only supports $this inside anonymous functions since 5.4 - $minifier = $this; - $keywords = $this->keywordsReserved; - $callback = function ($match) use ($minifier, $keywords) { - $property = trim($minifier->extracted[$match[1]], '\'"'); - - /* - * Check if the property is a reserved keyword. In this context (as - * property of an object literal/array) it shouldn't matter, but IE8 - * freaks out with "Expected identifier". - */ - if (in_array($property, $keywords)) { - return $match[0]; - } - - /* - * See if the property is in a variable-like format (e.g. - * array['key-here'] can't be replaced by array.key-here since '-' - * is not a valid character there. - */ - if (!preg_match('/^'.$minifier::REGEX_VARIABLE.'$/u', $property)) { - return $match[0]; - } - - return '.'.$property; - }; - - /* - * Figure out if previous character is a variable name (of the array - * we want to use property notation on) - this is to make sure - * standalone ['value'] arrays aren't confused for keys-of-an-array. - * We can (and only have to) check the last character, because PHP's - * regex implementation doesn't allow unfixed-length look-behind - * assertions. - */ - preg_match('/(\[[^\]]+\])[^\]]*$/', static::REGEX_VARIABLE, $previousChar); - $previousChar = $previousChar[1]; - - /* - * Make sure word preceding the ['value'] is not a keyword, e.g. - * return['x']. Because -again- PHP's regex implementation doesn't allow - * unfixed-length look-behind assertions, I'm just going to do a lot of - * separate look-behind assertions, one for each keyword. - */ - $keywords = $this->getKeywordsForRegex($keywords); - $keywords = '(?<!'.implode(')(?<!', $keywords).')'; - - return preg_replace_callback('/(?<='.$previousChar.'|\])'.$keywords.'\[\s*(([\'"])[0-9]+\\2)\s*\]/u', $callback, $content); - } - - /** - * Replaces true & false by !0 and !1. - * - * @param string $content - * - * @return string - */ - protected function shortenBools($content) - { - /* - * 'true' or 'false' could be used as property names (which may be - * followed by whitespace) - we must not replace those! - * Since PHP doesn't allow variable-length (to account for the - * whitespace) lookbehind assertions, I need to capture the leading - * character and check if it's a `.` - */ - $callback = function ($match) { - if (trim($match[1]) === '.') { - return $match[0]; - } - - return $match[1].($match[2] === 'true' ? '!0' : '!1'); - }; - $content = preg_replace_callback('/(^|.\s*)\b(true|false)\b(?!:)/', $callback, $content); - - // for(;;) is exactly the same as while(true), but shorter :) - $content = preg_replace('/\bwhile\(!0\){/', 'for(;;){', $content); - - // now make sure we didn't turn any do ... while(true) into do ... for(;;) - preg_match_all('/\bdo\b/', $content, $dos, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); - - // go backward to make sure positional offsets aren't altered when $content changes - $dos = array_reverse($dos); - foreach ($dos as $do) { - $offsetDo = $do[0][1]; - - // find all `while` (now `for`) following `do`: one of those must be - // associated with the `do` and be turned back into `while` - preg_match_all('/\bfor\(;;\)/', $content, $whiles, PREG_OFFSET_CAPTURE | PREG_SET_ORDER, $offsetDo); - foreach ($whiles as $while) { - $offsetWhile = $while[0][1]; - - $open = substr_count($content, '{', $offsetDo, $offsetWhile - $offsetDo); - $close = substr_count($content, '}', $offsetDo, $offsetWhile - $offsetDo); - if ($open === $close) { - // only restore `while` if amount of `{` and `}` are the same; - // otherwise, that `for` isn't associated with this `do` - $content = substr_replace($content, 'while(!0)', $offsetWhile, strlen('for(;;)')); - break; - } - } - } - - return $content; - } -} - -interface ConverterInterface { - public function convert($path); -} - -class Converter implements ConverterInterface { - public function convert($path) { - return $path; - } -} - -// Minify extensions -// Copyright (c) 2013-2019 Datenstrom - -class MinifyCss extends CSS { } - -class MinifyJavaScript extends JS { - - // Use hardcoded keywords and operators - public function __construct() { - $this->keywordsReserved = array("do", "if", "in", "for", "let", "new", "try", "var", "case", "else", "enum", "eval", "null", "this", "true", "void", "with", "break", "catch", "class", "const", "false", "super", "throw", "while", "yield", "delete", "export", "import", "public", "return", "static", "switch", "typeof", "default", "extends", "finally", "package", "private", "continue", "debugger", "function", "arguments", "interface", "protected", "implements", "instanceof", "abstract", "boolean", "byte", "char", "double", "final", "float", "goto", "int", "long", "native", "short", "synchronized", "throws", "transient", "volatile"); - $this->keywordsBefore = array("do", "in", "let", "new", "var", "case", "else", "enum", "void", "with", "class", "const", "yield", "delete", "export", "import", "public", "static", "typeof", "extends", "package", "private", "function", "protected", "implements", "instanceof"); - $this->keywordsAfter = array("in", "public", "extends", "private", "protected", "implements", "instanceof"); - $this->operators = array("+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", "&", "|", "^", "~", "<<", ">>", ">>>", "==", "===", "!=", "!==", ">", "<", ">=", "<=", "&&", "||", "!", ".", "[", "]", "?", ":", ",", ";", "(", ")", "{", "}"); - $this->operatorsBefore = array("+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", "&", "|", "^", "~", "<<", ">>", ">>>", "==", "===", "!=", "!==", ">", "<", ">=", "<=", "&&", "||", "!", ".", "[", "?", ":", ",", ";", "(", "{"); - $this->operatorsAfter = array("+", "-", "*", "/", "%", "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", "&", "|", "^", "<<", ">>", ">>>", "==", "===", "!=", "!==", ">", "<", ">=", "<=", "&&", "||", ".", "[", "]", "?", ":", ",", ";", "(", ")", "}"); - } -} - -class MinifyBasic extends Minify { - - // Minify data, remove only comments and empty lines - public function execute($path = null) { - $content = ""; - $this->extractStrings(); - foreach ($this->data as $source => $data) { - $data = $this->replace($data); - $data = preg_replace("/\/\*.*?\*\//s", "", $data); - $data = preg_replace("/\/\/.*?[\r\n]+/", "", $data); - $data = preg_replace("/[\r\n]+/", "\n", $data); - $content .= trim($data); - } - return $this->restoreExtractedData($content); - } -} diff --git a/system/extensions/command.php b/system/extensions/command.php @@ -1,623 +0,0 @@ -<?php -// Command extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/command -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowCommand { - const VERSION = "0.8.17"; - const TYPE = "feature"; - const PRIORITY = "3"; - public $yellow; //access to API - public $files; //number of files - public $links; //number of links - public $errors; //number of errors - public $locationsArguments; //locations with location arguments detected - public $locationsArgumentsPagination; //locations with pagination arguments detected - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - } - - // Handle command - public function onCommand($command, $text) { - switch ($command) { - case "": $statusCode = $this->processCommandHelp(); break; - case "build": $statusCode = $this->processCommandBuild($command, $text); break; - case "check": $statusCode = $this->processCommandCheck($command, $text); break; - case "clean": $statusCode = $this->processCommandClean($command, $text); break; - case "serve": $statusCode = $this->processCommandServe($command, $text); break; - default: $statusCode = 0; - } - return $statusCode; - } - - // Handle command help - public function onCommandHelp() { - $help .= "build [directory location]\n"; - $help .= "check [directory location]\n"; - $help .= "clean [directory location]\n"; - $help .= "serve [directory url]\n"; - return $help; - } - - // Process command to show available commands - public function processCommandHelp() { - echo "Datenstrom Yellow is for people who make small websites.\n"; - $lineCounter = 0; - foreach ($this->getCommandHelp() as $line) { - echo(++$lineCounter>1 ? " " : "Syntax: ")."php yellow.php $line\n"; - } - return 200; - } - - // Process command to build static website - public function processCommandBuild($command, $text) { - $statusCode = 0; - list($path, $location) = $this->yellow->toolbox->getTextArguments($text); - if (empty($location) || substru($location, 0, 1)=="/") { - if ($this->checkStaticSettings()) { - $statusCode = $this->buildStaticFiles($path, $location); - } else { - $statusCode = 500; - $this->files = 0; - $this->errors = 1; - $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); - echo "ERROR building files: Please configure CoreStaticUrl in file '$fileName'!\n"; - } - echo "Yellow $command: $this->files file".($this->files!=1 ? "s" : ""); - echo ", $this->errors error".($this->errors!=1 ? "s" : "")."\n"; - } else { - $statusCode = 400; - echo "Yellow $command: Invalid arguments\n"; - } - return $statusCode; - } - - // Build static files - public function buildStaticFiles($path, $locationFilter) { - $path = rtrim(empty($path) ? $this->yellow->system->get("coreStaticDirectory") : $path, "/"); - $this->files = $this->errors = 0; - $this->locationsArguments = $this->locationsArgumentsPagination = array(); - $statusCode = empty($locationFilter) ? $this->cleanStaticFiles($path, $locationFilter) : 200; - $staticUrl = $this->yellow->system->get("coreStaticUrl"); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - $locations = $this->getContentLocations(); - $filesEstimated = count($locations); - foreach ($locations as $location) { - echo "\rBuilding static website ".$this->getProgressPercent($this->files, $filesEstimated, 5, 60)."%... "; - if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; - $statusCode = max($statusCode, $this->buildStaticFile($path, $location, true)); - } - foreach ($this->locationsArguments as $location) { - echo "\rBuilding static website ".$this->getProgressPercent($this->files, $filesEstimated, 5, 60)."%... "; - if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; - $statusCode = max($statusCode, $this->buildStaticFile($path, $location, true)); - } - $filesEstimated = $this->files + count($this->locationsArguments) + count($this->locationsArgumentsPagination); - foreach ($this->locationsArgumentsPagination as $location) { - echo "\rBuilding static website ".$this->getProgressPercent($this->files, $filesEstimated, 5, 95)."%... "; - if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; - if (substru($location, -1)!=$this->yellow->toolbox->getLocationArgumentsSeparator()) { - $statusCode = max($statusCode, $this->buildStaticFile($path, $location, false, true)); - } - for ($pageNumber=2; $pageNumber<=999; ++$pageNumber) { - $statusCodeLocation = $this->buildStaticFile($path, $location.$pageNumber, false, true); - $statusCode = max($statusCode, $statusCodeLocation); - if ($statusCodeLocation==100) break; - } - } - if (empty($locationFilter)) { - foreach ($this->getMediaLocations() as $location) { - $statusCode = max($statusCode, $this->buildStaticFile($path, $location)); - } - foreach ($this->getSystemLocations() as $location) { - $statusCode = max($statusCode, $this->buildStaticFile($path, $location)); - } - foreach ($this->getExtraLocations($path) as $location) { - $statusCode = max($statusCode, $this->buildStaticFile($path, $location)); - } - $statusCode = max($statusCode, $this->buildStaticFile($path, "/error/", false, false, true)); - } - echo "\rBuilding static website 100%... done\n"; - return $statusCode; - } - - // Build static file - public function buildStaticFile($path, $location, $analyse = false, $probe = false, $error = false) { - $this->yellow->content = new YellowContent($this->yellow); - $this->yellow->page = new YellowPage($this->yellow); - $this->yellow->page->fileName = substru($location, 1); - if (!is_readable($this->yellow->page->fileName)) { - ob_start(); - $staticUrl = $this->yellow->system->get("coreStaticUrl"); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - $statusCode = $this->requestStaticFile($scheme, $address, $base, $location); - if ($statusCode<400 || $error) { - $fileData = ob_get_contents(); - $statusCode = $this->saveStaticFile($path, $location, $fileData, $statusCode); - } - ob_end_clean(); - } else { - $statusCode = $this->copyStaticFile($path, $location); - } - if ($statusCode==200 && $analyse) $this->analyseLocations($scheme, $address, $base, $fileData); - if ($statusCode==404 && $probe) $statusCode = 100; - if ($statusCode==404 && $error) $statusCode = 200; - if ($statusCode>=200) ++$this->files; - if ($statusCode>=400) { - ++$this->errors; - echo "\rERROR building location '$location', ".$this->yellow->page->getStatusCode(true)."\n"; - } - if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::buildStaticFile status:$statusCode location:$location<br/>\n"; - return $statusCode; - } - - // Request static file - public function requestStaticFile($scheme, $address, $base, $location) { - list($serverName, $serverPort) = $this->yellow->toolbox->getTextList($address, ":", 2); - if (empty($serverPort)) $serverPort = $scheme=="https" ? 443 : 80; - $_SERVER["HTTPS"] = $scheme=="https" ? "on" : "off"; - $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1"; - $_SERVER["SERVER_NAME"] = $serverName; - $_SERVER["SERVER_PORT"] = $serverPort; - $_SERVER["REQUEST_METHOD"] = "GET"; - $_SERVER["REQUEST_URI"] = $base.$location; - $_SERVER["SCRIPT_NAME"] = $base."/yellow.php"; - $_SERVER["REMOTE_ADDR"] = "127.0.0.1"; - $_REQUEST = array(); - return $this->yellow->request(); - } - - // Save static file - public function saveStaticFile($path, $location, $fileData, $statusCode) { - $modified = strtotime($this->yellow->page->getHeader("Last-Modified")); - if ($modified==0) $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName); - if ($statusCode>=301 && $statusCode<=303) { - $fileData = $this->getStaticRedirect($this->yellow->page->getHeader("Location")); - $modified = time(); - } - $fileName = $this->getStaticFile($path, $location, $statusCode); - if (is_file($fileName)) $this->yellow->toolbox->deleteFile($fileName); - if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || - !$this->yellow->toolbox->modifyFile($fileName, $modified)) { - $statusCode = 500; - $this->yellow->page->statusCode = $statusCode; - $this->yellow->page->set("pageError", "Can't write file '$fileName'!"); - } - return $statusCode; - } - - // Copy static file - public function copyStaticFile($path, $location) { - $statusCode = 200; - $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName); - $fileName = $this->getStaticFile($path, $location, $statusCode); - if (is_file($fileName)) $this->yellow->toolbox->deleteFile($fileName); - if (!$this->yellow->toolbox->copyFile($this->yellow->page->fileName, $fileName, true) || - !$this->yellow->toolbox->modifyFile($fileName, $modified)) { - $statusCode = 500; - $this->yellow->page->statusCode = $statusCode; - $this->yellow->page->set("pageError", "Can't write file '$fileName'!"); - } - return $statusCode; - } - - // Analyse locations with arguments - public function analyseLocations($scheme, $address, $base, $rawData) { - preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $rawData, $matches); - foreach ($matches[2] as $match) { - $location = rawurldecode($match); - if (preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1]; - if (preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $tokens)) { - if ($tokens[1]!=$scheme) continue; - if ($tokens[2]!=$address) continue; - $location = $tokens[3]; - } - if (substru($location, 0, strlenu($base))!=$base) continue; - $location = substru($location, strlenu($base)); - if (!$this->yellow->toolbox->isLocationArguments($location)) continue; - if (!$this->yellow->toolbox->isLocationArgumentsPagination($location)) { - $location = rtrim($location, "/")."/"; - if (!isset($this->locationsArguments[$location])) { - $this->locationsArguments[$location] = $location; - if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLocations detected location:$location<br/>\n"; - } - } else { - $location = rtrim($location, "0..9"); - if (!isset($this->locationsArgumentsPagination[$location])) { - $this->locationsArgumentsPagination[$location] = $location; - if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLocations detected location:$location<br/>\n"; - } - } - } - } - - // Process command to check static files for broken links - public function processCommandCheck($command, $text) { - $statusCode = 0; - list($path, $location) = $this->yellow->toolbox->getTextArguments($text); - if (empty($location) || substru($location, 0, 1)=="/") { - if ($this->checkStaticSettings()) { - $statusCode = $this->checkStaticFiles($path, $location); - } else { - $statusCode = 500; - $this->links = 0; - $this->errors = 1; - $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); - echo "ERROR checking files: Please configure CoreStaticUrl in file '$fileName'!\n"; - } - echo "Yellow $command: $this->links link".($this->links!=1 ? "s" : ""); - echo ", $this->errors error".($this->errors!=1 ? "s" : "")."\n"; - } else { - $statusCode = 400; - echo "Yellow $command: Invalid arguments\n"; - } - return $statusCode; - } - - // Check static files for broken links - public function checkStaticFiles($path, $locationFilter) { - $path = rtrim(empty($path) ? $this->yellow->system->get("coreStaticDirectory") : $path, "/"); - $this->links = $this->errors = 0; - $regex = "/^[^.]+$|".$this->yellow->system->get("coreStaticDefaultFile")."$/"; - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($path, $regex, false, false); - list($statusCodeFiles, $links) = $this->analyseLinks($path, $locationFilter, $fileNames); - list($statusCodeLinks, $broken, $redirect) = $this->analyseStatus($path, $links); - if ($statusCodeLinks!=200) { - $this->showLinks($broken, "Broken links"); - $this->showLinks($redirect, "Redirect links"); - } - return max($statusCodeFiles, $statusCodeLinks); - } - - // Analyse links in static files - public function analyseLinks($path, $locationFilter, $fileNames) { - $statusCode = 200; - $links = array(); - if (!empty($fileNames)) { - $staticUrl = $this->yellow->system->get("coreStaticUrl"); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - foreach ($fileNames as $fileName) { - if (is_readable($fileName)) { - $locationSource = $this->getStaticLocation($path, $fileName); - if (!preg_match("#^$base$locationFilter#", "$base$locationSource")) continue; - $fileData = $this->yellow->toolbox->readFile($fileName); - preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $fileData, $matches); - foreach ($matches[2] as $match) { - $location = rawurldecode($match); - if (preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1]; - if (preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $matches)) { - $url = $location.(empty($matches[3]) ? "/" : ""); - if (!isset($links[$url])) { - $links[$url] = $locationSource; - } else { - $links[$url] .= ",".$locationSource; - } - if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLinks detected url:$url<br/>\n"; - } elseif (substru($location, 0, 1)=="/") { - $url = "$scheme://$address$location"; - if (!isset($links[$url])) { - $links[$url] = $locationSource; - } else { - $links[$url] .= ",".$locationSource; - } - if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLinks detected url:$url<br/>\n"; - } - } - if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::analyseLinks location:$locationSource<br/>\n"; - } else { - $statusCode = 500; - ++$this->errors; - echo "ERROR reading files: Can't read file '$fileName'!\n"; - } - } - $this->links = count($links); - } else { - $statusCode = 500; - ++$this->errors; - echo "ERROR reading files: Can't find files in directory '$path'!\n"; - } - return array($statusCode, $links); - } - - // Analyse link status - public function analyseStatus($path, $links) { - $statusCode = 200; - $remote = $broken = $redirect = $data = array(); - $staticUrl = $this->yellow->system->get("coreStaticUrl"); - $staticUrlLength = strlenu(rtrim($staticUrl, "/")); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - $staticLocations = $this->getContentLocations(true); - foreach ($links as $url=>$value) { - if (preg_match("#^$staticUrl#", $url)) { - $location = substru($url, $staticUrlLength); - $fileName = $path.substru($url, $staticUrlLength); - if (is_readable($fileName)) continue; - if (in_array($location, $staticLocations)) continue; - } - if (preg_match("/^(http|https):/", $url)) $remote[$url] = $value; - } - $remoteNow = 0; - uksort($remote, "strnatcasecmp"); - foreach ($remote as $url=>$value) { - echo "\rChecking static website ".$this->getProgressPercent(++$remoteNow, count($remote), 5, 95)."%... "; - if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::analyseStatus url:$url\n"; - $referer = "$scheme://$address$base".(($pos = strposu($value, ",")) ? substru($value, 0, $pos) : $value); - $statusCodeUrl = $this->getLinkStatus($url, $referer); - if ($statusCodeUrl!=200) { - $statusCode = max($statusCode, $statusCodeUrl); - $data[$url] = "$statusCodeUrl,$value"; - } - } - foreach ($data as $url=>$value) { - $locations = preg_split("/\s*,\s*/", $value); - $statusCodeUrl = array_shift($locations); - foreach ($locations as $location) { - if ($statusCodeUrl==302) continue; - if ($statusCodeUrl>=300 && $statusCodeUrl<=399) { - $redirect["$scheme://$address$base$location -> $url - ".$this->getStatusFormatted($statusCodeUrl)] = $statusCodeUrl; - } else { - $broken["$scheme://$address$base$location -> $url - ".$this->getStatusFormatted($statusCodeUrl)] = $statusCodeUrl; - } - ++$this->errors; - } - } - echo "\rChecking static website 100%... done\n"; - return array($statusCode, $broken, $redirect); - } - - // Show links - public function showLinks($data, $text) { - if (!empty($data)) { - echo "$text\n\n"; - uksort($data, "strnatcasecmp"); - $data = array_slice($data, 0, 99); - foreach ($data as $key=>$value) { - echo "- $key\n"; - } - echo "\n"; - } - } - - // Process command to clean static files - public function processCommandClean($command, $text) { - $statusCode = 0; - list($path, $location) = $this->yellow->toolbox->getTextArguments($text); - if (empty($location) || substru($location, 0, 1)=="/") { - $statusCode = $this->cleanStaticFiles($path, $location); - echo "Yellow $command: Static file".(empty($location) ? "s" : "")." ".($statusCode!=200 ? "not " : "")."cleaned\n"; - } else { - $statusCode = 400; - echo "Yellow $command: Invalid arguments\n"; - } - return $statusCode; - } - - // Clean static files and directories - public function cleanStaticFiles($path, $location) { - $statusCode = 200; - $path = rtrim(empty($path) ? $this->yellow->system->get("coreStaticDirectory") : $path, "/"); - if (empty($location)) { - $statusCode = max($statusCode, $this->broadcastCommand("clean", "all")); - $statusCode = max($statusCode, $this->cleanStaticDirectory($path)); - } else { - if ($this->yellow->lookup->isFileLocation($location)) { - $fileName = $this->getStaticFile($path, $location, $statusCode); - $statusCode = $this->cleanStaticFile($fileName); - } else { - $statusCode = $this->cleanStaticDirectory($path.$location); - } - } - return $statusCode; - } - - // Clean static directory - public function cleanStaticDirectory($path) { - $statusCode = 200; - if (is_dir($path) && $this->checkStaticDirectory($path)) { - if (!$this->yellow->toolbox->deleteDirectory($path)) { - $statusCode = 500; - echo "ERROR cleaning files: Can't delete directory '$path'!\n"; - } - } - return $statusCode; - } - - // Clean static file - public function cleanStaticFile($fileName) { - $statusCode = 200; - if (is_file($fileName)) { - if (!$this->yellow->toolbox->deleteFile($fileName)) { - $statusCode = 500; - echo "ERROR cleaning files: Can't delete file '$fileName'!\n"; - } - } - return $statusCode; - } - - // Broadcast command to other extensions - public function broadcastCommand($command, $text) { - $statusCode = 0; - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onCommand") && $key!="command") { - $statusCode = max($statusCode, $value["obj"]->onCommand($command, $text)); - } - } - return $statusCode; - } - - // Process command to start built-in web server - public function processCommandServe($command, $text) { - list($path, $url) = $this->yellow->toolbox->getTextArguments($text); - if (empty($path) && is_dir($this->yellow->system->get("coreStaticDirectory"))) $path = $this->yellow->system->get("coreStaticDirectory"); - if (empty($url)) $url = "http://localhost:8000"; - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($url); - if ($scheme=="http" && !empty($address)) { - if (!preg_match("/\:\d+$/", $address)) $address .= ":8000"; - echo "Starting built-in web server on $scheme://$address/\n"; - echo "Press Ctrl-C to quit...\n"; - if (empty($path) || $path=="dynamic") { - system("php -S $address yellow.php", $returnStatus); - } else { - system("php -S $address -t $path", $returnStatus); - } - $statusCode = $returnStatus!=0 ? 500 : 200; - if ($statusCode!=200) echo "ERROR starting web server: Please check your arguments!\n"; - } else { - $statusCode = 400; - echo "Yellow $command: Invalid arguments\n"; - } - return $statusCode; - } - - // Check static settings - public function checkStaticSettings() { - return !empty($this->yellow->system->get("coreStaticUrl")); - } - - // Check static directory - public function checkStaticDirectory($path) { - $ok = false; - if (!empty($path)) { - if ($path==rtrim($this->yellow->system->get("coreStaticDirectory"), "/")) $ok = true; - if ($path==rtrim($this->yellow->system->get("coreTrashDirectory"), "/")) $ok = true; - if (is_file("$path/".$this->yellow->system->get("coreStaticDefaultFile"))) $ok = true; - if (is_file("$path/yellow.php")) $ok = false; - } - return $ok; - } - - // Return command help - public function getCommandHelp() { - $data = array(); - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onCommandHelp")) { - foreach (preg_split("/[\r\n]+/", $value["obj"]->onCommandHelp()) as $line) { - list($command, $dummy) = $this->yellow->toolbox->getTextList($line, " ", 2); - if (!empty($command) && !isset($data[$command])) $data[$command] = $line; - } - } - } - uksort($data, "strnatcasecmp"); - return $data; - } - - // Return human readable status - public function getStatusFormatted($statusCode) { - return $this->yellow->toolbox->getHttpStatusFormatted($statusCode, true); - } - - // Return progress in percent - public function getProgressPercent($now, $total, $increments, $max) - { - $percent = intval(($max / $total) * $now); - if ($increments>1) $percent = intval($percent / $increments) * $increments; - return min($max, $percent); - } - - // Return static file - public function getStaticFile($path, $location, $statusCode) { - if ($statusCode<400) { - $fileName = $path.$location; - if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->system->get("coreStaticDefaultFile"); - } elseif ($statusCode==404) { - $fileName = $path."/".$this->yellow->system->get("coreStaticErrorFile"); - } - return $fileName; - } - - // Return static location - public function getStaticLocation($path, $fileName) { - $location = substru($fileName, strlenu($path)); - if (basename($location)==$this->yellow->system->get("coreStaticDefaultFile")) { - $defaultFileLength = strlenu($this->yellow->system->get("coreStaticDefaultFile")); - $location = substru($location, 0, -$defaultFileLength); - } - return $location; - } - - // Return static redirect - public function getStaticRedirect($location) { - $output = "<!DOCTYPE html><html>\n<head>\n"; - $output .= "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n"; - $output .= "<meta http-equiv=\"refresh\" content=\"0;url=".htmlspecialchars($location)."\" />\n"; - $output .= "</head>\n</html>"; - return $output; - } - - // Return content locations - public function getContentLocations($includeAll = false) { - $locations = array(); - $staticUrl = $this->yellow->system->get("coreStaticUrl"); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - $this->yellow->page->setRequestInformation($scheme, $address, $base, "", ""); - foreach ($this->yellow->content->index(true, true) as $page) { - if (preg_match("/exclude/i", $page->get("build")) && !$includeAll) continue; - if ($page->get("status")=="private" || $page->get("status")=="draft") continue; - array_push($locations, $page->location); - } - if (!$this->yellow->content->find("/") && $this->yellow->system->get("coreMultiLanguageMode")) array_unshift($locations, "/"); - return $locations; - } - - // Return media locations - public function getMediaLocations() { - $locations = array(); - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->system->get("coreMediaDirectory"), "/.*/", false, false); - foreach ($fileNames as $fileName) { - array_push($locations, "/".$fileName); - } - return $locations; - } - - // Return system locations - public function getSystemLocations() { - $locations = array(); - $regex = "/\.(css|gif|ico|js|jpg|png|svg|txt|woff|woff2)$/"; - $extensionDirectoryLength = strlenu($this->yellow->system->get("coreExtensionDirectory")); - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->system->get("coreExtensionDirectory"), $regex, false, false); - foreach ($fileNames as $fileName) { - array_push($locations, $this->yellow->system->get("coreExtensionLocation").substru($fileName, $extensionDirectoryLength)); - } - $resourceDirectoryLength = strlenu($this->yellow->system->get("coreResourceDirectory")); - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->system->get("coreResourceDirectory"), $regex, false, false); - foreach ($fileNames as $fileName) { - array_push($locations, $this->yellow->system->get("coreResourceLocation").substru($fileName, $resourceDirectoryLength)); - } - return $locations; - } - - // Return extra locations - public function getExtraLocations($path) { - $locations = array(); - $pathIgnore = "($path/|". - $this->yellow->system->get("coreStaticDirectory")."|". - $this->yellow->system->get("coreCacheDirectory")."|". - $this->yellow->system->get("coreContentDirectory")."|". - $this->yellow->system->get("coreMediaDirectory")."|". - $this->yellow->system->get("coreSystemDirectory").")"; - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive(".", "/.*/", false, false); - foreach ($fileNames as $fileName) { - $fileName = substru($fileName, 2); - if (preg_match("#^$pathIgnore#", $fileName) || $fileName=="yellow.php") continue; - array_push($locations, "/".$fileName); - } - return $locations; - } - - // Return link status - public function getLinkStatus($url, $referer) { - $curlHandle = curl_init(); - curl_setopt($curlHandle, CURLOPT_URL, $url); - curl_setopt($curlHandle, CURLOPT_REFERER, $referer); - curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; DatenstromYellow/".YellowCore::VERSION."; LinkChecker)"); - curl_setopt($curlHandle, CURLOPT_NOBODY, 1); - curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); - curl_exec($curlHandle); - $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); - curl_close($curlHandle); - if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::getLinkStatus status:$statusCode url:$url<br/>\n"; - return $statusCode; - } -} diff --git a/system/extensions/core.php b/system/extensions/core.php @@ -1,71 +1,54 @@ <?php -// Core extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/core -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. +// Core extension, https://github.com/annaesvensson/yellow-core class YellowCore { - const VERSION = "0.8.13"; - const TYPE = "feature"; - public $page; //current page - public $content; //content files from file system - public $media; //media files from file system - public $system; //system settings - public $text; //text settings - public $lookup; //location and file lookup - public $toolbox; //toolbox with helpers - public $extensions; //features and themes + const VERSION = "0.8.125"; + const RELEASE = "0.8.23"; + public $content; // content files + public $media; // media files + public $system; // system settings + public $language; // language settings + public $user; // user settings + public $extension; // extensions + public $lookup; // lookup and normalisation methods + public $toolbox; // toolbox with helper methods + public $page; // current page public function __construct() { - $this->checkRequirements(); - $this->page = new YellowPage($this); $this->content = new YellowContent($this); $this->media = new YellowMedia($this); $this->system = new YellowSystem($this); - $this->text = new YellowText($this); + $this->language = new YellowLanguage($this); + $this->user = new YellowUser($this); + $this->extension = new YellowExtension($this); $this->lookup = new YellowLookup($this); - $this->toolbox = new YellowToolbox(); - $this->extensions = new YellowExtensions($this); - $this->system->setDefault("sitename", "Yellow"); - $this->system->setDefault("author", "Yellow"); + $this->toolbox = new YellowToolbox($this); + $this->page = new YellowPage($this); + $this->checkRequirements(); + $this->system->setDefault("sitename", "Localhost"); + $this->system->setDefault("author", "Datenstrom"); $this->system->setDefault("email", "webmaster"); $this->system->setDefault("language", "en"); $this->system->setDefault("layout", "default"); $this->system->setDefault("theme", "default"); $this->system->setDefault("parser", "markdown"); $this->system->setDefault("status", "public"); - $this->system->setDefault("coreStaticUrl", ""); - $this->system->setDefault("coreStaticDefaultFile", "index.html"); - $this->system->setDefault("coreStaticErrorFile", "404.html"); - $this->system->setDefault("coreStaticDirectory", "public/"); - $this->system->setDefault("coreCacheDirectory", "cache/"); - $this->system->setDefault("coreTrashDirectory", "system/trash/"); $this->system->setDefault("coreServerUrl", "auto"); - $this->system->setDefault("coreServerTimezone", "UTC"); - $this->system->setDefault("coreMultiLanguageMode", "0"); + $this->system->setDefault("coreTimezone", "UTC"); + $this->system->setDefault("coreContentExtension", ".md"); + $this->system->setDefault("coreContentDefaultFile", "page.md"); + $this->system->setDefault("coreContentErrorFile", "page-error-(.*).md"); + $this->system->setDefault("coreLanguageFile", "yellow-language.ini"); + $this->system->setDefault("coreUserFile", "yellow-user.ini"); + $this->system->setDefault("coreWebsiteFile", "yellow-website.log"); $this->system->setDefault("coreMediaLocation", "/media/"); $this->system->setDefault("coreDownloadLocation", "/media/downloads/"); $this->system->setDefault("coreImageLocation", "/media/images/"); + $this->system->setDefault("coreThumbnailLocation", "/media/thumbnails/"); $this->system->setDefault("coreExtensionLocation", "/media/extensions/"); - $this->system->setDefault("coreResourceLocation", "/media/resources/"); - $this->system->setDefault("coreMediaDirectory", "media/"); - $this->system->setDefault("coreDownloadDirectory", "media/downloads/"); - $this->system->setDefault("coreImageDirectory", "media/images/"); - $this->system->setDefault("coreSystemDirectory", "system/"); - $this->system->setDefault("coreExtensionDirectory", "system/extensions/"); - $this->system->setDefault("coreLayoutDirectory", "system/layouts/"); - $this->system->setDefault("coreResourceDirectory", "system/resources/"); - $this->system->setDefault("coreSettingDirectory", "system/settings/"); - $this->system->setDefault("coreContentDirectory", "content/"); - $this->system->setDefault("coreContentRootDirectory", "default/"); - $this->system->setDefault("coreContentHomeDirectory", "home/"); - $this->system->setDefault("coreContentSharedDirectory", "shared/"); - $this->system->setDefault("coreContentDefaultFile", "page.md"); - $this->system->setDefault("coreContentErrorFile", "page-error-(.*).md"); - $this->system->setDefault("coreContentExtension", ".md"); - $this->system->setDefault("coreDownloadExtension", ".download"); - $this->system->setDefault("coreSystemFile", "system.ini"); - $this->system->setDefault("coreTextFile", "text.ini"); - $this->system->setDefault("coreLogFile", "yellow.log"); + $this->system->setDefault("coreThemeLocation", "/media/themes/"); + $this->system->setDefault("coreMultiLanguageMode", "0"); + $this->system->setDefault("coreDebugMode", "0"); } public function __destruct() { @@ -74,42 +57,53 @@ class YellowCore { // Check requirements public function checkRequirements() { - $troubleshooting = PHP_SAPI!="cli" ? "<a href=\"https://datenstrom.se/yellow/help/troubleshooting\">See troubleshooting</a>." : ""; - version_compare(PHP_VERSION, "5.6", ">=") || die("Datenstrom Yellow requires PHP 5.6 or higher! $troubleshooting\n"); - extension_loaded("curl") || die("Datenstrom Yellow requires PHP curl extension! $troubleshooting\n"); - extension_loaded("gd") || die("Datenstrom Yellow requires PHP gd extension! $troubleshooting\n"); - extension_loaded("exif") || die("Datenstrom Yellow requires PHP exif extension! $troubleshooting\n"); - extension_loaded("mbstring") || die("Datenstrom Yellow requires PHP mbstring extension! $troubleshooting\n"); - extension_loaded("zip") || die("Datenstrom Yellow requires PHP zip extension! $troubleshooting\n"); + if (!version_compare(PHP_VERSION, "7.0", ">=")) $this->exitFatalError("Datenstrom Yellow requires PHP 7.0 or higher!"); + if (!extension_loaded("curl")) $this->exitFatalError("Datenstrom Yellow requires PHP curl extension!"); + if (!extension_loaded("gd")) $this->exitFatalError("Datenstrom Yellow requires PHP gd extension!"); + if (!extension_loaded("mbstring")) $this->exitFatalError("Datenstrom Yellow requires PHP mbstring extension!"); + if (!extension_loaded("zip")) $this->exitFatalError("Datenstrom Yellow requires PHP zip extension!"); mb_internal_encoding("UTF-8"); - if (defined("DEBUG") && DEBUG>=1) { - ini_set("display_errors", 1); - error_reporting(E_ALL); - } - error_reporting(E_ALL ^ E_NOTICE); //TODO: remove later, for backwards compatibility } // Handle initialisation public function load() { - $this->system->load($this->system->get("coreSettingDirectory").$this->system->get("coreSystemFile")); - $this->extensions->load($this->system->get("coreExtensionDirectory")); - $this->text->load($this->system->get("coreExtensionDirectory")); - $this->text->load($this->system->get("coreSettingDirectory"), $this->system->get("coreTextFile"), $this->system->get("language")); - $this->lookup->detectFileSystem(); + $this->system->load("system/extensions/yellow-system.ini"); + $this->system->set("coreSystemFile", "yellow-system.ini"); + $this->system->set("coreContentDirectory", "content/"); + $this->system->set("coreMediaDirectory", $this->lookup->findMediaDirectory("coreMediaLocation")); + $this->system->set("coreSystemDirectory", "system/"); + $this->system->set("coreCacheDirectory", "system/cache/"); + $this->system->set("coreExtensionDirectory", "system/extensions/"); + $this->system->set("coreLayoutDirectory", "system/layouts/"); + $this->system->set("coreThemeDirectory", "system/themes/"); + $this->system->set("coreTrashDirectory", "system/trash/"); + list($pathInstall, $pathRoot, $pathHome) = $this->lookup->findFileSystemInformation(); + $this->system->set("coreServerInstallDirectory", $pathInstall); + $this->system->set("coreServerRootDirectory", $pathRoot); + $this->system->set("coreServerHomeDirectory", $pathHome); + register_shutdown_function(array($this, "processFatalError")); + if ($this->system->get("coreDebugMode")>=1) { + ini_set("display_errors", 1); + error_reporting(E_ALL); + } + date_default_timezone_set($this->system->get("coreTimezone")); + $this->extension->load($this->system->get("coreExtensionDirectory")); + $this->language->load($this->system->get("coreExtensionDirectory").$this->system->get("coreLanguageFile")); + $this->user->load($this->system->get("coreExtensionDirectory").$this->system->get("coreUserFile")); $this->startup(); } - // Handle request + // Handle request from web browser public function request() { $statusCode = 0; $this->toolbox->timerStart($time); ob_start(); - list($scheme, $address, $base, $location, $fileName) = $this->getRequestInformation(); - $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName); - foreach ($this->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onRequest")) { + list($scheme, $address, $base, $location, $fileName) = $this->lookup->getRequestInformation(); + $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, true); + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onRequest")) { $this->lookup->requestHandler = $key; - $statusCode = $value["obj"]->onRequest($scheme, $address, $base, $location, $fileName); + $statusCode = $value["object"]->onRequest($scheme, $address, $base, $location, $fileName); if ($statusCode!=0) break; } } @@ -117,10 +111,10 @@ class YellowCore { $this->lookup->requestHandler = "core"; $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName, true); } - if ($this->page->isExisting("pageError")) $statusCode = $this->processRequestError(); + if ($this->page->isError()) $statusCode = $this->processRequestError(); ob_end_flush(); $this->toolbox->timerStop($time); - if (defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName)) { + if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) { echo "YellowCore::request status:$statusCode time:$time ms<br/>\n"; } return $statusCode; @@ -143,16 +137,14 @@ class YellowCore { } } if ($statusCode==0) { - $fileName = $this->lookup->findFileFromCache($location, $fileName, $cacheable && !$this->isCommandLine()); - if ($this->lookup->isContentFile($fileName) || !is_readable($fileName)) { - $fileName = $this->readPage($scheme, $address, $base, $location, $fileName, $cacheable, - max(is_readable($fileName) ? 200 : 404, $this->page->statusCode), $this->page->get("pageError")); - $statusCode = $this->sendPage(); - } else { - $statusCode = $this->sendFile(200, $fileName, true); + if ($this->lookup->isContentFile($fileName)) { + $statusCode = $this->sendPage($scheme, $address, $base, $location, $fileName, $cacheable, true); + } elseif (!is_string_empty($fileName)) { + $statusCode = $this->sendFile(200, $fileName, $cacheable); } + if (!is_readable($fileName)) $this->page->error(404); } - if (defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName)) { + if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) { echo "YellowCore::processRequest file:$fileName<br/>\n"; } return $statusCode; @@ -161,62 +153,72 @@ class YellowCore { // Process request with error public function processRequestError() { ob_clean(); - $fileName = $this->readPage($this->page->scheme, $this->page->address, $this->page->base, - $this->page->location, $this->page->fileName, $this->page->cacheable, $this->page->statusCode, - $this->page->get("pageError")); - $statusCode = $this->sendPage(); - if (defined("DEBUG") && DEBUG>=1) echo "YellowCore::processRequestError file:$fileName<br/>\n"; + $statusCode = $this->sendPage($this->page->scheme, $this->page->address, $this->page->base, + $this->page->location, $this->page->fileName, false, false); + if ($this->system->get("coreDebugMode")>=1) echo "YellowCore::processRequestError file:".$this->page->fileName."<br/>\n"; return $statusCode; } - // Read page - public function readPage($scheme, $address, $base, $location, $fileName, $cacheable, $statusCode, $pageError) { - if ($statusCode>=400) { - $locationError = $this->content->getHomeLocation($this->page->location).$this->system->get("coreContentSharedDirectory"); - $fileNameError = $this->lookup->findFileFromLocation($locationError, true).$this->system->get("coreContentErrorFile"); - $fileNameError = strreplaceu("(.*)", $statusCode, $fileNameError); - if (is_file($fileNameError)) { - $rawData = $this->toolbox->readFile($fileNameError); - } else { - $language = $this->lookup->findLanguageFromFile($fileName, $this->system->get("language")); - $rawData = "---\nTitle: ".$this->text->getText("coreError${statusCode}Title", $language)."\n"; - $rawData .= "Layout: error\n---\n".$this->text->getText("coreError${statusCode}Text", $language); - } - $cacheable = false; - } else { - $rawData = $this->toolbox->readFile($fileName); + // Process fatal runtime error + public function processFatalError() { + $error = error_get_last(); + if (!is_null($error) && isset($error["type"]) && ($error["type"]==E_ERROR || $error["type"]==E_PARSE)) { + $fileNameAbsolute = isset($error["file"]) ? $error["file"] : ""; + $fileName = substru($fileNameAbsolute, strlenu($this->system->get("coreServerInstallDirectory"))); + $this->toolbox->log("error", "Can't parse file '$fileName'!"); + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500)); + $troubleshooting = PHP_SAPI!="cli" ? + "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl(); + echo "<br/>\nDatenstrom Yellow stopped with fatal error. Activate the debug mode for more information. $troubleshooting\n"; } - $this->page = new YellowPage($this); - $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $this->page->parseData($rawData, $cacheable, $statusCode, $pageError); - $this->text->setLanguage($this->page->get("language")); - $this->page->parseContent(); - return $fileName; + } + + // Show error message and terminate immediately + public function exitFatalError($errorMessage = "") { + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500)); + $troubleshooting = PHP_SAPI!="cli" ? + "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl(); + echo "$errorMessage $troubleshooting\n"; + exit(1); } // Send page response - public function sendPage() { + public function sendPage($scheme, $address, $base, $location, $fileName, $cacheable, $showSource) { + $rawData = $showSource ? $this->toolbox->readFile($fileName) : $this->page->getRawDataError(); + $statusCode = max($this->page->statusCode, 200); + $errorMessage = $this->page->errorMessage; + $this->page = new YellowPage($this); + $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable); + $this->page->parseMeta($rawData, $statusCode, $errorMessage); + $this->language->set($this->page->get("language")); + $this->page->parseContent(); $this->page->parsePage(); - $statusCode = $this->page->statusCode; - $lastModifiedFormatted = $this->page->getHeader("Last-Modified"); - if ($statusCode==200 && $this->page->isCacheable() && $this->toolbox->isNotModified($lastModifiedFormatted)) { - $statusCode = 304; - @header($this->toolbox->getHttpStatusFormatted($statusCode)); - } else { - @header($this->toolbox->getHttpStatusFormatted($statusCode)); - foreach ($this->page->headerData as $key=>$value) { - @header("$key: $value"); - } - if (!is_null($this->page->outputData)) echo $this->page->outputData; - } - if (defined("DEBUG") && DEBUG>=1) { + $statusCode = $this->sendData($this->page->statusCode, $this->page->headerData, $this->page->outputData); + if ($this->system->get("coreDebugMode")>=1) { foreach ($this->page->headerData as $key=>$value) { echo "YellowCore::sendPage $key: $value<br/>\n"; } + $language = $this->page->get("language"); $layout = $this->page->get("layout"); $theme = $this->page->get("theme"); $parser = $this->page->get("parser"); - echo "YellowCore::sendPage layout:$layout theme:$theme parser:$parser<br/>\n"; + echo "YellowCore::sendPage language:$language layout:$layout theme:$theme parser:$parser<br/>\n"; + } + return $statusCode; + } + + // Send data response + public function sendData($statusCode, $headerData, $outputData) { + $lastModifiedFormatted = isset($headerData["Last-Modified"]) ? $headerData["Last-Modified"] : ""; + if ($statusCode==200 && !isset($headerData["Cache-Control"]) && $this->toolbox->isNotModified($lastModifiedFormatted)) { + $statusCode = 304; + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); + } else { + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); + foreach ($headerData as $key=>$value) { + $this->toolbox->sendHttpHeader("$key: $value"); + } + if (!is_null($outputData)) echo $outputData; } return $statusCode; } @@ -226,102 +228,91 @@ class YellowCore { $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted($this->toolbox->getFileModified($fileName)); if ($statusCode==200 && $cacheable && $this->toolbox->isNotModified($lastModifiedFormatted)) { $statusCode = 304; - @header($this->toolbox->getHttpStatusFormatted($statusCode)); + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); } else { - @header($this->toolbox->getHttpStatusFormatted($statusCode)); - if (!$cacheable) @header("Cache-Control: no-cache, no-store"); - @header("Content-Type: ".$this->toolbox->getMimeContentType($fileName)); - @header("Last-Modified: ".$lastModifiedFormatted); + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); + if (!$cacheable) $this->toolbox->sendHttpHeader("Cache-Control: no-cache, no-store"); + $this->toolbox->sendHttpHeader("Content-Type: ".$this->toolbox->getMimeContentType($fileName)); + $this->toolbox->sendHttpHeader("Last-Modified: ".$lastModifiedFormatted); echo $this->toolbox->readFile($fileName); } return $statusCode; } - // Send data response - public function sendData($statusCode, $rawData, $fileName, $cacheable) { - @header($this->toolbox->getHttpStatusFormatted($statusCode)); - if (!$cacheable) @header("Cache-Control: no-cache, no-store"); - @header("Content-Type: ".$this->toolbox->getMimeContentType($fileName)); - @header("Last-Modified: ".$this->toolbox->getHttpDateFormatted(time())); - echo $rawData; - return $statusCode; - } - // Send status response public function sendStatus($statusCode, $location = "") { - if (!empty($location)) $this->page->clean($statusCode, $location); - @header($this->toolbox->getHttpStatusFormatted($statusCode)); + if (!is_string_empty($location)) $this->page->status($statusCode, $location); + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); foreach ($this->page->headerData as $key=>$value) { - @header("$key: $value"); - } - if (defined("DEBUG") && DEBUG>=1) { - foreach ($this->page->headerData as $key=>$value) { - echo "YellowCore::sendStatus $key: $value<br/>\n"; - } + $this->toolbox->sendHttpHeader("$key: $value"); } return $statusCode; } - // Handle command + // Handle command from command line public function command($line = "") { $statusCode = 0; $this->toolbox->timerStart($time); - list($command, $text) = $this->getCommandInformation($line); - foreach ($this->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onCommand")) { + list($command, $text) = $this->lookup->getCommandInformation($line); + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onCommand")) { $this->lookup->commandHandler = $key; - $statusCode = $value["obj"]->onCommand($command, $text); + $statusCode = $value["object"]->onCommand($command, $text); if ($statusCode!=0) break; } } + if ($statusCode==0 && is_string_empty($command)) { + $lines = array(); + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onCommandHelp")) { + $this->lookup->commandHandler = $key; + $output = $value["object"]->onCommandHelp(); + $lines = array_merge($lines, is_array($output) ? $output : array($output)); + } + } + usort($lines, "strnatcasecmp"); + $this->showCommandHelp($lines); + $statusCode = 200; + } if ($statusCode==0) { $this->lookup->commandHandler = "core"; $statusCode = 400; echo "Yellow $command: Command not found\n"; } $this->toolbox->timerStop($time); - if (defined("DEBUG") && DEBUG>=1) { + if ($this->system->get("coreDebugMode")>=1) { echo "YellowCore::command status:$statusCode time:$time ms<br/>\n"; } return $statusCode<400 ? 0 : 1; } + // Show command help + public function showCommandHelp($lines) { + echo "Datenstrom Yellow is for people who make small websites. https://datenstrom.se/yellow/\n"; + $lineCounter = 0; + foreach ($lines as $line) { + echo(++$lineCounter>1 ? " " : "Syntax: ")."php yellow.php $line\n"; + } + } + // Handle startup public function startup() { - if ($this->isLoaded()) { - foreach ($this->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onStartup")) $value["obj"]->onStartup(); - } - foreach ($this->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onUpdate")) $value["obj"]->onUpdate("startup"); + if (isset($this->extension->data)) { + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onStartup")) $value["object"]->onStartup(); } } } // Handle shutdown public function shutdown() { - if ($this->isLoaded()) { - foreach ($this->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onShutdown")) $value["obj"]->onShutdown(); + if (isset($this->extension->data)) { + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onShutdown")) $value["object"]->onShutdown(); } } } - // Handle logging - public function log($action, $message) { - $statusCode = 0; - foreach ($this->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onLog")) { - $statusCode = $value["obj"]->onLog($action, $message); - if ($statusCode!=0) break; - } - } - if ($statusCode==0) { - $line = date("Y-m-d H:i:s")." ".trim($action)." ".trim($message)."\n"; - $this->toolbox->appendFile($this->system->get("coreExtensionDirectory").$this->system->get("coreLogFile"), $line); - } - } - // Include layout public function layout($name, $arguments = null) { $this->lookup->layoutArguments = func_get_args(); @@ -332,2926 +323,3515 @@ class YellowCore { public function getLayoutArguments($sizeMin = 9) { return array_pad($this->lookup->layoutArguments, $sizeMin, null); } - - public function getLayoutArgs($sizeMin = 9) { //TODO: remove later, for backwards compatibility - return $this->getLayoutArguments($sizeMin); - } - - // Return request information - public function getRequestInformation($scheme = "", $address = "", $base = "") { - if (empty($scheme) && empty($address) && empty($base)) { - $url = $this->system->get("coreServerUrl"); - if ($url=="auto" || $this->isCommandLine()) $url = $this->toolbox->detectServerUrl(); - list($scheme, $address, $base) = $this->lookup->getUrlInformation($url); - $this->system->set("coreServerScheme", $scheme); - $this->system->set("coreServerAddress", $address); - $this->system->set("coreServerBase", $base); - if (defined("DEBUG") && DEBUG>=3) echo "YellowCore::getRequestInformation $scheme://$address$base<br/>\n"; - } - $location = substru($this->toolbox->detectServerLocation(), strlenu($base)); - if (empty($fileName)) $fileName = $this->lookup->findFileFromSystem($location); - if (empty($fileName)) $fileName = $this->lookup->findFileFromMedia($location); - if (empty($fileName)) $fileName = $this->lookup->findFileFromLocation($location); - return array($scheme, $address, $base, $location, $fileName); - } - - // Return command information - public function getCommandInformation($line = "") { - if (empty($line)) { - $line = $this->toolbox->getTextString(array_slice($this->toolbox->getServer("argv"), 1)); - if (defined("DEBUG") && DEBUG>=3) echo "YellowCore::getCommandInformation $line<br/>\n"; - } - return $this->toolbox->getTextList($line, " ", 2); - } - - // Return request handler - public function getRequestHandler() { - return $this->lookup->requestHandler; - } - - // Return command handler - public function getCommandHandler() { - return $this->lookup->commandHandler; - } - - // Check if running at command line - public function isCommandLine() { - return isset($this->lookup->commandHandler); - } - - // Check if all extensions loaded - public function isLoaded() { - return isset($this->extensions->extensions); - } } - -class YellowPage { - public $yellow; //access to API - public $scheme; //server scheme - public $address; //server address - public $base; //base location - public $location; //page location - public $fileName; //content file name - public $rawData; //raw data of page - public $metaDataOffsetBytes; //meta data offset - public $metaData; //meta data - public $pageCollection; //page collection - public $pageRelations; //page relations - public $headerData; //response header - public $outputData; //response output - public $parser; //content parser - public $parserData; //content data of page - public $available; //page is available? (boolean) - public $visible; //page is visible location? (boolean) - public $active; //page is active location? (boolean) - public $cacheable; //page is cacheable? (boolean) - public $lastModified; //last modification date - public $statusCode; //status code +class YellowContent { + public $yellow; // access to API + public $pages; // scanned pages + public function __construct($yellow) { $this->yellow = $yellow; - $this->metaData = new YellowDataCollection(); - $this->pageCollection = new YellowPageCollection($yellow); - $this->pageRelations = array(); - $this->headerData = array(); - } - - // Set request information - public function setRequestInformation($scheme, $address, $base, $location, $fileName) { - $this->scheme = $scheme; - $this->address = $address; - $this->base = $base; - $this->location = $location; - $this->fileName = $fileName; + $this->pages = array(); } - // Parse page data - public function parseData($rawData, $cacheable, $statusCode, $pageError = "") { - $this->rawData = $rawData; - $this->parser = null; - $this->parserData = ""; - $this->available = $this->yellow->lookup->isAvailableLocation($this->location, $this->fileName); - $this->visible = true; - $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location); - $this->cacheable = $cacheable; - $this->lastModified = 0; - $this->statusCode = $statusCode; - $this->parseMeta($pageError); + // Scan file system on demand + public function scanLocation($location) { + if (!isset($this->pages[$location])) { + $this->pages[$location] = array(); + $scheme = $this->yellow->page->scheme; + $address = $this->yellow->page->address; + $base = $this->yellow->page->base; + if (is_string_empty($location)) { + $rootLocations = $this->yellow->lookup->findContentRootLocations(); + foreach ($rootLocations as $rootLocation=>$rootFileName) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $rootLocation, $rootFileName, false); + $page->parseMeta(""); + array_push($this->pages[$location], $page); + } + } else { + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowContent::scanLocation location:$location<br/>\n"; + $fileNames = $this->yellow->lookup->findChildrenFromContentLocation($location); + foreach ($fileNames as $fileName) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, + $this->yellow->lookup->findContentLocationFromFile($fileName), $fileName, false); + $page->parseMeta($this->yellow->toolbox->readFile($fileName, 4096)); + if (strlenb($page->rawData)<4096) $page->statusCode = 200; + array_push($this->pages[$location], $page); + } + } + } + return $this->pages[$location]; } - - // Parse page data update - public function parseDataUpdate() { - if ($this->statusCode==0) { - $this->rawData = $this->yellow->toolbox->readFile($this->fileName); - $this->statusCode = 200; - $this->parseMeta(); + + // Return page from, null if not found + public function find($location, $absoluteLocation = false) { + $found = false; + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + foreach ($this->scanLocation($this->getParentLocation($location)) as $page) { + if ($page->location==$location) { + if (!$this->yellow->lookup->isRootLocation($page->location)) { + $found = true; + break; + } + } } + return $found ? $page : null; } - // Parse page meta data - public function parseMeta($pageError = "") { - $this->metaData = new YellowDataCollection(); - if (!is_null($this->rawData)) { - $this->set("title", $this->yellow->toolbox->createTextTitle($this->location)); - $this->set("language", $this->yellow->lookup->findLanguageFromFile($this->fileName, $this->yellow->system->get("language"))); - $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); - $this->parseMetaRaw(array("sitename", "author", "layout", "theme", "parser", "status")); - $titleHeader = ($this->location==$this->yellow->content->getHomeLocation($this->location)) ? - $this->get("sitename") : $this->get("title")." - ".$this->get("sitename"); - if (!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title")); - if (!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title")); - if (!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader); - if ($this->get("status")=="unlisted") $this->visible = false; - if ($this->get("status")=="shared") $this->available = false; - $this->set("pageRead", $this->yellow->lookup->normaliseUrl( - $this->yellow->system->get("coreServerScheme"), - $this->yellow->system->get("coreServerAddress"), - $this->yellow->system->get("coreServerBase"), - $this->location)); - $this->set("pageEdit", $this->yellow->lookup->normaliseUrl( - $this->yellow->system->get("coreServerScheme"), - $this->yellow->system->get("coreServerAddress"), - $this->yellow->system->get("coreServerBase"), - rtrim($this->yellow->system->get("editLocation"), "/").$this->location)); - } else { - $this->set("type", $this->yellow->toolbox->getFileType($this->fileName)); - $this->set("group", $this->yellow->toolbox->getFileGroup($this->fileName, $this->yellow->system->get("coreMediaDirectory"))); - $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); - } - if (!empty($pageError)) $this->set("pageError", $pageError); - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onParseMeta")) $value["obj"]->onParseMeta($this); - } + // Return page collection with all pages + public function index($showInvisible = false, $multiLanguage = false, $levelMax = 0) { + $rootLocation = $multiLanguage ? "" : $this->getRootLocation($this->yellow->page->location); + return $this->getChildrenRecursive($rootLocation, $showInvisible, $levelMax); } - // Parse page meta data from raw data - public function parseMetaRaw($defaultKeys) { - foreach ($defaultKeys as $key) { - $value = $this->yellow->system->get($key); - if (!empty($key) && !strempty($value)) $this->set($key, $value); - } - if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+/s", $this->rawData, $parts)) { - $this->metaDataOffsetBytes = strlenb($parts[0]); - foreach (preg_split("/[\r\n]+/", $parts[2]) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!empty($matches[1]) && !strempty($matches[2])) $this->set($matches[1], $matches[2]); + // Return page collection with top-level navigation + public function top($showInvisible = false, $showOnePager = true) { + $rootLocation = $this->getRootLocation($this->yellow->page->location); + $pages = $this->getChildren($rootLocation, $showInvisible); + if (count($pages)==1 && $showOnePager) { + $scheme = $this->yellow->page->scheme; + $address = $this->yellow->page->address; + $base = $this->yellow->page->base; + $one = ($pages->offsetGet(0)->location!=$this->yellow->page->location) ? $pages->offsetGet(0) : $this->yellow->page; + preg_match_all("/<h(\d) id=\"([^\"]+)\">(.*?)<\/h\d>/i", $one->getContentHtml(), $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + if ($match[1]==2) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $one->location."#".$match[2], $one->fileName, false); + $page->parseMeta("---\nTitle: $match[3]\n---\n"); + $pages->append($page); } } - } elseif (preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parts)) { - $this->metaDataOffsetBytes = strlenb($parts[0]); - $this->set("title", $parts[2]); } + return $pages; } - // Parse page content on demand - public function parseContent($sizeMax = 0) { - if (!is_object($this->parser)) { - if ($this->yellow->extensions->isExisting($this->get("parser"))) { - $value = $this->yellow->extensions->extensions[$this->get("parser")]; - if (method_exists($value["obj"], "onParseContentRaw")) { - $this->parser = $value["obj"]; - $this->parserData = $this->getContent(true, $sizeMax); - $this->parserData = preg_replace("/@pageRead/i", $this->get("pageRead"), $this->parserData); - $this->parserData = preg_replace("/@pageEdit/i", $this->get("pageEdit"), $this->parserData); - $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData); - $this->parserData = $this->yellow->toolbox->normaliseData($this->parserData, "html"); - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onParseContentText")) { - $output = $value["obj"]->onParseContentText($this, $this->parserData); - if (!is_null($output)) $this->parserData = $output; - } - } - } - } else { - $this->parserData = $this->getContent(true, $sizeMax); - $this->parserData = preg_replace("/\[yellow error\]/i", $this->get("pageError"), $this->parserData); - } - if (!$this->isExisting("description")) { - $description = $this->yellow->toolbox->createTextDescription($this->parserData, 150); - $this->set("description", !empty($description) ? $description : $this->get("title")); + // Return page collection with path ancestry + public function path($location, $absoluteLocation = false) { + $pages = new YellowPageCollection($this->yellow); + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + if ($page = $this->find($location)) { + $pages->prepend($page); + for (; $parent = $page->getParent(); $page=$parent) { + $pages->prepend($parent); } - if (defined("DEBUG") && DEBUG>=3) echo "YellowPage::parseContent location:".$this->location."<br/>\n"; + $home = $this->find($this->getHomeLocation($page->location)); + if ($home && $home->location!=$page->location) $pages->prepend($home); } + return $pages; } - // Parse page content shortcut - public function parseContentShortcut($name, $text, $type) { - $output = null; - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onParseContentShortcut")) { - $output = $value["obj"]->onParseContentShortcut($this, $name, $text, $type); - if (!is_null($output)) break; + // Return page collection with multiple languages + public function multi($location, $absoluteLocation = false, $showInvisible = false) { + $pages = new YellowPageCollection($this->yellow); + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + $locationEnd = substru($location, strlenu($this->getRootLocation($location)) - 4); + foreach ($this->scanLocation("") as $page) { + if ($content = $this->find(substru($page->location, 4).$locationEnd)) { + if ($content->isAvailable() && ($content->isVisible() || $showInvisible)) { + if (!$this->yellow->lookup->isRootLocation($content->location)) $pages->append($content); + } } } - if (is_null($output)) { - if ($name=="yellow" && $type=="inline") { - $output = "Datenstrom Yellow ".YellowCore::VERSION; - if ($text=="error") $output = $this->get("pageError"); - if ($text=="log") { - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLogFile"); - $fileHandle = @fopen($fileName, "r"); - if ($fileHandle) { - $dataBufferSize = 512; - fseek($fileHandle, max(0, filesize($fileName) - $dataBufferSize)); - $dataBuffer = fread($fileHandle, $dataBufferSize); - if (strlenb($dataBuffer)==$dataBufferSize) { - $dataBuffer = ($pos = strposu($dataBuffer, "\n")) ? substru($dataBuffer, $pos+1) : $dataBuffer; - } - fclose($fileHandle); - } - $output = strreplaceu("\n", "<br />\n", htmlspecialchars($dataBuffer)); - } + return $pages; + } + + // Return page collection that's empty + public function clean() { + return new YellowPageCollection($this->yellow); + } + + // Return languages in multi language mode + public function getLanguages($showInvisible = false) { + $languages = array(); + if ($this->yellow->system->get("coreMultiLanguageMode")) { + foreach ($this->scanLocation("") as $page) { + if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) array_push($languages, $page->get("language")); } } - if (defined("DEBUG") && DEBUG>=3 && !empty($name)) echo "YellowPage::parseContentShortcut name:$name type:$type<br/>\n"; - return $output; + return $languages; } - // Parse page - public function parsePage() { - $this->parsePageLayout($this->get("layout")); - if (!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, no-store"); - if (!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8"); - if (!$this->isHeader("Content-Modified")) $this->setHeader("Content-Modified", $this->getModified(true)); - if (!$this->isHeader("Last-Modified")) $this->setHeader("Last-Modified", $this->getLastModified(true)); - $fileNameTheme = $this->yellow->system->get("coreResourceDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; - if (!is_file($fileNameTheme)) { - $this->error(500, "Theme '".$this->get("theme")."' does not exist!"); - } - if (!is_object($this->parser)) { - $this->error(500, "Parser '".$this->get("parser")."' does not exist!"); - } - if (!$this->yellow->text->isLanguage($this->get("language"))) { - $this->error(500, "Language '".$this->get("language")."' does not exist!"); - } - if ($this->yellow->lookup->isNestedLocation($this->location, $this->fileName, true)) { - $this->error(500, "Folder '".dirname($this->fileName)."' may not contain subfolders!"); - } - if ($this->yellow->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200) { - $location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->location); - $location = $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, "", $location); - $this->clean(301, $location); - } - if ($this->yellow->getRequestHandler()=="core" && !$this->isAvailable() && $this->statusCode==200) { - $this->error(404); - } - if ($this->isExisting("pageClean")) $this->outputData = null; - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onParsePageOutput")) { - $output = $value["obj"]->onParsePageOutput($this, $this->outputData); - if (!is_null($output)) $this->outputData = $output; + // Return child pages + public function getChildren($location, $showInvisible = false) { + $pages = new YellowPageCollection($this->yellow); + foreach ($this->scanLocation($location) as $page) { + if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { + if (!$this->yellow->lookup->isRootLocation($page->location) && is_readable($page->fileName)) $pages->append($page); } } + return $pages; } - // Parse page layout - public function parsePageLayout($name) { - $this->outputData = null; - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onParsePageLayout")) { - $value["obj"]->onParsePageLayout($this, $name); + // Return child pages recursively + public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { + --$levelMax; + $pages = new YellowPageCollection($this->yellow); + foreach ($this->scanLocation($location) as $page) { + if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { + if (!$this->yellow->lookup->isRootLocation($page->location) && is_readable($page->fileName)) $pages->append($page); + } + if (!$this->yellow->lookup->isFileLocation($page->location) && $levelMax!=0) { + $pages->merge($this->getChildrenRecursive($page->location, $showInvisible, $levelMax)); } } - if (is_null($this->outputData)) { - ob_start(); - $this->includeLayout($name); - $this->outputData = ob_get_contents(); - ob_end_clean(); - } - } - - // Include page layout - public function includeLayout($name) { - $fileNameLayoutNormal = $this->yellow->system->get("coreLayoutDirectory").$this->yellow->lookup->normaliseName($name).".html"; - $fileNameLayoutTheme = $this->yellow->system->get("coreLayoutDirectory"). - $this->yellow->lookup->normaliseName($this->get("theme"))."-".$this->yellow->lookup->normaliseName($name).".html"; - if (is_file($fileNameLayoutTheme)) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowPage::includeLayout file:$fileNameLayoutTheme<br>\n"; - $this->setLastModified(filemtime($fileNameLayoutTheme)); - require($fileNameLayoutTheme); - } elseif (is_file($fileNameLayoutNormal)) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowPage::includeLayout file:$fileNameLayoutNormal<br>\n"; - $this->setLastModified(filemtime($fileNameLayoutNormal)); - require($fileNameLayoutNormal); - } else { - $this->error(500, "Layout '$name' does not exist!"); - echo "Layout error<br/>\n"; - } + return $pages; } - // Set page setting - public function set($key, $value) { - $this->metaData[$key] = $value; + // Return shared pages + public function getShared($location) { + $pages = new YellowPageCollection($this->yellow); + $sharedLocation = $this->getHomeLocation($location)."shared/"; + return $pages->merge($this->scanLocation($sharedLocation)); } - // Return page setting - public function get($key) { - return $this->isExisting($key) ? $this->metaData[$key] : ""; + // Return root location + public function getRootLocation($location) { + $rootLocation = "root/"; + if ($this->yellow->system->get("coreMultiLanguageMode")) { + foreach ($this->scanLocation("") as $page) { + $token = substru($page->location, 4); + if ($token!="/" && substru($location, 0, strlenu($token))==$token) { + $rootLocation = "root$token"; + break; + } + } + } + return $rootLocation; } - // Return page setting, HTML encoded - public function getHtml($key) { - return htmlspecialchars($this->get($key)); + // Return home location + public function getHomeLocation($location) { + return substru($this->getRootLocation($location), 4); } - // Return page setting as language specific date format - public function getDate($key, $format = "") { - if (!empty($format)) { - $format = $this->yellow->text->get($format); - } else { - $format = $this->yellow->text->get("coreDateFormatMedium"); + // Return parent location + public function getParentLocation($location) { + $parentLocation = ""; + $token = rtrim(substru($this->getRootLocation($location), 4), "/"); + if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { + if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; } - return $this->yellow->text->getDateFormatted(strtotime($this->get($key)), $format); + if (is_string_empty($parentLocation)) $parentLocation = "root$token/"; + return $parentLocation; } - - // Return page setting as language specific date format, HTML encoded - public function getDateHtml($key, $format = "") { - return htmlspecialchars($this->getDate($key, $format)); + + // Return top-level location + public function getParentTopLocation($location) { + $parentTopLocation = ""; + $token = rtrim(substru($this->getRootLocation($location), 4), "/"); + if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; + if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/"; + return $parentTopLocation; + } +} + +class YellowMedia { + public $yellow; // access to API + public $files; // scanned files + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->files = array(); } - // Return page setting as language specific date format, relative to today - public function getDateRelative($key, $format = "", $daysLimit = 30) { - if (!empty($format)) { - $format = $this->yellow->text->get($format); - } else { - $format = $this->yellow->text->get("coreDateFormatMedium"); + // Scan file system on demand + public function scanLocation($location) { + if (!isset($this->files[$location])) { + $this->files[$location] = array(); + $scheme = $this->yellow->page->scheme; + $address = $this->yellow->page->address; + $base = $this->yellow->system->get("coreServerBase"); + if (is_string_empty($location)) { + $fileNames = array($this->yellow->system->get("coreMediaDirectory")); + } else { + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowMedia::scanLocation location:$location<br/>\n"; + $fileNames = $this->yellow->lookup->findChildrenFromMediaLocation($location); + } + foreach ($fileNames as $fileName) { + $file = new YellowPage($this->yellow); + $file->setRequestInformation($scheme, $address, $base, + $this->yellow->lookup->findMediaLocationFromFile($fileName), $fileName, false); + $file->parseMeta(null); + array_push($this->files[$location], $file); + } } - return $this->yellow->text->getDateRelative(strtotime($this->get($key)), $format, $daysLimit); + return $this->files[$location]; } - // Return page setting as language specific date format, relative to today, HTML encoded - public function getDateRelativeHtml($key, $format = "", $daysLimit = 30) { - return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit)); + // Return page with media file information, null if not found + public function find($location, $absoluteLocation = false) { + $found = false; + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->system->get("coreServerBase"))); + foreach ($this->scanLocation($this->getParentLocation($location)) as $file) { + if ($file->location==$location) { + if ($this->yellow->lookup->isFileLocation($file->location)) { + $found = true; + break; + } + } + } + return $found ? $file : null; } - // Return page setting as custom date format - public function getDateFormatted($key, $format) { - return $this->yellow->text->getDateFormatted(strtotime($this->get($key)), $format); + // Return page collection with all media files + public function index($showInvisible = false, $multiPass = false, $levelMax = 0) { + return $this->getChildrenRecursive("", $showInvisible, $levelMax); } - // Return page setting as custom date format, HTML encoded - public function getDateFormattedHtml($key, $format) { - return htmlspecialchars($this->getDateFormatted($key, $format)); + // Return page collection that's empty + public function clean() { + return new YellowPageCollection($this->yellow); } - // Return page content, HTML encoded or raw format - public function getContent($rawFormat = false, $sizeMax = 0) { - if ($rawFormat) { - $this->parseDataUpdate(); - $text = substrb($this->rawData, $this->metaDataOffsetBytes); - } else { - $this->parseContent($sizeMax); - $text = $this->parserData; + // Return child files + public function getChildren($location, $showInvisible = false) { + $files = new YellowPageCollection($this->yellow); + foreach ($this->scanLocation($location) as $file) { + if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) { + if ($this->yellow->lookup->isFileLocation($file->location)) $files->append($file); + } } - return $sizeMax ? substrb($text, 0, $sizeMax) : $text; - } - - // Return parent page, null if none - public function getParent() { - $parentLocation = $this->yellow->content->getParentLocation($this->location); - return $this->yellow->content->find($parentLocation); + return $files; } - // Return top-level parent page, null if none - public function getParentTop($homeFallback = false) { - $parentTopLocation = $this->yellow->content->getParentTopLocation($this->location); - if (!$this->yellow->content->find($parentTopLocation) && $homeFallback) { - $parentTopLocation = $this->yellow->content->getHomeLocation($this->location); + // Return child files recursively + public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { + --$levelMax; + $files = new YellowPageCollection($this->yellow); + foreach ($this->scanLocation($location) as $file) { + if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) { + if ($this->yellow->lookup->isFileLocation($file->location)) $files->append($file); + } + if (!$this->yellow->lookup->isFileLocation($file->location) && $levelMax!=0) { + $files->merge($this->getChildrenRecursive($file->location, $showInvisible, $levelMax)); + } } - return $this->yellow->content->find($parentTopLocation); - } - - // Return page collection with pages on the same level - public function getSiblings($showInvisible = false) { - $parentLocation = $this->yellow->content->getParentLocation($this->location); - return $this->yellow->content->getChildren($parentLocation, $showInvisible); + return $files; } - // Return page collection with child pages - public function getChildren($showInvisible = false) { - return $this->yellow->content->getChildren($this->location, $showInvisible); + // Return home location + public function getHomeLocation($location) { + return $this->yellow->system->get("coreMediaLocation"); } - // Return page collection with sub pages - public function getChildrenRecursive($showInvisible = false, $levelMax = 0) { - return $this->yellow->content->getChildrenRecursive($this->location, $showInvisible, $levelMax); + // Return parent location + public function getParentLocation($location) { + $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); + if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { + if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; + } + if (is_string_empty($parentLocation)) $parentLocation = ""; + return $parentLocation; } - // Set page collection with additional pages - public function setPages($pages) { - $this->pageCollection = $pages; + // Return top-level location + public function getParentTopLocation($location) { + $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); + if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; + if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/"; + return $parentTopLocation; } +} - // Return page collection with additional pages - public function getPages() { - return $this->pageCollection; - } +class YellowSystem { + public $yellow; // access to API + public $modified; // system modification date + public $settings; // system settings + public $settingsDefaults; // system settings defaults - // Set related page - public function setPage($key, $page) { - $this->pageRelations[$key] = $page; + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->settings = new YellowArray(); + $this->settingsDefaults = new YellowArray(); } - // Return related page - public function getPage($key) { - return isset($this->pageRelations[$key]) ? $this->pageRelations[$key] : $this; + // Load system settings from file + public function load($fileName) { + $this->modified = $this->yellow->toolbox->getFileModified($fileName); + $fileData = $this->yellow->toolbox->readFile($fileName); + $this->settings = $this->yellow->toolbox->getTextSettings($fileData, ""); + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowSystem::load file:$fileName<br/>\n"; + if ($this->yellow->system->get("coreDebugMode")>=3) { + foreach ($this->settings as $key=>$value) { + echo "YellowSystem::load ".ucfirst($key).":$value<br/>\n"; + } + } } - // Return page URL - public function getUrl() { - return $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, $this->base, $this->location); + // Save system settings to file + public function save($fileName, $settings) { + $this->modified = time(); + $settingsNew = new YellowArray(); + foreach ($settings as $key=>$value) { + if (!is_string_empty($key) && !is_string_empty($value)) { + $this->set($key, $value); + $settingsNew[$key] = $value; + } + } + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileData = $this->yellow->toolbox->setTextSettings($fileData, "", "", $settingsNew); + return $this->yellow->toolbox->createFile($fileName, $fileData); } - // Return page base - public function getBase($multiLanguage = false) { - return $multiLanguage ? rtrim($this->base.$this->yellow->content->getHomeLocation($this->location), "/") : $this->base; + // Set default system setting + public function setDefault($key, $value) { + $this->settingsDefaults[$key] = $value; + } + + // Set default system settings + public function setDefaults($lines) { + foreach ($lines as $line) { + if (preg_match("/^\#/", $line)) continue; + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $this->settingsDefaults[$matches[1]] = $matches[2]; + } + } + } } - // Return page location - public function getLocation($absoluteLocation = false) { - return $absoluteLocation ? $this->base.$this->location : $this->location; + // Set system setting + public function set($key, $value) { + $this->settings[$key] = $value; } - // Set page request argument - public function setRequest($key, $value) { - $_REQUEST[$key] = $value; + // Return system setting + public function get($key) { + if (isset($this->settings[$key])) { + $value = $this->settings[$key]; + } else { + $value = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : ""; + } + return $value; } - // Return page request argument - public function getRequest($key) { - return isset($_REQUEST[$key]) ? $_REQUEST[$key] : ""; + // Return system setting, HTML encoded + public function getHtml($key) { + return htmlspecialchars($this->get($key)); } - // Return page request argument, HTML encoded - public function getRequestHtml($key) { - return htmlspecialchars($this->getRequest($key)); + // Return different value for system setting + public function getDifferent($key) { + $array = array_diff($this->getAvailable($key), array($this->get($key))); + return reset($array); } + + // Return available values for system setting + public function getAvailable($key) { + $values = array(); + $valueDefault = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : ""; + if ($key=="email") { + foreach ($this->yellow->user->settings as $userKey=>$userValue) { + array_push($values, $userKey); + } + } elseif ($key=="language") { + foreach ($this->yellow->language->settings as $languageKey=>$languageValue) { + array_push($values, $languageKey); + } + } elseif ($key=="layout") { + $path = $this->yellow->system->get("coreLayoutDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.html$/", true, false, false) as $entry) { + array_push($values, lcfirst(substru($entry, 0, -5))); + } + } elseif ($key=="theme") { + $path = $this->yellow->system->get("coreThemeDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false, false) as $entry) { + array_push($values, lcfirst(substru($entry, 0, -4))); + } + } + return !is_array_empty($values) ? $values : array($valueDefault); + } + public function getValues($key) { return $this->getAvailable($key); } //TODO: remove later, for backwards compatibility - // Set page response header - public function setHeader($key, $value) { - $this->headerData[$key] = $value; + // Return system settings + public function getSettings($filterStart = "", $filterEnd = "") { + $settings = array(); + if (is_string_empty($filterStart) && is_string_empty($filterEnd)) { + $settings = array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()); + } else { + foreach (array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()) as $key=>$value) { + if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; + if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; + } + } + return $settings; } - // Return page response header - public function getHeader($key) { - return $this->isHeader($key) ? $this->headerData[$key] : ""; + // Return system settings modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; } - // Return page extra data - public function getExtra($name) { - $output = ""; - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onParsePageExtra")) { - $outputExtension = $value["obj"]->onParsePageExtra($this, $name); - if (!is_null($outputExtension)) $output .= $outputExtension; + // Check if system setting exists + public function isExisting($key) { + return isset($this->settings[$key]); + } +} + +class YellowLanguage { + public $yellow; // access to API + public $modified; // language modification date + public $settings; // language settings + public $settingsDefaults; // language settings defaults + public $language; // current language + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->settings = new YellowArray(); + $this->settingsDefaults = new YellowArray(); + $this->language = ""; + } + + // Load language settings from file + public function load($fileName) { + $this->modified = $this->yellow->toolbox->getFileModified($fileName); + $fileData = $this->yellow->toolbox->readFile($fileName); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "language"); + foreach ($settings as $language=>$block) { + if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); + foreach ($block as $key=>$value) { + $this->settings[$language][$key] = $value; } } - if ($name=="header") { - $fileNameTheme = $this->yellow->system->get("coreResourceDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; - if (is_file($fileNameTheme)) { - $locationTheme = $this->yellow->system->get("coreServerBase"). - $this->yellow->system->get("coreResourceLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; - $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"$locationTheme\" />\n"; - } - $fileNameScript = $this->yellow->system->get("coreResourceDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; - if (is_file($fileNameScript)) { - $locationScript = $this->yellow->system->get("coreServerBase"). - $this->yellow->system->get("coreResourceLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; - $output .= "<script type=\"text/javascript\" src=\"$locationScript\"></script>\n"; + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowLanguage::load file:$fileName<br/>\n"; + foreach ($this->settings->getArrayCopy() as $key=>$value) { + if (!isset($this->settings[$key]["languageDescription"])) { + unset($this->settings[$key]); } } - return $output; + $callback = function ($a, $b) { + return strnatcmp($a["languageDescription"], $b["languageDescription"]); + }; + $this->settings->uasort($callback); } - // Set page response output - public function setOutput($output) { - $this->outputData = $output; + // Set current language + public function set($language) { + $this->language = $language; } - // Return page modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - $modified = strtotime($this->get("modified")); - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; + // Set default language setting + public function setDefault($key, $value, $language) { + if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); + $this->settings[$language][$key] = $value; + $this->settingsDefaults[$key] = true; } - // Set last modification date, Unix time - public function setLastModified($modified) { - $this->lastModified = max($this->lastModified, $modified); + // Set default language settings + public function setDefaults($lines) { + $language = ""; + foreach ($lines as $line) { + if (preg_match("/^\#/", $line)) continue; + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])=="language" && !is_string_empty($matches[2])) { + $language = $matches[2]; + if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); + } + if (!is_string_empty($language) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $this->settings[$language][$matches[1]] = $matches[2]; + $this->settingsDefaults[$matches[1]] = true; + } + } + } } - // Return last modification date, Unix time or HTTP format - public function getLastModified($httpFormat = false) { - $lastModified = max($this->lastModified, $this->getModified(), $this->pageCollection->getModified(), - $this->yellow->system->getModified(), $this->yellow->text->getModified(), $this->yellow->extensions->getModified()); - foreach ($this->pageRelations as $page) $lastModified = max($lastModified, $page->getModified()); - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($lastModified) : $lastModified; + // Set language setting + public function setText($key, $value, $language) { + if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); + $this->settings[$language][$key] = $value; } - // Return page status code, number or HTTP format - public function getStatusCode($httpFormat = false) { - $statusCode = $this->statusCode; - if ($httpFormat) { - $statusCode = $this->yellow->toolbox->getHttpStatusFormatted($statusCode); - if ($this->isExisting("pageError")) $statusCode .= ": ".$this->get("pageError"); + // Return language setting + public function getText($key, $language = "") { + if (is_string_empty($language)) $language = $this->language; + return $this->isText($key, $language) ? $this->settings[$language][$key] : "[$key]"; + } + + // Return language setting, HTML encoded + public function getTextHtml($key, $language = "") { + return htmlspecialchars($this->getText($key, $language)); + } + + // Return text as language specific date, convert to one of the standard formats + public function getDateStandard($text, $language = "") { + if (preg_match("/^\d+$/", $text)) { + $output = $text; + } elseif (preg_match("/^\d+\-\d+$/", $text)) { + $format = $this->getText("coreDateFormatShort", $language); + $output = $this->getDateFormatted(strtotime($text), $format, $language); + } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) { + $format = $this->getText("coreDateFormatMedium", $language); + $output = $this->getDateFormatted(strtotime($text), $format, $language); + } else { + $format = $this->getText("coreDateFormatLong", $language); + $output = $this->getDateFormatted(strtotime($text), $format, $language); } - return $statusCode; + return $output; } - // Respond with error page - public function error($statusCode, $pageError = "") { - if (!$this->isExisting("pageError") && $statusCode>0) { - $this->statusCode = $statusCode; - $this->set("pageError", empty($pageError) ? "Layout error!" : $pageError); + // Return Unix time as date, relative to today + public function getDateRelative($timestamp, $format, $daysLimit, $language = "") { + $timeDifference = time() - $timestamp; + $days = abs(intval($timeDifference/86400)); + $key = $timeDifference>=0 ? "coreDatePast" : "coreDateFuture"; + $tokens = preg_split("/\s*,\s*/", $this->getText($key, $language)); + if (count($tokens)>=8) { + if ($days<=$daysLimit || $daysLimit==0) { + if ($days==0) { + $output = $tokens[0]; + } elseif ($days==1) { + $output = $tokens[1]; + } elseif ($days>=2 && $days<=29) { + $output = preg_replace("/@x/i", $days, $tokens[2]); + } elseif ($days>=30 && $days<=59) { + $output = $tokens[3]; + } elseif ($days>=60 && $days<=364) { + $output = preg_replace("/@x/i", intval($days/30), $tokens[4]); + } elseif ($days>=365 && $days<=729) { + $output = $tokens[5]; + } else { + $output = preg_replace("/@x/i", intval($days/365.25), $tokens[6]); + } + } else { + $output = preg_replace("/@x/i", $this->getDateFormatted($timestamp, $format, $language), $tokens[7]); + } + } else { + $output = "[$key]"; } + return $output; } - // Respond with status code, no page content - public function clean($statusCode, $location = "") { - if (!$this->isExisting("pageClean") && $statusCode>0) { - $this->statusCode = $statusCode; - $this->lastModified = 0; - $this->headerData = array(); - if (!empty($location)) { - $this->setHeader("Location", $location); - $this->setHeader("Cache-Control", "no-cache, no-store"); + // Return Unix time as date + public function getDateFormatted($timestamp, $format, $language = "") { + $dateMonthsNominative = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsNominative", $language)); + $dateMonthsGenitive = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsGenitive", $language)); + $dateWeekdays = preg_split("/\s*,\s*/", $this->getText("coreDateWeekdays", $language)); + $monthNominative = $dateMonthsNominative[date("n", $timestamp) - 1]; + $monthGenitive = $dateMonthsGenitive[date("n", $timestamp) - 1]; + $weekday = $dateWeekdays[date("N", $timestamp) - 1]; + $timeZone = $this->yellow->system->get("coreTimezone"); + $timeZoneHelper = new DateTime("now", new DateTimeZone($timeZone)); + $timeZoneOffset = $timeZoneHelper->getOffset(); + $timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600)); + $format = preg_replace("/(?<!\\\)F/", addcslashes($monthNominative, "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)V/", addcslashes($monthGenitive, "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)M/", addcslashes(substru($monthNominative, 0, 3), "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)D/", addcslashes(substru($weekday, 0, 3), "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)l/", addcslashes($weekday, "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)T/", addcslashes($timeZoneAbbreviation, "A..Za..z"), $format); + return date($format, $timestamp); + } + + // Return language settings + public function getSettings($filterStart = "", $filterEnd = "", $language = "") { + $settings = array(); + if (is_string_empty($language)) $language = $this->language; + if (isset($this->settings[$language])) { + if (is_string_empty($filterStart) && is_string_empty($filterEnd)) { + $settings = $this->settings[$language]->getArrayCopy(); + } else { + foreach ($this->settings[$language] as $key=>$value) { + if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; + if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; + } } - $this->set("pageClean", (string)$statusCode); } + return $settings; } - // Check if page is available - public function isAvailable() { - return $this->available; + // Return language settings modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; } - // Check if page is visible - public function isVisible() { - return $this->visible; + // Check if language setting exists + public function isText($key, $language = "") { + if (is_string_empty($language)) $language = $this->language; + return isset($this->settings[$language]) && isset($this->settings[$language][$key]); } - // Check if page is within current HTTP request - public function isActive() { - return $this->active; + // Check if language exists + public function isExisting($language) { + return isset($this->settings[$language]); } +} + +class YellowUser { + public $yellow; // access to API + public $modified; // user modification date + public $settings; // user settings + public $email; // current email - // Check if page is cacheable - public function isCacheable() { - return $this->cacheable; + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->settings = new YellowArray(); + $this->email = ""; } - // Check if page with error - public function isError() { - return $this->statusCode>=400; + // Load user settings from file + public function load($fileName) { + $this->modified = $this->yellow->toolbox->getFileModified($fileName); + $fileData = $this->yellow->toolbox->readFile($fileName); + $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "email"); + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUser::load file:$fileName<br/>\n"; + } + + // Save user settings to file + public function save($fileName, $email, $settings) { + $this->modified = time(); + $settingsNew = new YellowArray(); + $settingsNew["email"] = $email; + foreach ($settings as $key=>$value) { + if (!is_string_empty($key) && !is_string_empty($value)) { + $this->setUser($key, $value, $email); + $settingsNew[$key] = $value; + } + } + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileData = $this->yellow->toolbox->setTextSettings($fileData, "email", $email, $settingsNew); + return $this->yellow->toolbox->createFile($fileName, $fileData); } - // Check if page setting exists - public function isExisting($key) { - return isset($this->metaData[$key]); + // Remove user settings from file + public function remove($fileName, $email) { + $this->modified = time(); + if (isset($this->settings[$email])) unset($this->settings[$email]); + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileData = $this->yellow->toolbox->unsetTextSettings($fileData, "email", $email); + return $this->yellow->toolbox->createFile($fileName, $fileData); } - // Check if request argument exists - public function isRequest($key) { - return isset($_REQUEST[$key]); + // Set current email + public function set($email) { + $this->email = $email; } - // Check if response header exists - public function isHeader($key) { - return isset($this->headerData[$key]); + // Set user setting + public function setUser($key, $value, $email) { + if (!isset($this->settings[$email])) $this->settings[$email] = new YellowArray(); + $this->settings[$email][$key] = $value; } - // Check if related page exists - public function isPage($key) { - return isset($this->pageRelations[$key]); + // Return user setting + public function getUser($key, $email = "") { + if (is_string_empty($email)) $email = $this->email; + return isset($this->settings[$email]) && isset($this->settings[$email][$key]) ? $this->settings[$email][$key] : ""; } -} -class YellowDataCollection extends ArrayObject { - public function __construct() { - parent::__construct(array()); + // Return user setting, HTML encoded + public function getUserHtml($key, $email = "") { + return htmlspecialchars($this->getUser($key, $email)); } - - // Return array element - public function offsetGet($key) { - if (is_string($key)) $key = lcfirst($key); - return parent::offsetGet($key); + + // Return user settings + public function getSettings($email = "") { + $settings = array(); + if (is_string_empty($email)) $email = $this->email; + if (isset($this->settings[$email])) $settings = $this->settings[$email]->getArrayCopy(); + return $settings; } - // Set array element - public function offsetSet($key, $value) { - if (is_string($key)) $key = lcfirst($key); - parent::offsetSet($key, $value); + // Return user settings modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; } - // Remove array element - public function offsetUnset($key) { - if (is_string($key)) $key = lcfirst($key); - parent::offsetUnset($key); + // Check if user setting exists + public function isUser($key, $email = "") { + if (is_string_empty($email)) $email = $this->email; + return isset($this->settings[$email]) && isset($this->settings[$email][$key]); } - // Check if array element exists - public function offsetExists($key) { - if (is_string($key)) $key = lcfirst($key); - return parent::offsetExists($key); + // Check if user exists + public function isExisting($email) { + return isset($this->settings[$email]); } } - -class YellowPageCollection extends ArrayObject { - public $yellow; //access to API - public $filterValue; //current page filter value - public $paginationNumber; //current page number in pagination - public $paginationCount; //highest page number in pagination +class YellowExtension { + public $yellow; // access to API + public $modified; // extension modification date + public $data; // extension data + public function __construct($yellow) { - parent::__construct(array()); $this->yellow = $yellow; + $this->modified = 0; + $this->data = array(); } - // Filter page collection by setting - public function filter($key, $value, $exactMatch = true) { - $array = array(); - $value = strreplaceu(" ", "-", strtoloweru($value)); - $valueLength = strlenu($value); - $this->filterValue = ""; - foreach ($this->getArrayCopy() as $page) { - if ($page->isExisting($key)) { - foreach (preg_split("/\s*,\s*/", $page->get($key)) as $pageValue) { - $pageValueLength = $exactMatch ? strlenu($pageValue) : $valueLength; - if ($value==substru(strreplaceu(" ", "-", strtoloweru($pageValue)), 0, $pageValueLength)) { - if (empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength); - array_push($array, $page); - break; - } - } - } + // Load extensions + public function load($path) { + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) { + $this->modified = max($this->modified, filemtime($entry)); + require_once($entry); + $name = $this->yellow->lookup->normaliseName(basename($entry), true, true); + $this->register(lcfirst($name), "Yellow".ucfirst($name)); + if ($this->yellow->system->get("coreDebugMode")>=3) echo "YellowExtension::load file:$entry<br/>\n"; } - $this->exchangeArray($array); - return $this; - } - - // Filter page collection by file name - public function match($regex = "/.*/") { - $array = array(); - foreach ($this->getArrayCopy() as $page) { - if (preg_match($regex, $page->fileName)) array_push($array, $page); - } - $this->exchangeArray($array); - return $this; - } - - // Sort page collection by setting - public function sort($key, $ascendingOrder = true) { - $array = $this->getArrayCopy(); - $sortIndex = 0; - foreach ($array as $page) { - $page->set("sortindex", ++$sortIndex); - } - $callback = function ($a, $b) use ($key, $ascendingOrder) { - $result = $ascendingOrder ? - strnatcasecmp($a->get($key), $b->get($key)) : - strnatcasecmp($b->get($key), $a->get($key)); - return $result==0 ? $a->get("sortindex") - $b->get("sortindex") : $result; + $callback = function ($a, $b) { + return $a["priority"] - $b["priority"]; }; - usort($array, $callback); - $this->exchangeArray($array); - return $this; + uasort($this->data, $callback); + foreach ($this->data as $key=>$value) { + if (method_exists($this->data[$key]["object"], "onLoad")) $this->data[$key]["object"]->onLoad($this->yellow); + } } - // Sort page collection by settings similarity - public function similar($page, $ascendingOrder = false) { - $location = $page->location; - $keywords = strtoloweru($page->get("title").",".$page->get("tag").",".$page->get("author")); - $tokens = array_unique(array_filter(preg_split("/[,\s\(\)\+\-]/", $keywords), "strlen")); - if (!empty($tokens)) { - $array = array(); - foreach ($this->getArrayCopy() as $page) { - $searchScore = 0; - foreach ($tokens as $token) { - if (stristr($page->get("title"), $token)) $searchScore += 10; - if (stristr($page->get("tag"), $token)) $searchScore += 5; - if (stristr($page->get("author"), $token)) $searchScore += 2; - } - if ($page->location!=$location) { - $page->set("searchscore", $searchScore); - array_push($array, $page); - } - } - $this->exchangeArray($array); - $this->sort("modified", $ascendingOrder)->sort("searchscore", $ascendingOrder); + // Register extension + public function register($key, $class) { + if (!$this->isExisting($key) && class_exists($class)) { + $this->data[$key] = array(); + $this->data[$key]["object"] = $class=="YellowCore" ? new stdClass : new $class; + $this->data[$key]["class"] = $class; + $this->data[$key]["version"] = defined("$class::VERSION") ? $class::VERSION : 0; + $this->data[$key]["priority"] = defined("$class::PRIORITY") ? $class::PRIORITY : count($this->data) + 10; } - return $this; - } - - // Calculate union, merge page collection - public function merge($input) { - $this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input)); - return $this; } - // Calculate intersection, remove pages that are not present in another page collection - public function intersect($input) { - $callback = function ($a, $b) { - return strcmp(spl_object_hash($a), spl_object_hash($b)); - }; - $this->exchangeArray(array_uintersect($this->getArrayCopy(), (array)$input, $callback)); - return $this; - } - - // Calculate difference, remove pages that are present in another page collection - public function diff($input) { - $callback = function ($a, $b) { - return strcmp(spl_object_hash($a), spl_object_hash($b)); - }; - $this->exchangeArray(array_udiff($this->getArrayCopy(), (array)$input, $callback)); - return $this; + // Return extension + public function get($key) { + return $this->data[$key]["object"]; } - // Append to end of page collection - public function append($page) { - parent::append($page); - return $this; + // Return extensions modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; } - // Prepend to start of page collection - public function prepend($page) { - $array = $this->getArrayCopy(); - array_unshift($array, $page); - $this->exchangeArray($array); - return $this; + // Check if extension exists + public function isExisting($key) { + return isset($this->data[$key]); } +} + +class YellowLookup { + public $yellow; // access to API + public $requestHandler; // request handler name + public $commandHandler; // command handler name + public $layoutArguments; // layout arguments - // Limit the number of pages in page collection - public function limit($pagesMax) { - $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax)); - return $this; + public function __construct($yellow) { + $this->yellow = $yellow; } - // Reverse page collection - public function reverse() { - $this->exchangeArray(array_reverse($this->getArrayCopy())); - return $this; + // Return file system information + public function findFileSystemInformation() { + $pathInstall = substru(__DIR__, 0, 1-strlenu($this->yellow->system->get("coreExtensionDirectory"))); + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreMultiLanguageMode") ? "default/" : ""; + $pathHome = "home/"; + if (!is_string_empty($pathRoot)) { + $firstRoot = ""; + $token = $root = rtrim($pathRoot, "/"); + foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { + if (is_string_empty($firstRoot)) $firstRoot = $token = $entry; + if ($this->normaliseToken($entry)==$root) { + $token = $entry; + break; + } + } + $pathRoot = $this->normaliseToken($token)."/"; + $pathBase .= "$firstRoot/"; + } + if (!is_string_empty($pathHome)) { + $firstHome = ""; + $token = $home = rtrim($pathHome, "/"); + foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { + if (is_string_empty($firstHome)) $firstHome = $token = $entry; + if ($this->normaliseToken($entry)==$home) { + $token = $entry; + break; + } + } + $pathHome = $this->normaliseToken($token)."/"; + } + return array($pathInstall, $pathRoot, $pathHome); } - // Randomize page collection - public function shuffle() { - $array = $this->getArrayCopy(); - shuffle($array); - $this->exchangeArray($array); - return $this; + // Return content language + public function findContentLanguage($fileName, $languageDefault) { + $language = $languageDefault; + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); + if (!is_string_empty($pathRoot)) { + $fileName = substru($fileName, strlenu($pathBase)); + if (preg_match("/^(.+?)\//", $fileName, $matches)) { + $name = $this->normaliseToken($matches[1]); + if (strlenu($name)==2) $language = $name; + } + } + return $language; } - // Paginate page collection - public function pagination($limit, $reverse = true) { - $this->paginationNumber = 1; - $this->paginationCount = ceil($this->count() / $limit); - if ($this->yellow->page->isRequest("page")) $this->paginationNumber = intval($this->yellow->page->getRequest("page")); - if ($this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0; - if ($this->paginationNumber>=1) { - $array = $this->getArrayCopy(); - if ($reverse) $array = array_reverse($array); - $this->exchangeArray(array_slice($array, ($this->paginationNumber - 1) * $limit, $limit)); + // Return content root locations + public function findContentRootLocations() { + $rootLocations = array(); + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); + if (!is_string_empty($pathRoot)) { + foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { + $token = $this->normaliseToken($entry)."/"; + if ($token==$pathRoot) $token = ""; + $rootLocations["root/$token"] = "$pathBase$entry/"; + } + } else { + $rootLocations["root/"] = "$pathBase"; } - return $this; + if ($this->yellow->system->get("coreDebugMode")>=3) { + foreach ($rootLocations as $key=>$key) { + echo "YellowLookup::findContentRootLocations $key -> $value<br/>\n"; + } + } + return $rootLocations; } - // Return current page number in pagination - public function getPaginationNumber() { - return $this->paginationNumber; + // Return content location from file path + public function findContentLocationFromFile($fileName) { + $invalid = false; + $location = "/"; + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); + $pathHome = $this->yellow->system->get("coreServerHomeDirectory"); + $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); + $fileExtension = $this->yellow->system->get("coreContentExtension"); + if (substru($fileName, 0, strlenu($pathBase))==$pathBase && mb_check_encoding($fileName, "UTF-8")) { + $fileName = substru($fileName, strlenu($pathBase)); + $tokens = explode("/", $fileName); + if (!is_string_empty($pathRoot)) { + $token = $this->normaliseToken($tokens[0])."/"; + if ($token!=$pathRoot) $location .= $token; + array_shift($tokens); + } + for ($i=0; $i<count($tokens)-1; ++$i) { + $token = $this->normaliseToken($tokens[$i])."/"; + if ($i || $token!=$pathHome) $location .= $token; + } + $token = $this->normaliseToken($tokens[$i], $fileExtension); + if ($token!=$fileDefault) { + $location .= $this->normaliseToken($tokens[$i], $fileExtension, true); + } + $extension = ($pos = strrposu($fileName, ".")) ? substru($fileName, $pos) : ""; + if ($extension!=$fileExtension) $invalid = true; + } else { + $invalid = true; + } + if ($this->yellow->system->get("coreDebugMode")>=2) { + $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName"; + echo "YellowLookup::findContentLocationFromFile $debug<br/>\n"; + } + return $invalid ? "" : $location; } - // Return highest page number in pagination - public function getPaginationCount() { - return $this->paginationCount; + // Return file path from content location + public function findFileFromContentLocation($location, $directory = false) { + $found = $invalid = false; + $path = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); + $pathHome = $this->yellow->system->get("coreServerHomeDirectory"); + $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); + $fileExtension = $this->yellow->system->get("coreContentExtension"); + $tokens = explode("/", $location); + if ($this->isRootLocation($location)) { + if (!is_string_empty($pathRoot)) { + $token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, "/"); + $path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid); + } + } else { + if (!is_string_empty($pathRoot)) { + if (count($tokens)>2) { + if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathRoot, "/"))) $invalid = true; + $path .= $this->findFileDirectory($path, $tokens[1], "", true, false, $found, $invalid); + if ($found) array_shift($tokens); + } + if (!$found) { + $path .= $this->findFileDirectory($path, rtrim($pathRoot, "/"), "", true, true, $found, $invalid); + } + } + if (count($tokens)>2) { + if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathHome, "/"))) $invalid = true; + for ($i=1; $i<count($tokens)-1; ++$i) { + $path .= $this->findFileDirectory($path, $tokens[$i], "", true, true, $found, $invalid); + } + } else { + $i = 1; + $tokens[0] = rtrim($pathHome, "/"); + $path .= $this->findFileDirectory($path, $tokens[0], "", true, true, $found, $invalid); + } + if (!$directory) { + if (!is_string_empty($tokens[$i])) { + $token = $tokens[$i].$fileExtension; + if ($token==$fileDefault) $invalid = true; + $path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid); + } else { + $path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false); + } + if ($this->yellow->system->get("coreDebugMode")>=2) { + $debug = "$location -> ".($invalid ? "INVALID" : $path); + echo "YellowLookup::findFileFromContentLocation $debug<br/>\n"; + } + } + } + return $invalid ? "" : $path; } - // Return location for a page in pagination - public function getPaginationLocation($absoluteLocation = true, $pageNumber = 1) { - $location = $locationArguments = ""; - if ($pageNumber>=1 && $pageNumber<=$this->paginationCount) { - $location = $this->yellow->page->getLocation($absoluteLocation); - $locationArguments = $this->yellow->toolbox->getLocationArgumentsNew("page", $pageNumber>1 ? "$pageNumber" : ""); + // Return children from content location + public function findChildrenFromContentLocation($location) { + $fileNames = array(); + if (!$this->isFileLocation($location)) { + $path = $this->findFileFromContentLocation($location, true); + $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); + $fileExtension = $this->yellow->system->get("coreContentExtension"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { + $token = $this->findFileDefault($path.$entry, $fileDefault, $fileExtension, false); + array_push($fileNames, $path.$entry."/".$token); + } + if (!$this->isRootLocation($location)) { + $regex = "/^.*\\".$fileExtension."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { + if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue; + array_push($fileNames, $path.$entry); + } + } } - return $location.$locationArguments; + return $fileNames; } - // Return location for previous page in pagination - public function getPaginationPrevious($absoluteLocation = true) { - $pageNumber = $this->paginationNumber-1; - return $this->getPaginationLocation($absoluteLocation, $pageNumber); + // Return media location from file path + public function findMediaLocationFromFile($fileName) { + $location = ""; + $extensionDirectoryLength = strlenu($this->yellow->system->get("coreExtensionDirectory")); + $themeDirectoryLength = strlenu($this->yellow->system->get("coreThemeDirectory")); + $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); + if (substru($fileName, 0, $extensionDirectoryLength)==$this->yellow->system->get("coreExtensionDirectory")) { + if ($this->isSafeFile($fileName)) { + $location = $this->yellow->system->get("coreExtensionLocation").substru($fileName, $extensionDirectoryLength); + } + } elseif (substru($fileName, 0, $themeDirectoryLength)==$this->yellow->system->get("coreThemeDirectory")) { + if ($this->isSafeFile($fileName)) { + $location = $this->yellow->system->get("coreThemeLocation").substru($fileName, $themeDirectoryLength); + } + } elseif (substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory")) { + $location = "/".$fileName; + } + return $location; } - - // Return location for next page in pagination - public function getPaginationNext($absoluteLocation = true) { - $pageNumber = $this->paginationNumber+1; - return $this->getPaginationLocation($absoluteLocation, $pageNumber); + + // Return file path from media location + public function findFileFromMediaLocation($location) { + $fileName = ""; + $extensionLocationLength = strlenu($this->yellow->system->get("coreExtensionLocation")); + $themeLocationLength = strlenu($this->yellow->system->get("coreThemeLocation")); + $mediaLocationLength = strlenu($this->yellow->system->get("coreMediaLocation")); + if (substru($location, 0, $extensionLocationLength)==$this->yellow->system->get("coreExtensionLocation")) { + if ($this->isSafeFile($location)) { + $fileName = $this->yellow->system->get("coreExtensionDirectory").substru($location, $extensionLocationLength); + } + } elseif (substru($location, 0, $themeLocationLength)==$this->yellow->system->get("coreThemeLocation")) { + if ($this->isSafeFile($location)) { + $fileName = $this->yellow->system->get("coreThemeDirectory").substru($location, $themeLocationLength); + } + } elseif (substru($location, 0, $mediaLocationLength)==$this->yellow->system->get("coreMediaLocation")) { + $fileName = substru($location, 1); + } + return $fileName; } - // Return current page number in collection - public function getPageNumber($page) { - $pageNumber = 0; - foreach ($this->getIterator() as $key=>$value) { - if ($page->getLocation()==$value->getLocation()) { - $pageNumber = $key+1; - break; + // Return children from media location + public function findChildrenFromMediaLocation($location) { + $fileNames = array(); + if (!$this->isFileLocation($location)) { + $path = $this->findFileFromMediaLocation($location); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, true) as $entry) { + array_push($fileNames, $entry."/"); + } + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, false, true) as $entry) { + array_push($fileNames, $entry); } } - return $pageNumber; + return $fileNames; } - // Return page in collection, null if none - public function getPage($pageNumber = 1) { - return ($pageNumber>=1 && $pageNumber<=$this->count()) ? $this->offsetGet($pageNumber-1) : null; + // Return media directory from a system setting + public function findMediaDirectory($key) { + return substru($key, -8, 8)=="Location" ? $this->findFileFromMediaLocation($this->yellow->system->get($key)) : ""; } - // Return previous page in collection, null if none - public function getPagePrevious($page) { - $pageNumber = $this->getPageNumber($page)-1; - return $this->getPage($pageNumber); - } - - // Return next page in collection, null if none - public function getPageNext($page) { - $pageNumber = $this->getPageNumber($page)+1; - return $this->getPage($pageNumber); + // Return file or directory that matches token + public function findFileDirectory($path, $token, $fileExtension, $directory, $default, &$found, &$invalid) { + if ($this->normaliseToken($token, $fileExtension)!=$token) $invalid = true; + if (!$invalid) { + $regex = "/^[\d\-\_\.]*".str_replace("-", ".", $token)."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, $directory, false) as $entry) { + if ($this->normaliseToken($entry, $fileExtension)==$token) { + $token = $entry; + $found = true; + break; + } + } + } + if ($directory) $token .= "/"; + return ($default || $found) ? $token : ""; } - // Return current page filter - public function getFilter() { - return $this->filterValue; + // Return default file in directory + public function findFileDefault($path, $fileDefault, $fileExtension, $includePath = true) { + $token = $fileDefault; + if (!is_file($path."/".$fileDefault)) { + $regex = "/^[\d\-\_\.]*($fileDefault)$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { + if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) { + $token = $entry; + break; + } + } + } + return $includePath ? "$path/$token" : $token; } - // Return page collection modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - $modified = 0; - foreach ($this->getIterator() as $page) { - $modified = max($modified, $page->getModified()); - } - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; + // Normalise file/directory token + public function normaliseToken($text, $fileExtension = "", $removeExtension = false) { + if (!is_string_empty($fileExtension)) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; + if (preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_empty($matches[1])) $text = $matches[1]; + return preg_replace("/[^\pL\d\-\_]/u", "-", $text).($removeExtension ? "" : $fileExtension); } - // Check if there is a pagination - public function isPagination() { - return $this->paginationCount>1; + // Normalise name + public function normaliseName($text, $removePrefix = false, $removeExtension = false, $filterStrict = false) { + if ($removeExtension) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; + if ($removePrefix && preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_empty($matches[1])) $text = $matches[1]; + if ($filterStrict) $text = strtoloweru($text); + return preg_replace("/[^\pL\d\-\_]/u", "-", $text); } -} - -class YellowContent { - public $yellow; //access to API - public $pages; //scanned pages - public function __construct($yellow) { - $this->yellow = $yellow; - $this->pages = array(); + // Normalise prefix + public function normalisePrefix($text) { + $prefix = ""; + if (preg_match("/^([\d\-\_\.]*)(.*)$/", $text, $matches)) $prefix = $matches[1]; + if (!is_string_empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-"; + return $prefix; } - // Scan file system on demand - public function scanLocation($location) { - if (!isset($this->pages[$location])) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowContent::scanLocation location:$location<br/>\n"; - $this->pages[$location] = array(); - $scheme = $this->yellow->page->scheme; - $address = $this->yellow->page->address; - $base = $this->yellow->page->base; - if (empty($location)) { - $rootLocations = $this->yellow->lookup->findRootLocations(); - foreach ($rootLocations as $rootLocation) { - list($rootLocation, $fileName) = $this->yellow->toolbox->getTextList($rootLocation, " ", 2); - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $rootLocation, $fileName); - $page->parseData("", false, 0); - array_push($this->pages[$location], $page); + // Normalise elements and attributes in HTML/SVG data + public function normaliseData($text, $type = "html", $filterStrict = true) { + $output = ""; + $elementsHtml = array( + "a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"); + $elementsSvg = array( + "svg", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern"); + $attributesHtml = array( + "accept", "action", "align", "allow", "allowfullscreen", "alt", "autocomplete", "autoplay", "background", "bgcolor", "border", "cellpadding", "cellspacing", "charset", "checked", "cite", "class", "clear", "color", "cols", "colspan", "content", "contenteditable", "controls", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "frameborder", "headers", "height", "hidden", "high", "href", "hreflang", "id", "integrity", "ismap", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "muted", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "poster", "prefix", "preload", "property", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "sandbox", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "width", "xmlns"); + $attributesSvg = array( + "accent-height", "accumulate", "additivive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "datenstrom", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "tabindex", "targetx", "targety", "transform", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xlink:href", "xml:id", "xml:space", "xmlns", "y", "y1", "y2", "z", "zoomandpan"); + $attributesAllowEmptyString = array("alt", "download", "sandbox", "value"); + $elementsSafe = $elementsHtml; + $attributesSafe = $attributesHtml; + if ($type=="svg") { + $elementsSafe = array_merge($elementsHtml, $elementsSvg); + $attributesSafe = array_merge($attributesHtml, $attributesSvg); + } + $offsetBytes = 0; + while (true) { + $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementStart = $elementFound ? $matches[1][0] : ""; + $elementName = $elementFound ? $matches[2][0]: ""; + $elementMiddle = $elementFound ? $matches[3][0]: ""; + $elementEnd = $elementFound ? $matches[4][0]: ""; + $output .= $elementBefore; + if (substrb($elementName, 0, 1)=="!") { + $output .= "<$elementName$elementMiddle>"; + } elseif (in_array(strtolower($elementName), $elementsSafe)) { + $elementAttributes = $this->getTextAttributes($elementMiddle, $attributesAllowEmptyString); + foreach ($elementAttributes as $key=>$value) { + if (!in_array(strtolower($key), $attributesSafe) && !preg_match("/^(aria|data)-/i", $key)) { + unset($elementAttributes[$key]); + } } - } else { - $fileNames = $this->yellow->lookup->findChildrenFromLocation($location); - foreach ($fileNames as $fileName) { - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, - $this->yellow->lookup->findLocationFromFile($fileName), $fileName); - $page->parseData($this->yellow->toolbox->readFile($fileName, 4096), false, 0); - if (strlenb($page->rawData)<4096) $page->statusCode = 200; - array_push($this->pages[$location], $page); + if ($filterStrict) { + $href = isset($elementAttributes["href"]) ? $elementAttributes["href"] : ""; + if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) { + $elementAttributes["href"] = "error-xss-filter"; + } + $href = isset($elementAttributes["xlink:href"]) ? $elementAttributes["xlink:href"] : ""; + if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) { + $elementAttributes["xlink:href"] = "error-xss-filter"; + } } + $output .= "<$elementStart$elementName"; + foreach ($elementAttributes as $key=>$value) $output .= " $key=\"$value\""; + if (!is_string_empty($elementEnd)) $output .= " "; + $output .= "$elementEnd>"; } + if (!$elementFound) break; + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); } - return $this->pages[$location]; + return $output; } - - // Return page from file system, null if not found - public function find($location, $absoluteLocation = false) { - $found = false; - if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); - foreach ($this->scanLocation($this->getParentLocation($location)) as $page) { - if ($page->location==$location) { - if (!$this->yellow->lookup->isRootLocation($page->location)) { - $found = true; - break; + + // Normalise fields in MIME headers + public function normaliseHeaders($input, $type = "mime", $filterStrict = true) { + $output = ""; + if ($type=="mime") { + $keysMixedEncoding = array("To", "From", "Reply-To", "Cc", "Bcc"); + foreach ($input as $key=>$value) { + $key = ucwords(preg_replace("/[^a-zA-Z\-]/u", "-", $key), "-"); + if (in_array($key, $keysMixedEncoding)) { + $text = "$key: "; + foreach (preg_split("/\s*,\s*/", $value) as $email) { + if (!preg_match("/^(.*?)(\s*)<(.*?)>$/", $email, $matches)) { + $matches[1] = $matches[2] = ""; + $matches[3] = $email; + } + if ($filterStrict && !preg_match("/[\w\+\-\.\@]+/", $matches[3])) { + $matches[3] = "error-mail-filter"; + } + if (substru($text, -2, 2)!=": ") $text .= ",\r\n "; + $text = $this->getMimeHeader($text, $matches[1]); + $text = $this->getMimeHeader($text, "$matches[2]<$matches[3]>", false); + } + $text .= "\r\n"; + } else { + $text = $this->getMimeHeader("$key: ", $value)."\r\n"; } + $output .= $text; } } - return $found ? $page : null; - } - - // Return page collection with all pages - public function index($showInvisible = false, $multiLanguage = false, $levelMax = 0) { - $rootLocation = $multiLanguage ? "" : $this->getRootLocation($this->yellow->page->location); - return $this->getChildrenRecursive($rootLocation, $showInvisible, $levelMax); + return $output; } - // Return page collection with top-level navigation - public function top($showInvisible = false, $showOnePager = true) { - $rootLocation = $this->getRootLocation($this->yellow->page->location); - $pages = $this->getChildren($rootLocation, $showInvisible); - if (count($pages)==1 && $showOnePager) { - $scheme = $this->yellow->page->scheme; - $address = $this->yellow->page->address; - $base = $this->yellow->page->base; - $one = ($pages->offsetGet(0)->location!=$this->yellow->page->location) ? $pages->offsetGet(0) : $this->yellow->page; - preg_match_all("/<h(\d) id=\"([^\"]+)\">(.*?)<\/h\d>/i", $one->getContent(), $matches, PREG_SET_ORDER); - foreach ($matches as $match) { - if ($match[1]==2) { - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $one->location."#".$match[2], $one->fileName); - $page->parseData("---\nTitle: $match[3]\n---\n", false, 0); - $pages->append($page); - } + // Normalise relative path tokens + public function normalisePath($text) { + $textFiltered = ""; + $textLength = strlenb($text); + for ($pos=0; $pos<$textLength; ++$pos) { + if ($text[$pos]=="." && ($pos==0 || $text[$pos-1]=="/")) { + while ($text[$pos]==".") ++$pos; + if ($text[$pos]=="/") ++$pos; + --$pos; + continue; } + $textFiltered .= $text[$pos]; } - return $pages; + return $textFiltered; } - // Return page collection with path ancestry - public function path($location, $absoluteLocation = false) { - $pages = new YellowPageCollection($this->yellow); - if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); - if ($page = $this->find($location)) { - $pages->prepend($page); - for (; $parent = $page->getParent(); $page=$parent) { - $pages->prepend($parent); - } - $home = $this->find($this->getHomeLocation($page->location)); - if ($home && $home->location!=$page->location) $pages->prepend($home); + // Normalise text lines, convert line endings + public function normaliseLines($text, $endOfLine = "lf") { + if ($endOfLine=="lf") { + $text = preg_replace("/\R/u", "\n", $text); + } else { + $text = preg_replace("/\R/u", "\r\n", $text); } - return $pages; + return $text; } - // Return page collection with multiple languages - public function multi($location, $absoluteLocation = false, $showInvisible = false) { - $pages = new YellowPageCollection($this->yellow); - if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); - $locationEnd = substru($location, strlenu($this->getRootLocation($location)) - 4); - foreach ($this->scanLocation("") as $page) { - if ($content = $this->find(substru($page->location, 4).$locationEnd)) { - if ($content->isAvailable() && ($content->isVisible() || $showInvisible)) { - if (!$this->yellow->lookup->isRootLocation($content->location)) $pages->append($content); - } - } + // Normalise text into UTF-8 NFC + public function normaliseUnicode($text) { + if (PHP_OS=="Darwin" && !mb_check_encoding($text, "ASCII")) { + $utf8nfc = preg_match("//u", $text) && !preg_match("/[^\\x00-\\x{2FF}]/u", $text); + if (!$utf8nfc) $text = iconv("UTF-8-MAC", "UTF-8", $text); } - return $pages; + return $text; } - // Return page with shared content, null if not found - public function shared($name) { - $location = $this->yellow->lookup->getDirectoryLocation($this->yellow->page->location).$name; - $page = $this->find($location); - if ($page==null) { - $location = $this->getHomeLocation($this->yellow->page->location).$this->yellow->system->get("coreContentSharedDirectory").$name; - $page = $this->find($location); + // Normalise location, make absolute location + public function normaliseLocation($location, $pageLocation, $filterStrict = true) { + if (!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) { + $pageBase = $this->yellow->page->base; + $mediaBase = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreMediaLocation"); + if (!preg_match("/^\#/", $location)) { + if (!preg_match("/^\//", $location)) { + $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; + } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) { + $location = $pageBase.$location; + } + } else { + $location = $pageBase.$pageLocation.$location; + } + $location = str_replace("/./", "/", $location); + $location = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $location); + } else { + if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter"; } - if ($page) $page->setPage("main", $this->yellow->page); - return $page; + return $location; } - // Return page collection that's empty - public function clean() { - return new YellowPageCollection($this->yellow); + // Normalise location arguments + public function normaliseArguments($text, $appendSlash = true, $filterStrict = true) { + if ($appendSlash) $text .= "/"; + if ($filterStrict) $text = str_replace(" ", "-", strtoloweru($text)); + $text = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $text); + return str_replace(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text)); } - // Return languages in multi language mode - public function getLanguages($showInvisible = false) { - $languages = array(); - foreach ($this->scanLocation("") as $page) { - if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) array_push($languages, $page->get("language")); + // Normalise URL, make absolute URL + public function normaliseUrl($scheme, $address, $base, $location, $filterStrict = true) { + if (!preg_match("/^\w+:/", $location)) { + $url = "$scheme://$address$base$location"; + } else { + if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter"; + $url = $location; } - return $languages; + return $url; } - // Return child pages - public function getChildren($location, $showInvisible = false) { - $pages = new YellowPageCollection($this->yellow); - foreach ($this->scanLocation($location) as $page) { - if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { - if (!$this->yellow->lookup->isRootLocation($page->location) && is_readable($page->fileName)) $pages->append($page); - } + // Return URL information + public function getUrlInformation($url) { + $scheme = $address = $base = ""; + if (preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, "/"), $matches)) { + $scheme = $matches[1]; + $address = $matches[2]; + $base = $matches[3]; } - return $pages; + return array($scheme, $address, $base); } - // Return sub pages - public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { - --$levelMax; - $pages = new YellowPageCollection($this->yellow); - foreach ($this->scanLocation($location) as $page) { - if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { - if (!$this->yellow->lookup->isRootLocation($page->location) && is_readable($page->fileName)) $pages->append($page); - } - if (!$this->yellow->lookup->isFileLocation($page->location) && $levelMax!=0) { - $pages->merge($this->getChildrenRecursive($page->location, $showInvisible, $levelMax)); - } - } - return $pages; + // Return request information + public function getRequestInformation($scheme = "", $address = "", $base = "") { + if (is_string_empty($scheme) && is_string_empty($address) && is_string_empty($base)) { + $url = $this->yellow->system->get("coreServerUrl"); + if ($url=="auto" || $this->isCommandLine()) $url = $this->yellow->toolbox->detectServerUrl(); + list($scheme, $address, $base) = $this->getUrlInformation($url); + $this->yellow->system->set("coreServerScheme", $scheme); + $this->yellow->system->set("coreServerAddress", $address); + $this->yellow->system->set("coreServerBase", $base); + if ($this->yellow->system->get("coreDebugMode")>=3) { + echo "YellowLookup::getRequestInformation $scheme://$address$base<br/>\n"; + } + } + $location = substru($this->yellow->toolbox->detectServerLocation(), strlenu($base)); + $fileName = ""; + if (is_string_empty($fileName)) $fileName = $this->findFileFromMediaLocation($location); + if (is_string_empty($fileName)) $fileName = $this->findFileFromContentLocation($location); + return array($scheme, $address, $base, $location, $fileName); } - // Return root location - public function getRootLocation($location) { - $rootLocation = "root/"; - if ($this->yellow->system->get("coreMultiLanguageMode")) { - foreach ($this->scanLocation("") as $page) { - $token = substru($page->location, 4); - if ($token!="/" && substru($location, 0, strlenu($token))==$token) { - $rootLocation = "root$token"; - break; - } + // Return command information + public function getCommandInformation($line = "") { + if (is_string_empty($line)) { + $line = $this->yellow->toolbox->getTextString(array_slice($this->yellow->toolbox->getServer("argv"), 1)); + if ($this->yellow->system->get("coreDebugMode")>=3) { + echo "YellowLookup::getCommandInformation $line<br/>\n"; } } - return $rootLocation; + return $this->yellow->toolbox->getTextList($line, " ", 2); } - // Return home location - public function getHomeLocation($location) { - return substru($this->getRootLocation($location), 4); - } - - // Return parent location - public function getParentLocation($location) { - $token = rtrim(substru($this->getRootLocation($location), 4), "/"); - if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { - if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; - } - if (empty($parentLocation)) $parentLocation = "root$token/"; - return $parentLocation; + // Return request handler + public function getRequestHandler() { + return $this->requestHandler; } - - // Return top-level location - public function getParentTopLocation($location) { - $token = rtrim(substru($this->getRootLocation($location), 4), "/"); - if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; - if (empty($parentTopLocation)) $parentTopLocation = "$token/"; - return $parentTopLocation; + + // Return command handler + public function getCommandHandler() { + return $this->commandHandler; } -} -class YellowMedia { - public $yellow; //access to API - public $files; //scanned files - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->files = array(); - } - - // Scan file system on demand - public function scanLocation($location) { - if (!isset($this->files[$location])) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowMedia::scanLocation location:$location<br/>\n"; - $this->files[$location] = array(); - $scheme = $this->yellow->page->scheme; - $address = $this->yellow->page->address; - $base = $this->yellow->system->get("coreServerBase"); - if (empty($location)) { - $fileNames = array($this->yellow->system->get("coreMediaDirectory")); - } else { - $fileNames = array(); - $path = substru($location, 1); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, true) as $entry) { - array_push($fileNames, $entry."/"); - } - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, false, true) as $entry) { - array_push($fileNames, $entry); + // Return attributes from text + public function getTextAttributes($text, $attributesAllowEmptyString) { + $tokens = array(); + $posStart = $posQuote = 0; + $textLength = strlenb($text); + for ($pos=0; $pos<$textLength; ++$pos) { + if ($text[$pos]==" " && !$posQuote) { + if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + $posStart = $pos+1; + } + if ($text[$pos]=="=" && !$posQuote) { + if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + array_push($tokens, "="); + $posStart = $pos+1; + } + if ($text[$pos]=="\"") { + if ($posQuote) { + if ($pos>$posQuote) array_push($tokens, substrb($text, $posQuote+1, $pos-$posQuote-1)); + $posQuote = 0; + $posStart = $pos+1; + } else { + if ($pos==$posStart) $posQuote = $pos; } } - foreach ($fileNames as $fileName) { - $file = new YellowPage($this->yellow); - $file->setRequestInformation($scheme, $address, $base, "/".$fileName, $fileName); - $file->parseData(null, false, 0); - array_push($this->files[$location], $file); + } + if ($pos>$posStart && !$posQuote) { + array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + } + $attributes = array(); + for ($i=0; $i<count($tokens); ++$i) { + if ($i+2<count($tokens) && $tokens[$i+1]=="=") { + $key = $tokens[$i]; + $value = $tokens[$i+2]; + $i += 2; + } else { + $key = $value = $tokens[$i]; + } + if (!is_string_empty($key) && (!is_string_empty($value) || in_array(strtolower($key), $attributesAllowEmptyString))) { + $attributes[$key] = $value; } } - return $this->files[$location]; + return $attributes; } - // Return page with media file information, null if not found - public function find($location, $absoluteLocation = false) { - $found = false; - if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->system->get("coreServerBase"))); - foreach ($this->scanLocation($this->getParentLocation($location)) as $file) { - if ($file->location==$location) { - if ($this->yellow->lookup->isFileLocation($file->location)) { - $found = true; + // Return MIME header field, encode and fold if necessary + public function getMimeHeader($text, $field, $allowEncode = true) { + if ($allowEncode) { + $encode = preg_match("/[\x7F-\xFF]/", $field); + $fieldPos = 0; + while (true) { + $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0); + $bytesAvailable = max(0, 78-$textPos); + $fragment = substrb($field, $fieldPos); + if ($encode && !is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?="; + if ($bytesAvailable<strlenb($fragment)) { + $bytesHandled = $bytesAvailable; + if (!$encode) { + for ($pos=$bytesHandled;$pos>0;--$pos) { + if ($field[$fieldPos+$pos]==" ") { + $fragment = substrb($field, $fieldPos, $pos); + $bytesHandled = $pos+1; + break; + } + } + if ($pos==0) $encode = true; + } + if ($encode) { + while (true) { + $fragment = substrb($field, $fieldPos, $bytesHandled); + if (!is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?="; + if ($bytesAvailable>=strlenb($fragment) || $bytesHandled==0) break; + --$bytesHandled; + } + } + $text .= $fragment."\r\n "; + $fieldPos += $bytesHandled; + } else { + $text .= $fragment; break; } } + } else { + $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0); + $bytesAvailable = max(0, 78-$textPos); + if ($bytesAvailable<strlenb($field)) { + $text .= "\r\n ".ltrim($field); + } else { + $text .= $field; + } } - return $found ? $file : null; - } - - // Return page collection with all media files - public function index($showInvisible = false, $multiPass = false, $levelMax = 0) { - return $this->getChildrenRecursive("", $showInvisible, $levelMax); + return $text; } - // Return page collection that's empty - public function clean() { - return new YellowPageCollection($this->yellow); + // Return directory location + public function getDirectoryLocation($location) { + return ($pos = strrposu($location, "/")) ? substru($location, 0, $pos+1) : "/"; } - // Return child files - public function getChildren($location, $showInvisible = false) { - $files = new YellowPageCollection($this->yellow); - foreach ($this->scanLocation($location) as $file) { - if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) { - if ($this->yellow->lookup->isFileLocation($file->location)) $files->append($file); - } + // Return redirect location + public function getRedirectLocation($location) { + if ($this->isFileLocation($location)) { + $location = "$location/"; + } else { + $languageDefault = $this->yellow->system->get("language"); + $language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->content->getLanguages(), $languageDefault); + $location = "/$language/"; } - return $files; + return $location; } - // Return sub files - public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { - --$levelMax; - $files = new YellowPageCollection($this->yellow); - foreach ($this->scanLocation($location) as $file) { - if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) { - if ($this->yellow->lookup->isFileLocation($file->location)) $files->append($file); - } - if (!$this->yellow->lookup->isFileLocation($file->location) && $levelMax!=0) { - $files->merge($this->getChildrenRecursive($file->location, $showInvisible, $levelMax)); - } - } - return $files; + // Check if clean URL is requested + public function isRequestCleanUrl($location) { + return isset($_REQUEST["clean-url"]) && substru($location, -1, 1)=="/"; } - // Return home location - public function getHomeLocation($location) { - return $this->yellow->system->get("coreMediaLocation"); - } - - // Return parent location - public function getParentLocation($location) { - $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); - if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { - if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; - } - if (empty($parentLocation)) $parentLocation = ""; - return $parentLocation; + // Check if location is specifying root + public function isRootLocation($location) { + return substru($location, 0, 1)!="/"; } - // Return top-level location - public function getParentTopLocation($location) { - $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); - if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; - if (empty($parentTopLocation)) $parentTopLocation = "$token/"; - return $parentTopLocation; + // Check if location is specifying file or directory + public function isFileLocation($location) { + return substru($location, -1, 1)!="/"; } -} - -class YellowSystem { - public $yellow; //access to API - public $modified; //settings modification date - public $settings; //settings - public $settingsDefaults; //settings defaults - public function __construct($yellow) { - $this->yellow = $yellow; - $this->modified = 0; - $this->settings = new YellowDataCollection(); - $this->settingsDefaults = new YellowDataCollection(); + // Check if location can be redirected into directory + public function isRedirectLocation($location) { + $redirect = false; + if ($this->isFileLocation($location)) { + $redirect = is_dir($this->findFileFromContentLocation("$location/", true)); + } elseif ($location=="/") { + $redirect = $this->yellow->system->get("coreMultiLanguageMode"); + } + return $redirect; } - // Load system settings from file - public function load($fileName) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowSystem::load file:$fileName<br/>\n"; - $this->modified = $this->yellow->toolbox->getFileModified($fileName); - $fileData = $this->yellow->toolbox->readFile($fileName); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\#/", $line)) continue; - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!empty($matches[1]) && !strempty($matches[2])) { - $this->set($matches[1], $matches[2]); - if (defined("DEBUG") && DEBUG>=3) echo "YellowSystem::load $matches[1]:$matches[2]<br/>\n"; - } - } + // Check if location contains nested directories + public function isNestedLocation($location, $fileName, $checkHomeLocation = false) { + $nested = false; + if (!$checkHomeLocation || $location==$this->yellow->content->getHomeLocation($location)) { + $path = dirname($fileName); + if (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true; } + return $nested; } - // Save system settings to file - public function save($fileName, $settings) { - $settingsNew = new YellowDataCollection(); - foreach ($settings as $key=>$value) { - if (!empty($key) && !strempty($value)) { - $this->set($key, $value); - $settingsNew[$key] = $value; - } - } - $this->modified = time(); - $fileData = $this->yellow->toolbox->readFile($fileName); - $fileDataNew = ""; - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!empty($matches[1]) && isset($settingsNew[$matches[1]])) { - $fileDataNew .= "$matches[1]: ".$settingsNew[$matches[1]]."\n"; - unset($settingsNew[$matches[1]]); - continue; - } + // Check if location is within shared directory + public function isSharedLocation($location) { + $sharedLocation = $this->yellow->content->getHomeLocation($location)."shared/"; + return substru($location, 0, strlenu($sharedLocation))==$sharedLocation; + } + + // Check if location is within current HTTP request + public function isActiveLocation($location, $currentLocation) { + if ($this->isFileLocation($location)) { + $active = $currentLocation==$location; + } else { + if ($location==$this->yellow->content->getHomeLocation($location)) { + $active = $this->getDirectoryLocation($currentLocation)==$location; + } else { + $active = substru($currentLocation, 0, strlenu($location))==$location; } - $fileDataNew .= $line; } - foreach ($settingsNew as $key=>$value) { - $fileDataNew .= ucfirst($key).": $value\n"; - } - return $this->yellow->toolbox->createFile($fileName, $fileDataNew); + return $active; } - // Set default system setting - public function setDefault($key, $value) { - $this->settingsDefaults[$key] = $value; + // Check if URL is a well-known URL scheme + public function isSafeUrl($url) { + return preg_match("/^(http|https|ftp|mailto|tel):/", $url); } - // Set system setting - public function set($key, $value) { - $this->settings[$key] = $value; + // Check if file is a well-known file type + public function isSafeFile($fileName) { + return preg_match("/\.(css|gif|ico|js|jpg|map|png|scss|svg|woff|woff2)$/", $fileName); } - // Return system setting - public function get($key) { - if (isset($this->settings[$key])) { - $value = $this->settings[$key]; - } else { - $value = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : ""; - } - return $value; + // Check if file is valid + public function isValidFile($fileName) { + $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); + $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); + $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); + return strposu($fileName, "/")===false || + substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory") || + substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory") || + substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); } - // Return system setting, HTML encoded - public function getHtml($key) { - return htmlspecialchars($this->get($key)); + // Check if content file + public function isContentFile($fileName) { + $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); + return substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory"); } - // Return system settings - public function getData($filterStart = "", $filterEnd = "") { - $settings = array(); - if (empty($filterStart) && empty($filterEnd)) { - $settings = array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()); - } else { - foreach (array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()) as $key=>$value) { - if (!empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; - if (!empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; - } - } - return $settings; + // Check if media file + public function isMediaFile($fileName) { + $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); + return substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory"); } - // Return system settings modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; + // Check if system file + public function isSystemFile($fileName) { + $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); + return substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); } - // Check if system setting exists - public function isExisting($key) { - return isset($this->settings[$key]); + // Check if running at command line + public function isCommandLine() { + return isset($this->commandHandler); } } -class YellowText { - public $yellow; //access to API - public $modified; //text modification date - public $text; //text - public $language; //current language +class YellowToolbox { + public $yellow; // access to API public function __construct($yellow) { $this->yellow = $yellow; - $this->modified = 0; - $this->text = new YellowDataCollection(); - } - - // Load text settings - public function load($path, $fileName = "", $languageDefault = "") { - $regex = empty($fileName) ? "/^.*\.txt$/" : "/^$fileName$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false) as $entry) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowText::load file:$entry<br/>\n"; - $language = $languageDefault; - $this->modified = max($this->modified, filemtime($entry)); - $fileData = $this->yellow->toolbox->readFile($entry); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\#/", $line)) continue; - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])=="language" && !strempty($matches[2])) $language = $matches[2]; - if (!empty($language) && !empty($matches[1]) && !strempty($matches[2])) { - $this->setText($matches[1], $matches[2], $language); - if (defined("DEBUG") && DEBUG>=3) echo "YellowText::load $matches[1]:$matches[2]<br/>\n"; - } - } - } - } - } - - // Set current language - public function setLanguage($language) { - $this->language = $language; - } - - // Set text settings for specific language - public function setText($key, $value, $language) { - if (!isset($this->text[$language])) $this->text[$language] = new YellowDataCollection(); - $this->text[$language][$key] = $value; - } - - // Return text setting - public function get($key) { - return $this->getText($key, $this->language); } - // Return text setting, HTML encoded - public function getHtml($key) { - return htmlspecialchars($this->getText($key, $this->language)); + // Return browser cookie from from current HTTP request + public function getCookie($key) { + return isset($_COOKIE[$key]) ? $_COOKIE[$key] : ""; } - // Return text setting for specific language - public function getText($key, $language) { - return $this->isExisting($key, $language) ? $this->text[$language][$key] : "[$key]"; + // Return server argument from current HTTP request + public function getServer($key) { + return isset($_SERVER[$key]) ? $_SERVER[$key] : ""; } - // Return text setting for specific language, HTML encoded - public function getTextHtml($key, $language) { - return htmlspecialchars($this->getText($key, $language)); + // Return location arguments from current HTTP request + public function getLocationArguments() { + return $this->getServer("LOCATION_ARGUMENTS"); } - // Return text settings - public function getData($filterStart = "", $language = "") { - $text = array(); - if (empty($language)) $language = $this->language; - if ($this->isLanguage($language)) { - if (empty($filterStart)) { - $text = $this->text[$language]; - } else { - foreach ($this->text[$language] as $key=>$value) { - if (substru($key, 0, strlenu($filterStart))==$filterStart) $text[$key] = $value; + // Return location arguments from current HTTP request, modify existing arguments + public function getLocationArgumentsNew($key, $value) { + $locationArguments = ""; + $found = false; + $separator = $this->getLocationArgumentsSeparator(); + foreach (explode("/", $this->getServer("LOCATION_ARGUMENTS")) as $token) { + if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { + if ($matches[1]==$key) { + $matches[2] = $value; + $found = true; + } + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + if (!is_string_empty($locationArguments)) $locationArguments .= "/"; + $locationArguments .= "$matches[1]:$matches[2]"; } } } - return $text; - } - - // Return human readable date, custom date format - public function getDateFormatted($timestamp, $format) { - $dateMonths = preg_split("/\s*,\s*/", $this->get("coreDateMonths")); - $dateWeekdays = preg_split("/\s*,\s*/", $this->get("coreDateWeekdays")); - $month = $dateMonths[date("n", $timestamp) - 1]; - $weekday = $dateWeekdays[date("N", $timestamp) - 1]; - $timeZone = $this->yellow->system->get("coreServerTimezone"); - $timeZoneHelper = new DateTime(null, new DateTimeZone($timeZone)); - $timeZoneOffset = $timeZoneHelper->getOffset(); - $timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600)); - $format = preg_replace("/(?<!\\\)F/", addcslashes($month, "A..Za..z"), $format); - $format = preg_replace("/(?<!\\\)M/", addcslashes(substru($month, 0, 3), "A..Za..z"), $format); - $format = preg_replace("/(?<!\\\)D/", addcslashes(substru($weekday, 0, 3), "A..Za..z"), $format); - $format = preg_replace("/(?<!\\\)l/", addcslashes($weekday, "A..Za..z"), $format); - $format = preg_replace("/(?<!\\\)T/", addcslashes($timeZoneAbbreviation, "A..Za..z"), $format); - return date($format, $timestamp); + if (!$found && !is_string_empty($key) && !is_string_empty($value)) { + if (!is_string_empty($locationArguments)) $locationArguments .= "/"; + $locationArguments .= "$key:$value"; + } + if (!is_string_empty($locationArguments)) { + $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false, false); + if (!$this->isLocationArgumentsPagination($locationArguments)) $locationArguments .= "/"; + } + return $locationArguments; } - // Return human readable date, relative to today - public function getDateRelative($timestamp, $format, $daysLimit) { - $timeDifference = time() - $timestamp; - $days = abs(intval($timeDifference / 86400)); - $key = $timeDifference>=0 ? "coreDatePast" : "coreDateFuture"; - $tokens = preg_split("/\s*,\s*/", $this->get($key)); - if (count($tokens)>=8) { - if ($days<=$daysLimit || $daysLimit==0) { - if ($days==0) { - $output = $tokens[0]; - } elseif ($days==1) { - $output = $tokens[1]; - } elseif ($days>=2 && $days<=29) { - $output = preg_replace("/@x/i", $days, $tokens[2]); - } elseif ($days>=30 && $days<=59) { - $output = $tokens[3]; - } elseif ($days>=60 && $days<=364) { - $output = preg_replace("/@x/i", intval($days/30), $tokens[4]); - } elseif ($days>=365 && $days<=729) { - $output = $tokens[5]; - } else { - $output = preg_replace("/@x/i", intval($days/365.25), $tokens[6]); - } - } else { - $output = preg_replace("/@x/i", $this->getDateFormatted($timestamp, $format), $tokens[7]); + // Return location arguments from current HTTP request, convert form parameters + public function getLocationArgumentsCleanUrl() { + $locationArguments = ""; + foreach (array_merge($_GET, $_POST) as $key=>$value) { + if (!is_string_empty($key) && !is_string_empty($value)) { + if (!is_string_empty($locationArguments)) $locationArguments .= "/"; + $key = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $key); + $value = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $value); + $locationArguments .= "$key:$value"; } - } else { - $output = "[$key]"; } - return $output; - } - - // Return text settings modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; - } - - // Return languages - public function getLanguages() { - $languages = array(); - foreach ($this->text as $key=>$value) { - array_push($languages, $key); + if (!is_string_empty($locationArguments)) { + $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false, false); + if (!$this->isLocationArgumentsPagination($locationArguments)) $locationArguments .= "/"; } - return $languages; + return $locationArguments; } - - // Normalise date into known format - public function normaliseDate($text) { - if (preg_match("/^\d+\-\d+$/", $text)) { - $output = $this->getDateFormatted(strtotime($text), $this->get("coreDateFormatShort")); - } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) { - $output = $this->getDateFormatted(strtotime($text), $this->get("coreDateFormatMedium")); - } elseif (preg_match("/^\d+\-\d+\-\d+ \d+\:\d+$/", $text)) { - $output = $this->getDateFormatted(strtotime($text), $this->get("coreDateFormatLong")); - } else { - $output = $text; - } - return $output; + + // Return location arguments separator + public function getLocationArgumentsSeparator() { + return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ":" : "="; } - // Check if language exists - public function isLanguage($language) { - return isset($this->text[$language]); + // Return human readable HTTP date + public function getHttpDateFormatted($timestamp) { + return gmdate("D, d M Y H:i:s", $timestamp)." GMT"; } - // Check if text setting exists - public function isExisting($key, $language = "") { - if (empty($language)) $language = $this->language; - return isset($this->text[$language]) && isset($this->text[$language][$key]); + // Return human readable HTTP server status + public function getHttpStatusFormatted($statusCode, $shortFormat = false) { + switch ($statusCode) { + case 0: $text = "No data"; break; + case 200: $text = "OK"; break; + case 301: $text = "Moved permanently"; break; + case 302: $text = "Moved temporarily"; break; + case 303: $text = "Reload please"; break; + case 304: $text = "Not modified"; break; + case 400: $text = "Bad request"; break; + case 403: $text = "Forbidden"; break; + case 404: $text = "Not found"; break; + case 420: $text = "Not public"; break; + case 430: $text = "Login failed"; break; + case 434: $text = "Can create"; break; + case 435: $text = "Can restore"; break; + case 450: $text = "Update error"; break; + case 500: $text = "Server error"; break; + case 503: $text = "Service unavailable"; break; + default: $text = "Error $statusCode"; + } + $serverProtocol = $this->getServer("SERVER_PROTOCOL"); + if (!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1"; + return $shortFormat ? $text : "$serverProtocol $statusCode $text"; } -} - -class YellowLookup { - public $yellow; //access to API - public $requestHandler; //request handler name - public $commandHandler; //command handler name - public $layoutArguments; //layout arguments - public function __construct($yellow) { - $this->yellow = $yellow; + // Return MIME content type + public function getMimeContentType($fileName) { + $contentType = ""; + $contentTypes = array( + "css" => "text/css", + "gif" => "image/gif", + "html" => "text/html; charset=utf-8", + "ico" => "image/x-icon", + "js" => "application/javascript", + "json" => "application/json", + "jpg" => "image/jpeg", + "md" => "text/markdown", + "png" => "image/png", + "scss" => "text/x-scss", + "svg" => "image/svg+xml", + "txt" => "text/plain", + "woff" => "application/font-woff", + "woff2" => "application/font-woff2", + "xml" => "text/xml; charset=utf-8"); + $fileType = $this->getFileType($fileName); + if (is_string_empty($fileType)) { + $contentType = $contentTypes["html"]; + } elseif (array_key_exists($fileType, $contentTypes)) { + $contentType = $contentTypes[$fileType]; + } + return $contentType; } - // Detect file system - public function detectFileSystem() { - list($pathRoot, $pathHome) = $this->findFileSystemInformation(); - $this->yellow->system->set("coreContentRootDirectory", $pathRoot); - $this->yellow->system->set("coreContentHomeDirectory", $pathHome); - date_default_timezone_set($this->yellow->system->get("coreServerTimezone")); + // Send HTTP header + public function sendHttpHeader($text) { + if (!headers_sent()) header($text); } - // Return file system information - public function findFileSystemInformation() { - $path = $this->yellow->system->get("coreContentDirectory"); - $pathRoot = $this->yellow->system->get("coreContentRootDirectory"); - $pathHome = $this->yellow->system->get("coreContentHomeDirectory"); - if (!$this->yellow->system->get("coreMultiLanguageMode")) $pathRoot = ""; - if (!empty($pathRoot)) { - $token = $root = rtrim($pathRoot, "/"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { - if (empty($firstRoot)) $firstRoot = $token = $entry; - if ($this->normaliseToken($entry)==$root) { - $token = $entry; - break; - } - } - $pathRoot = $this->normaliseToken($token)."/"; - $path .= "$firstRoot/"; - } - if (!empty($pathHome)) { - $token = $home = rtrim($pathHome, "/"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { - if (empty($firstHome)) $firstHome = $token = $entry; - if ($this->normaliseToken($entry)==$home) { - $token = $entry; - break; + // Return files and directories + public function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true) { + $entries = array(); + $directoryHandle = @opendir($path); + if ($directoryHandle) { + $path = rtrim($path, "/"); + while (($entry = readdir($directoryHandle))!==false) { + if (substru($entry, 0, 1)==".") continue; + $entry = $this->yellow->lookup->normaliseUnicode($entry); + if (preg_match($regex, $entry)) { + if ($directories) { + if (is_dir("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); + } else { + if (is_file("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); + } } } - $pathHome = $this->normaliseToken($token)."/"; + if ($sort) natcasesort($entries); + closedir($directoryHandle); } - return array($pathRoot, $pathHome); + return $entries; } - - // Return root locations - public function findRootLocations($includePath = true) { - $locations = array(); - $pathBase = $this->yellow->system->get("coreContentDirectory"); - $pathRoot = $this->yellow->system->get("coreContentRootDirectory"); - if (!empty($pathRoot)) { - foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { - $token = $this->normaliseToken($entry)."/"; - if ($token==$pathRoot) $token = ""; - array_push($locations, $includePath ? "root/$token $pathBase$entry/" : "root/$token"); - if (defined("DEBUG") && DEBUG>=2) echo "YellowLookup::findRootLocations root/$token<br/>\n"; + + // Return files and directories recursively + public function getDirectoryEntriesRecursive($path, $regex = "/.*/", $sort = true, $directories = true, $levelMax = 0) { + --$levelMax; + $entries = $this->getDirectoryEntries($path, $regex, $sort, $directories); + if ($levelMax!=0) { + foreach ($this->getDirectoryEntries($path, "/.*/", $sort, true) as $entry) { + $entries = array_merge($entries, $this->getDirectoryEntriesRecursive($entry, $regex, $sort, $directories, $levelMax)); } - } else { - array_push($locations, $includePath ? "root/ $pathBase" : "root/"); } - return $locations; + return $entries; } - // Return location from file path - public function findLocationFromFile($fileName) { - $invalid = false; - $location = "/"; - $pathBase = $this->yellow->system->get("coreContentDirectory"); - $pathRoot = $this->yellow->system->get("coreContentRootDirectory"); - $pathHome = $this->yellow->system->get("coreContentHomeDirectory"); - $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); - $fileExtension = $this->yellow->system->get("coreContentExtension"); - if (substru($fileName, 0, strlenu($pathBase))==$pathBase && mb_check_encoding($fileName, "UTF-8")) { - $fileName = substru($fileName, strlenu($pathBase)); - $tokens = explode("/", $fileName); - if (!empty($pathRoot)) { - $token = $this->normaliseToken($tokens[0])."/"; - if ($token!=$pathRoot) $location .= $token; - array_shift($tokens); - } - for ($i=0; $i<count($tokens)-1; ++$i) { - $token = $this->normaliseToken($tokens[$i])."/"; - if ($i || $token!=$pathHome) $location .= $token; - } - $token = $this->normaliseToken($tokens[$i], $fileExtension); - $fileFolder = $this->normaliseToken($tokens[$i-1], $fileExtension); - if ($token!=$fileDefault && $token!=$fileFolder) { - $location .= $this->normaliseToken($tokens[$i], $fileExtension, true); + // Read file, empty string if not found + public function readFile($fileName, $sizeMax = 0) { + $fileData = ""; + $fileHandle = @fopen($fileName, "rb"); + if ($fileHandle) { + clearstatcache(true, $fileName); + if (flock($fileHandle, LOCK_SH)) { + $fileSize = $sizeMax ? $sizeMax : filesize($fileName); + if ($fileSize) $fileData = fread($fileHandle, $fileSize); + flock($fileHandle, LOCK_UN); } - $extension = ($pos = strrposu($fileName, ".")) ? substru($fileName, $pos) : ""; - if ($extension!=$fileExtension) $invalid = true; - } else { - $invalid = true; - } - if (defined("DEBUG") && DEBUG>=2) { - $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName"; - echo "YellowLookup::findLocationFromFile $debug<br/>\n"; + fclose($fileHandle); } - return $invalid ? "" : $location; + return $fileData; } - // Return file path from location - public function findFileFromLocation($location, $directory = false) { - $found = $invalid = false; - $path = $this->yellow->system->get("coreContentDirectory"); - $pathRoot = $this->yellow->system->get("coreContentRootDirectory"); - $pathHome = $this->yellow->system->get("coreContentHomeDirectory"); - $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); - $fileExtension = $this->yellow->system->get("coreContentExtension"); - $tokens = explode("/", $location); - if ($this->isRootLocation($location)) { - if (!empty($pathRoot)) { - $token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, "/"); - $path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid); - } - } else { - if (!empty($pathRoot)) { - if (count($tokens)>2) { - if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathRoot, "/"))) $invalid = true; - $path .= $this->findFileDirectory($path, $tokens[1], "", true, false, $found, $invalid); - if ($found) array_shift($tokens); - } - if (!$found) { - $path .= $this->findFileDirectory($path, rtrim($pathRoot, "/"), "", true, true, $found, $invalid); - } - } - if (count($tokens)>2) { - if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathHome, "/"))) $invalid = true; - for ($i=1; $i<count($tokens)-1; ++$i) { - $path .= $this->findFileDirectory($path, $tokens[$i], "", true, true, $found, $invalid); - } - } else { - $i = 1; - $tokens[0] = rtrim($pathHome, "/"); - $path .= $this->findFileDirectory($path, $tokens[0], "", true, true, $found, $invalid); - } - if (!$directory) { - if (!strempty($tokens[$i])) { - $token = $tokens[$i].$fileExtension; - $fileFolder = $tokens[$i-1].$fileExtension; - if ($token==$fileDefault || $token==$fileFolder) $invalid = true; - $path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid); - } else { - $path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false); - } - if (defined("DEBUG") && DEBUG>=2) { - $debug = "$location -> ".($invalid ? "INVALID" : $path); - echo "YellowLookup::findFileFromLocation $debug<br/>\n"; - } + // Create file + public function createFile($fileName, $fileData, $mkdir = false) { + $ok = false; + if ($mkdir) { + $path = dirname($fileName); + if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + } + $fileHandle = @fopen($fileName, "cb"); + if ($fileHandle) { + clearstatcache(true, $fileName); + if (flock($fileHandle, LOCK_EX)) { + ftruncate($fileHandle, 0); + fwrite($fileHandle, $fileData); + flock($fileHandle, LOCK_UN); } + fclose($fileHandle); + $ok = true; } - return $invalid ? "" : $path; + return $ok; } - // Return file or directory that matches token - public function findFileDirectory($path, $token, $fileExtension, $directory, $default, &$found, &$invalid) { - if ($this->normaliseToken($token, $fileExtension)!=$token) $invalid = true; - if (!$invalid) { - $regex = "/^[\d\-\_\.]*".strreplaceu("-", ".", $token)."$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, $directory, false) as $entry) { - if ($this->normaliseToken($entry, $fileExtension)==$token) { - $token = $entry; - $found = true; - break; - } + // Append file + public function appendFile($fileName, $fileData, $mkdir = false) { + $ok = false; + if ($mkdir) { + $path = dirname($fileName); + if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + } + $fileHandle = @fopen($fileName, "ab"); + if ($fileHandle) { + clearstatcache(true, $fileName); + if (flock($fileHandle, LOCK_EX)) { + fwrite($fileHandle, $fileData); + flock($fileHandle, LOCK_UN); } + fclose($fileHandle); + $ok = true; } - if ($directory) $token .= "/"; - return ($default || $found) ? $token : ""; + return $ok; } - // Return default file in directory - public function findFileDefault($path, $fileDefault, $fileExtension, $includePath = true) { - $token = $fileDefault; - if (!is_file($path."/".$fileDefault)) { - $fileFolder = $this->normaliseToken(basename($path), $fileExtension); - $regex = "/^[\d\-\_\.]*($fileDefault|$fileFolder)$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { - if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) { - $token = $entry; - break; - } - if ($this->normaliseToken($entry, $fileExtension)==$fileFolder) { - $token = $entry; - break; - } - } + // Copy file + public function copyFile($fileNameSource, $fileNameDestination, $mkdir = false) { + clearstatcache(); + if ($mkdir) { + $path = dirname($fileNameDestination); + if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); } - return $includePath ? "$path/$token" : $token; + return @copy($fileNameSource, $fileNameDestination); } - // Return children from location - public function findChildrenFromLocation($location) { - $fileNames = array(); - $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); - $fileExtension = $this->yellow->system->get("coreContentExtension"); - if (!$this->isFileLocation($location)) { - $path = $this->findFileFromLocation($location, true); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { - $token = $this->findFileDefault($path.$entry, $fileDefault, $fileExtension, false); - array_push($fileNames, $path.$entry."/".$token); - } - if (!$this->isRootLocation($location)) { - $fileFolder = $this->normaliseToken(basename($path), $fileExtension); - $regex = "/^.*\\".$fileExtension."$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { - if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue; - if ($this->normaliseToken($entry, $fileExtension)==$fileFolder) continue; - array_push($fileNames, $path.$entry); - } - } + // Rename file + public function renameFile($fileNameSource, $fileNameDestination, $mkdir = false) { + clearstatcache(); + if ($mkdir) { + $path = dirname($fileNameDestination); + if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); } - return $fileNames; + return @rename($fileNameSource, $fileNameDestination); } - - // Return language from file path - public function findLanguageFromFile($fileName, $languageDefault) { - $language = $languageDefault; - $pathBase = $this->yellow->system->get("coreContentDirectory"); - $pathRoot = $this->yellow->system->get("coreContentRootDirectory"); - if (!empty($pathRoot)) { - $fileName = substru($fileName, strlenu($pathBase)); - if (preg_match("/^(.+?)\//", $fileName, $matches)) { - $name = $this->normaliseToken($matches[1]); - if (strlenu($name)==2) $language = $name; - } - } - return $language; + + // Rename directory + public function renameDirectory($pathSource, $pathDestination, $mkdir = false) { + return $pathSource==$pathDestination || $this->renameFile($pathSource, $pathDestination, $mkdir); } - // Return file path from media location - public function findFileFromMedia($location) { - $fileName = null; - if ($this->isFileLocation($location)) { - $mediaLocationLength = strlenu($this->yellow->system->get("coreMediaLocation")); - if (substru($location, 0, $mediaLocationLength)==$this->yellow->system->get("coreMediaLocation")) { - $fileName = $this->yellow->system->get("coreMediaDirectory").substru($location, 7); - } + // Delete file + public function deleteFile($fileName, $pathTrash = "") { + clearstatcache(); + if (is_string_empty($pathTrash)) { + $ok = @unlink($fileName); + } else { + if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); + $fileNameDestination = $pathTrash; + $fileNameDestination .= pathinfo($fileName, PATHINFO_FILENAME); + $fileNameDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s")); + $fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION); + $ok = @rename($fileName, $fileNameDestination); } - return $fileName; + return $ok; } - // Return file path from system location - public function findFileFromSystem($location) { - $fileName = null; - if (preg_match("/\.(css|gif|ico|js|jpg|png|svg|txt|woff|woff2)$/", $location)) { - $extensionLocationLength = strlenu($this->yellow->system->get("coreExtensionLocation")); - $resourceLocationLength = strlenu($this->yellow->system->get("coreResourceLocation")); - if (substru($location, 0, $extensionLocationLength)==$this->yellow->system->get("coreExtensionLocation")) { - $fileName = $this->yellow->system->get("coreExtensionDirectory").substru($location, $extensionLocationLength); - } elseif (substru($location, 0, $resourceLocationLength)==$this->yellow->system->get("coreResourceLocation")) { - $fileName = $this->yellow->system->get("coreResourceDirectory").substru($location, $resourceLocationLength); + // Delete directory + public function deleteDirectory($path, $pathTrash = "") { + clearstatcache(); + if (is_string_empty($pathTrash)) { + $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); + $files = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST); + foreach ($files as $file) { + if ($file->getType()=="dir") { + @rmdir($file->getPathname()); + } else { + @unlink($file->getPathname()); + } } + $ok = @rmdir($path); + } else { + if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); + $pathDestination = $pathTrash; + $pathDestination .= basename($path); + $pathDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s")); + $ok = @rename($path, $pathDestination); } - return $fileName; + return $ok; + } + + // Set file/directory modification date, Unix time + public function modifyFile($fileName, $modified) { + clearstatcache(true, $fileName); + return @touch($fileName, $modified); + } + + // Return file/directory modification date, Unix time + public function getFileModified($fileName) { + return (is_file($fileName) || is_dir($fileName)) ? filemtime($fileName) : 0; } - // Return file path from cache if possible - public function findFileFromCache($location, $fileName, $cacheable) { - if ($cacheable) { - $location .= $this->yellow->toolbox->getLocationArguments(); - $fileNameStatic = rtrim($this->yellow->system->get("coreCacheDirectory"), "/").$location; - if (!$this->isFileLocation($location)) $fileNameStatic .= $this->yellow->system->get("coreStaticDefaultFile"); - if (is_readable($fileNameStatic)) $fileName = $fileNameStatic; + // Return file/directory deletion date, Unix time + public function getFileDeleted($fileName) { + $deleted = 0; + $text = basename($fileName); + $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; + if (preg_match("#^(.+)-(\d\d\d\d-\d\d-\d\d)-(\d\d)-(\d\d)-(\d\d)$#", $text, $matches)) { + $deleted = strtotime("$matches[2] $matches[3]:$matches[4]:$matches[5]"); } - return $fileName; + return $deleted; } - // Normalise file/directory token - public function normaliseToken($text, $fileExtension = "", $removeExtension = false) { - if (!empty($fileExtension)) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; - if (preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !empty($matches[1])) $text = $matches[1]; - return preg_replace("/[^\pL\d\-\_]/u", "-", $text).($removeExtension ? "" : $fileExtension); + // Return file type + public function getFileType($fileName) { + return strtoloweru(($pos = strrposu($fileName, ".")) ? substru($fileName, $pos+1) : ""); } - // Normalise name - public function normaliseName($text, $removePrefix = false, $removeExtension = false, $filterStrict = false) { - if ($removeExtension) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; - if ($removePrefix && preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !empty($matches[1])) $text = $matches[1]; - if ($filterStrict) $text = strtoloweru($text); - return preg_replace("/[^\pL\d\-\_]/u", "-", $text); + // Return file group + public function getFileGroup($fileName, $path) { + $group = "none"; + if (preg_match("#^$path(.+?)\/#", $fileName, $matches)) $group = strtoloweru($matches[1]); + return $group; } - // Normalise prefix - public function normalisePrefix($text) { - $prefix = ""; - if (preg_match("/^([\d\-\_\.]*)(.*)$/", $text, $matches)) $prefix = $matches[1]; - if (!empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-"; - return $prefix; + // Return number of bytes + public function getNumberBytes($text) { + $bytes = intval($text); + switch (strtoupperu(substru($text, -1))) { + case "G": $bytes *= 1024*1024*1024; break; + case "M": $bytes *= 1024*1024; break; + case "K": $bytes *= 1024; break; + } + return $bytes; } - // Normalise array, make keys with same upper/lower case - public function normaliseUpperLower($input) { - $array = array(); - foreach ($input as $key=>$value) { - if (empty($key) || strempty($value)) continue; - $keySearch = strtoloweru($key); - foreach ($array as $keyNew=>$valueNew) { - if (strtoloweru($keyNew)==$keySearch) { - $key = $keyNew; - break; - } - } - $array[$key] += $value; + // Return lines from text, including newline + public function getTextLines($text) { + $lines = preg_split("/\n/", $text); + foreach ($lines as &$line) { + $line = $line."\n"; } - return $array; + if (is_string_empty($text) || substru($text, -1, 1)=="\n") array_pop($lines); + return $lines; } - // Normalise location, make absolute location - public function normaliseLocation($location, $pageLocation, $filterStrict = true) { - if (!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) { - $pageBase = $this->yellow->page->base; - $mediaBase = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreMediaLocation"); - if (!preg_match("/^\#/", $location)) { - if (!preg_match("/^\//", $location)) { - $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; - } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) { - $location = $pageBase.$location; + // Return settings from text + function getTextSettings($text, $blockStart) { + $settings = new YellowArray(); + if (is_string_empty($blockStart)) { + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\#/", $line)) continue; + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $settings[$matches[1]] = $matches[2]; + } } } - $location = strreplaceu("/./", "/", $location); - $location = strreplaceu(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $location); } else { - if ($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter"; + $blockKey = ""; + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\#/", $line)) continue; + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { + $blockKey = $matches[2]; + $settings[$blockKey] = new YellowArray(); + } + if (!is_string_empty($blockKey) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $settings[$blockKey][$matches[1]] = $matches[2]; + } + } + } } - return $location; + return $settings; } - // Normalise URL, make absolute URL - public function normaliseUrl($scheme, $address, $base, $location, $filterStrict = true) { - if (!preg_match("/^\w+:/", $location)) { - $url = "$scheme://$address$base$location"; + // Set settings in text + function setTextSettings($text, $blockStart, $blockKey, $settings) { + $textNew = ""; + if (is_string_empty($blockStart)) { + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && isset($settings[$matches[1]])) { + $textNew .= "$matches[1]: ".$settings[$matches[1]]."\n"; + unset($settings[$matches[1]]); + continue; + } + } + $textNew .= $line; + } + foreach ($settings as $key=>$value) { + $textNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } } else { - if ($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter"; - $url = $location; + $scan = false; + $textStart = $textMiddle = $textEnd = ""; + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { + $scan = lcfirst($matches[2])==lcfirst($blockKey); + } + } + if (!$scan && is_string_empty($textMiddle)) { + $textStart .= $line; + } elseif ($scan) { + $textMiddle .= $line; + } else { + $textEnd .= $line; + } + } + $textSettings = ""; + foreach ($this->getTextLines($textMiddle) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && isset($settings[$matches[1]])) { + $textSettings .= "$matches[1]: ".$settings[$matches[1]]."\n"; + unset($settings[$matches[1]]); + continue; + } + $textSettings .= $line; + } + } + foreach ($settings as $key=>$value) { + $textSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + if (!is_string_empty($textMiddle)) { + $textMiddle = $textSettings; + if (!is_string_empty($textEnd)) $textMiddle .= "\n"; + } else { + if (!is_string_empty($textStart)) $textEnd .= "\n"; + $textEnd .= $textSettings; + } + $textNew = $textStart.$textMiddle.$textEnd; } - return $url; + return $textNew; } - - // Return URL information - public function getUrlInformation($url) { - $scheme = $address = $base = ""; - if (preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, "/"), $matches)) { - $scheme = $matches[1]; - $address = $matches[2]; - $base = $matches[3]; + + // Remove settings from text + function unsetTextSettings($text, $blockStart, $blockKey) { + $textNew = ""; + if (!is_string_empty($blockStart)) { + $scan = false; + $textStart = $textMiddle = $textEnd = ""; + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { + $scan = lcfirst($matches[2])==lcfirst($blockKey); + } + } + if (!$scan && is_string_empty($textMiddle)) { + $textStart .= $line; + } elseif ($scan) { + $textMiddle .= $line; + } else { + $textEnd .= $line; + } + } + $textNew = rtrim($textStart.$textEnd)."\n"; } - return array($scheme, $address, $base); + return $textNew; } - // Return directory location - public function getDirectoryLocation($location) { - return ($pos = strrposu($location, "/")) ? substru($location, 0, $pos+1) : "/"; + // Return array of specific size from text + public function getTextList($text, $separator, $size) { + $tokens = explode($separator, $text, $size); + return array_pad($tokens, $size, ""); } - // Return redirect location - public function getRedirectLocation($location) { - if ($this->isFileLocation($location)) { - $location = "$location/"; - } else { - $languageDefault = $this->yellow->system->get("language"); - $language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->content->getLanguages(), $languageDefault); - $location = "/$language/"; + // Return array of variable size from text, space separated + public function getTextArguments($text, $optional = "-", $sizeMin = 9) { + $text = preg_replace("/\s+/s", " ", trim($text)); + $tokens = str_getcsv($text, " ", "\""); + foreach ($tokens as $key=>$value) { + if (is_null($value) || $value==$optional) $tokens[$key] = ""; } - return $location; - } - - // Check if clean URL is requested - public function isRequestCleanUrl($location) { - return isset($_REQUEST["clean-url"]) && substru($location, -1, 1)=="/"; + return array_pad($tokens, $sizeMin, ""); } - // Check if location is specifying root - public function isRootLocation($location) { - return substru($location, 0, 1)!="/"; + // Return text from array, space separated + public function getTextString($tokens, $optional = "-") { + $text = ""; + foreach ($tokens as $token) { + if (preg_match("/\s/", $token)) $token = "\"$token\""; + if (is_string_empty($token)) $token = $optional; + if (!is_string_empty($text)) $text .= " "; + $text .= $token; + } + return $text; } - - // Check if location is specifying file or directory - public function isFileLocation($location) { - return substru($location, -1, 1)!="/"; + + // Return number of words in text + public function getTextWords($text) { + $text = preg_replace("/([\p{Han}\p{Hiragana}\p{Katakana}]{3})/u", "$1 ", $text); + $text = preg_replace("/(\pL|\p{N})/u", "x", $text); + return str_word_count($text); } - // Check if location can be redirected into directory - public function isRedirectLocation($location) { - $redirect = false; - if ($this->isFileLocation($location)) { - $redirect = is_dir($this->findFileFromLocation("$location/", true)); - } elseif ($location=="/") { - $redirect = $this->yellow->system->get("coreMultiLanguageMode"); + // Return text truncated at word boundary + public function getTextTruncated($text, $lengthMax) { + if (strlenu($text)>$lengthMax-1) { + $text = substru($text, 0, $lengthMax); + $pos = strrposu($text, " "); + $text = substru($text, 0, $pos ? $pos : $lengthMax-1)."…"; } - return $redirect; + return $text; } - // Check if location contains nested directories - public function isNestedLocation($location, $fileName, $checkHomeLocation = false) { - $nested = false; - if (!$checkHomeLocation || $location==$this->yellow->content->getHomeLocation($location)) { - $path = dirname($fileName); - if (count($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true; + // Create text description, with or without HTML + public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") { + $output = ""; + $elementsBlock = array("blockquote", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "li", "ol", "p", "pre", "ul"); + $elementsVoid = array("area", "br", "col", "embed", "hr", "img", "input", "param", "source", "wbr"); + if ($lengthMax==0) $lengthMax = strlenu($text); + if ($removeHtml) { + $hiddenLevel = 0; + $offsetBytes = 0; + while (true) { + $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; + $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; + $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; + $elementAttributes = isset($matches[3][0]) ? $matches[3][0] : ""; + $elementEnd = isset($matches[4][0]) ? $matches[4][0] : ""; + if (!is_string_empty($elementBefore) && !$hiddenLevel) { + $rawText = preg_replace("/\s+/s", " ", html_entity_decode($elementBefore, ENT_QUOTES, "UTF-8")); + if (is_string_empty($elementStart) && in_array(strtolower($elementName), $elementsBlock)) $rawText = rtrim($rawText)." "; + if (substru($rawText, 0, 1)==" " && (is_string_empty($output) || substru($output, -1)==" ")) $rawText = ltrim($rawText); + $output .= $this->getTextTruncated($rawText, $lengthMax); + $lengthMax -= strlenu($rawText); + } + if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) { + $output .= $endMarkerText; + $lengthMax = 0; + } + if ($lengthMax<=0 || !$elementFound) break; + if ($hiddenLevel>0 || + preg_match("/aria-hidden=\"true\"/i", $elementAttributes) || + preg_match("/role=\"doc-noteref\"/i", $elementAttributes)) { + if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { + if (is_string_empty($elementStart)) { + ++$hiddenLevel; + } else { + --$hiddenLevel; + } + } + } + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + } + $output = preg_replace("/\s+\…$/s", "…", $output); + } else { + $elementsOpen = array(); + $offsetBytes = 0; + while (true) { + $elementFound = preg_match("/&.*?\;|<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; + $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; + $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; + $elementEnd = isset($matches[4][0]) ? $matches[4][0] : ""; + if (!is_string_empty($elementBefore)) { + $output .= $this->getTextTruncated($elementBefore, $lengthMax); + $lengthMax -= strlenu($elementBefore); + } + if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) { + $output .= $endMarkerText; + $lengthMax = 0; + } + if ($lengthMax<=0 || !$elementFound) break; + if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { + if (is_string_empty($elementStart)) { + array_push($elementsOpen, $elementName); + } else { + array_pop($elementsOpen); + } + } + $output .= $elementRawData; + if ($elementRawData[0]=="&") --$lengthMax; + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + } + $output = preg_replace("/\s+\…$/s", "…", $output); + for ($i=count($elementsOpen)-1; $i>=0; --$i) { + $output .= "</".$elementsOpen[$i].">"; + } } - return $nested; + return trim($output); } - // Check if location is available - public function isAvailableLocation($location, $fileName) { - $available = true; - $pathBase = $this->yellow->system->get("coreContentDirectory"); - if (substru($fileName, 0, strlenu($pathBase))==$pathBase) { - $sharedLocation = $this->yellow->content->getHomeLocation($location).$this->yellow->system->get("coreContentSharedDirectory"); - if (substru($location, 0, strlenu($sharedLocation))==$sharedLocation) $available = false; - } - return $available; + // Create title from text + public function createTextTitle($text) { + if (preg_match("/^.*\/([\pL\d\-\_]+)/u", $text, $matches)) $text = str_replace("-", " ", ucfirst($matches[1])); + return $text; } - - // Check if location is within current HTTP request - public function isActiveLocation($location, $currentLocation) { - if ($this->isFileLocation($location)) { - $active = $currentLocation==$location; - } else { - if ($location==$this->yellow->content->getHomeLocation($location)) { - $active = $this->getDirectoryLocation($currentLocation)==$location; + + // Create random text for cryptography + public function createSalt($length, $bcryptFormat = false) { + $dataBuffer = $salt = ""; + $dataBufferSize = $bcryptFormat ? intval(ceil($length/4) * 3) : intval(ceil($length/2)); + if (is_string_empty($dataBuffer) && function_exists("random_bytes")) { + $dataBuffer = @random_bytes($dataBufferSize); + } + if (is_string_empty($dataBuffer) && function_exists("openssl_random_pseudo_bytes")) { + $dataBuffer = @openssl_random_pseudo_bytes($dataBufferSize); + } + if (strlenb($dataBuffer)==$dataBufferSize) { + if ($bcryptFormat) { + $salt = substrb(base64_encode($dataBuffer), 0, $length); + $base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + $bcrypt64Chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + $salt = strtr($salt, $base64Chars, $bcrypt64Chars); } else { - $active = substru($currentLocation, 0, strlenu($location))==$location; + $salt = substrb(bin2hex($dataBuffer), 0, $length); } } - return $active; - } - - // Check if file is valid - public function isValidFile($fileName) { - $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); - $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); - $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); - return substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory") || - substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory") || - substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); + return $salt; } - // Check if content file - public function isContentFile($fileName) { - $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); - return substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory"); + // Create hash with random salt, bcrypt or sha256 + public function createHash($text, $algorithm, $cost = 0) { + $hash = ""; + switch ($algorithm) { + case "bcrypt": $prefix = sprintf("$2y$%02d$", $cost); + $salt = $this->createSalt(22, true); + $hash = crypt($text, $prefix.$salt); + if (is_string_empty($salt) || strlenb($hash)!=60) $hash = ""; + break; + case "sha256": $prefix = "$5y$"; + $salt = $this->createSalt(32); + $hash = "$prefix$salt".hash("sha256", $salt.$text); + if (is_string_empty($salt) || strlenb($hash)!=100) $hash = ""; + break; + } + return $hash; } - // Check if media file - public function isMediaFile($fileName) { - $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); - return substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory"); + // Verify that text matches hash + public function verifyHash($text, $algorithm, $hash) { + $hashCalculated = ""; + switch ($algorithm) { + case "bcrypt": if (substrb($hash, 0, 4)=="$2y$" || substrb($hash, 0, 4)=="$2a$") { + $hashCalculated = crypt($text, $hash); + } + break; + case "sha256": if (substrb($hash, 0, 4)=="$5y$") { + $prefix = "$5y$"; + $salt = substrb($hash, 4, 32); + $hashCalculated = "$prefix$salt".hash("sha256", $salt.$text); + } + break; + } + return $this->verifyToken($hashCalculated, $hash); } - // Check if system file - public function isSystemFile($fileName) { - $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); - return substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); - } -} - -class YellowToolbox { - - // Return browser cookie from from current HTTP request - public function getCookie($key) { - return isset($_COOKIE[$key]) ? $_COOKIE[$key] : ""; - } - - // Return server argument from current HTTP request - public function getServer($key) { - return isset($_SERVER[$key]) ? $_SERVER[$key] : ""; - } - - // Return location arguments from current HTTP request - public function getLocationArguments() { - return $this->getServer("LOCATION_ARGUMENTS"); + // Verify that token is not empty and identical, timing attack safe string comparison + public function verifyToken($tokenExpected, $tokenReceived) { + $ok = false; + $lengthExpected = strlenb($tokenExpected); + $lengthReceived = strlenb($tokenReceived); + if ($lengthExpected!=0 && $lengthReceived!=0) { + $ok = $lengthExpected==$lengthReceived; + for ($i=0; $i<$lengthReceived; ++$i) { + $ok &= $tokenExpected[$i<$lengthExpected ? $i : 0]==$tokenReceived[$i]; + } + } + return $ok; } - // Return location arguments from current HTTP request, modify existing arguments - public function getLocationArgumentsNew($key, $value) { - $locationArguments = ""; - $found = false; - $separator = $this->getLocationArgumentsSeparator(); - foreach (explode("/", $this->getServer("LOCATION_ARGUMENTS")) as $token) { - if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { - if ($matches[1]==$key) { - $matches[2] = $value; - $found = true; - } - if (!empty($matches[1]) && !strempty($matches[2])) { - if (!empty($locationArguments)) $locationArguments .= "/"; - $locationArguments .= "$matches[1]:$matches[2]"; + // Return meta data from raw data + public function getMetaData($rawData, $key) { + $value = ""; + if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { + $key = lcfirst($key); + foreach ($this->getTextLines($parts[2]) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$key && !is_string_empty($matches[2])) { + $value = $matches[2]; + break; + } } } } - if (!$found && !empty($key) && !strempty($value)) { - if (!empty($locationArguments)) $locationArguments .= "/"; - $locationArguments .= "$key:$value"; - } - if (!empty($locationArguments)) { - $locationArguments = $this->normaliseArguments($locationArguments, false, false); - if (!$this->isLocationArgumentsPagination($locationArguments)) $locationArguments .= "/"; - } - return $locationArguments; + return $value; } - // Return location arguments from current HTTP request, convert form parameters - public function getLocationArgumentsCleanUrl() { - $locationArguments = ""; - foreach (array_merge($_GET, $_POST) as $key=>$value) { - if (!empty($key) && !strempty($value)) { - if (!empty($locationArguments)) $locationArguments .= "/"; - $key = strreplaceu(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $key); - $value = strreplaceu(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $value); - $locationArguments .= "$key:$value"; + // Set meta data in raw data + public function setMetaData($rawData, $key, $value) { + if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { + $found = false; + $key = lcfirst($key); + $rawDataMiddle = ""; + foreach ($this->getTextLines($parts[2]) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$key) { + $rawDataMiddle .= "$matches[1]: $value\n"; + $found = true; + continue; + } + } + $rawDataMiddle .= $line; } + if (!$found) $rawDataMiddle .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3]; + } else { + $rawDataNew = $rawData; } - if (!empty($locationArguments)) { - $locationArguments = $this->normaliseArguments($locationArguments, false, false); - if (!$this->isLocationArgumentsPagination($locationArguments)) $locationArguments .= "/"; - } - return $locationArguments; - } - - // Return location arguments separator - public function getLocationArgumentsSeparator() { - return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ":" : "="; + return $rawDataNew; } - // Normalise path or location, take care of relative path tokens - public function normaliseTokens($text, $prependSlash = false) { - $textFiltered = ""; - if ($prependSlash && substru($text, 0, 1)!="/") $textFiltered .= "/"; - $textLength = strlenb($text); - for ($pos=0; $pos<$textLength; ++$pos) { - if (($text[$pos]=="/" || $pos==0) && $pos+1<$textLength) { - if ($text[$pos+1]=="/") continue; - if ($text[$pos+1]==".") { - $posNew = $pos+1; - while ($text[$posNew]==".") { - ++$posNew; - } - if ($text[$posNew]=="/" || $text[$posNew]=="") { - $pos = $posNew-1; - continue; - } + // Remove meta data in raw data + public function unsetMetaData($rawData, $key) { + if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { + $key = lcfirst($key); + $rawDataMiddle = ""; + foreach ($this->getTextLines($parts[2]) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$key) continue; } + $rawDataMiddle .= $line; } - $textFiltered .= $text[$pos]; + $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3]; + } else { + $rawDataNew = $rawData; } - return $textFiltered; + return $rawDataNew; } - // Normalise location arguments - public function normaliseArguments($text, $appendSlash = true, $filterStrict = true) { - if ($appendSlash) $text .= "/"; - if ($filterStrict) $text = strreplaceu(" ", "-", strtoloweru($text)); - $text = strreplaceu(":", $this->getLocationArgumentsSeparator(), $text); - return strreplaceu(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text)); + // Return troubleshooting URL + public function getTroubleshootingUrl() { + return "https://datenstrom.se/yellow/help/troubleshooting"; + } + + // Detect server URL + public function detectServerUrl() { + $scheme = "http"; + if ($this->getServer("REQUEST_SCHEME")=="https" || $this->getServer("HTTPS")=="on") $scheme = "https"; + if ($this->getServer("HTTP_X_FORWARDED_PROTO")=="https") $scheme = "https"; + $address = $this->getServer("SERVER_NAME"); + $port = $this->getServer("SERVER_PORT"); + if ($port!=80 && $port!=443) $address .= ":$port"; + $base = ""; + if (preg_match("/^(.*)\/.*\.php$/", $this->getServer("SCRIPT_NAME"), $matches)) $base = $matches[1]; + return "$scheme://$address$base/"; } - // Normalise elements and attributes in html/svg data - public function normaliseData($text, $type = "html", $filterStrict = true) { - $output = ""; - $elementsHtml = array( - "a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"); - $elementsSvg = array( - "svg", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern"); - $attributesHtml = array( - "accept", "action", "align", "allowfullscreen", "alt", "autocomplete", "background", "bgcolor", "border", "cellpadding", "cellspacing", "charset", "checked", "cite", "class", "clear", "color", "cols", "colspan", "content", "controls", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "frameborder", "headers", "height", "hidden", "high", "href", "hreflang", "id", "integrity", "ismap", "label", "lang", "list", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "poster", "prefix", "preload", "property", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "width", "xmlns"); - $attributesSvg = array( - "accent-height", "accumulate", "additivive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "datenstrom", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "tabindex", "targetx", "targety", "transform", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xlink:href", "xml:id", "xml:space", "xmlns", "y", "y1", "y2", "z", "zoomandpan"); - $elementsSafe = $elementsHtml; - $attributesSafe = $attributesHtml; - if ($type=="svg") { - $elementsSafe = array_merge($elementsHtml, $elementsSvg); - $attributesSafe = array_merge($attributesHtml, $attributesSvg); - } - $offsetBytes = 0; - while (true) { - $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); - $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); - $elementStart = $elementFound ? $matches[1][0] : ""; - $elementName = $elementFound ? $matches[2][0]: ""; - $elementMiddle = $elementFound ? $matches[3][0]: ""; - $elementEnd = $elementFound ? $matches[4][0]: ""; - $output .= $elementBefore; - if (substrb($elementName, 0, 1)=="!") { - $output .= "<$elementName$elementMiddle>"; - } elseif (in_array(strtolower($elementName), $elementsSafe)) { - $elementAttributes = $this->getTextAttributes($elementMiddle); - foreach ($elementAttributes as $key=>$value) { - if (!in_array(strtolower($key), $attributesSafe) && !preg_match("/^(aria|data)-/i", $key)) { - unset($elementAttributes[$key]); - } - } - if ($filterStrict) { - $href = isset($elementAttributes["href"]) ? $elementAttributes["href"] : ""; - if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { - $elementAttributes["href"] = "error-xss-filter"; - } - $href = isset($elementAttributes["xlink:href"]) ? $elementAttributes["xlink:href"] : ""; - if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { - $elementAttributes["xlink:href"] = "error-xss-filter"; + // Detect server location + public function detectServerLocation() { + if (isset($_SERVER["REQUEST_URI"])) { + $location = $_SERVER["REQUEST_URI"]; + $location = rawurldecode(($pos = strposu($location, "?")) ? substru($location, 0, $pos) : $location); + $location = $this->yellow->lookup->normalisePath($location); + if (substru($location, 0, 1)!="/") $location = "/".$location; + $separator = $this->getLocationArgumentsSeparator(); + if (preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches)) { + $_SERVER["LOCATION"] = $location = $matches[1]; + $_SERVER["LOCATION_ARGUMENTS"] = $matches[2]; + foreach (explode("/", $matches[2]) as $token) { + if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $matches[1] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[1]); + $matches[2] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[2]); + $_REQUEST[$matches[1]] = $matches[2]; + } } } - $output .= "<$elementStart$elementName"; - foreach ($elementAttributes as $key=>$value) $output .= " $key=\"$value\""; - if (!empty($elementEnd)) $output .= " "; - $output .= "$elementEnd>"; + } else { + $_SERVER["LOCATION"] = $location; + $_SERVER["LOCATION_ARGUMENTS"] = ""; } - if (!$elementFound) break; - $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); } - return $output; + return $this->getServer("LOCATION"); } - // Normalise text lines, convert line endings - public function normaliseLines($text, $endOfLine = "lf") { - if ($endOfLine=="lf") { - $text = preg_replace("/\R/u", "\n", $text); - } else { - $text = preg_replace("/\R/u", "\r\n", $text); + // Detect server sitename + public function detectServerSitename() { + $sitename = "Localhost"; + if (preg_match("#^(www\.)?([\w\-]+)#", $this->getServer("SERVER_NAME"), $matches)) { + $sitename = ucfirst($matches[2]); } - return $text; + return $sitename; } - // Normalise text into UTF-8 NFC - public function normaliseUnicode($text) { - if (PHP_OS=="Darwin" && !mb_check_encoding($text, "ASCII")) { - $utf8nfc = preg_match("//u", $text) && !preg_match("/[^\\x00-\\x{2FF}]/u", $text); - if (!$utf8nfc) $text = iconv("UTF-8-MAC", "UTF-8", $text); + // Detect server timezone + public function detectServerTimezone() { + $timezone = ini_get("date.timezone"); + if (is_string_empty($timezone)) { + if (PHP_OS=="Darwin") { + if (preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1]; + } else { + if (preg_match("/^(\S+)\/(\S+)/", $this->readFile("/etc/timezone"), $matches)) $timezone = $matches[1]; + } } - return $text; - } - - // Return human readable HTTP date - public function getHttpDateFormatted($timestamp) { - return gmdate("D, d M Y H:i:s", $timestamp)." GMT"; + if (!in_array($timezone, timezone_identifiers_list())) $timezone = "UTC"; + return $timezone; } - // Return human readable HTTP server status - public function getHttpStatusFormatted($statusCode, $shortFormat = false) { - switch ($statusCode) { - case 0: $text = "No data"; break; - case 200: $text = "OK"; break; - case 301: $text = "Moved permanently"; break; - case 302: $text = "Moved temporarily"; break; - case 303: $text = "Reload please"; break; - case 304: $text = "Not modified"; break; - case 400: $text = "Bad request"; break; - case 403: $text = "Forbidden"; break; - case 404: $text = "Not found"; break; - case 430: $text = "Login failed"; break; - case 434: $text = "Not existing"; break; - case 500: $text = "Server error"; break; - case 503: $text = "Service unavailable"; break; - default: $text = "Error $statusCode"; + // Detect server name, version and operating system + public function detectServerInformation() { + $name = "Unknown"; + $version = "x.x.x"; + $os = PHP_OS; + if (preg_match("/^(\S+)\/(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) { + $name = $matches[1]; + $version = $matches[2]; + } elseif (preg_match("/^(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) { + $name = $matches[1]; } - $serverProtocol = $this->getServer("SERVER_PROTOCOL"); - if (!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1"; - return $shortFormat ? $text : "$serverProtocol $statusCode $text"; + if (PHP_SAPI=="cli" || PHP_SAPI=="cli-server") { + $name = "Built-in"; + $version = PHP_VERSION; + } + if (PHP_OS=="Darwin") { + $os = "Mac"; + } elseif (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") { + $os = "Windows"; + } + return array($name, $version, $os); } - // Return MIME content type - public function getMimeContentType($fileName) { - $contentType = ""; - $contentTypes = array( - "css" => "text/css", - "gif" => "image/gif", - "html" => "text/html; charset=utf-8", - "ico" => "image/x-icon", - "js" => "application/javascript", - "json" => "application/json", - "jpg" => "image/jpeg", - "md" => "text/markdown", - "png" => "image/png", - "svg" => "image/svg+xml", - "txt" => "text/plain", - "woff" => "application/font-woff", - "woff2" => "application/font-woff2", - "xml" => "text/xml; charset=utf-8"); - $fileType = $this->getFileType($fileName); - if (empty($fileType)) { - $contentType = $contentTypes["html"]; - } elseif (array_key_exists($fileType, $contentTypes)) { - $contentType = $contentTypes[$fileType]; + // Detect browser language + public function detectBrowserLanguage($languages, $languageDefault) { + $languageFound = $languageDefault; + foreach (preg_split("/\s*,\s*/", $this->getServer("HTTP_ACCEPT_LANGUAGE")) as $text) { + list($language, $dummy) = $this->getTextList($text, ";", 2); + if (!is_string_empty($language) && in_array($language, $languages)) { + $languageFound = $language; + break; + } } - return $contentType; + return $languageFound; } - // Return file type - public function getFileType($fileName) { - return strtoloweru(($pos = strrposu($fileName, ".")) ? substru($fileName, $pos+1) : ""); - } - - // Return file group - public function getFileGroup($fileName, $path) { - $group = "none"; - if (preg_match("#^$path(.+?)\/#", $fileName, $matches)) $group = strtoloweru($matches[1]); - return $group; - } - - // Return number of bytes - public function getNumberBytes($string) { - $bytes = intval($string); - switch (strtoupperu(substru($string, -1))) { - case "G": $bytes *= 1024*1024*1024; break; - case "M": $bytes *= 1024*1024; break; - case "K": $bytes *= 1024; break; - } - return $bytes; - } - - // Return files and directories - public function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true) { - $entries = array(); - $directoryHandle = @opendir($path); - if ($directoryHandle) { - $path = rtrim($path, "/"); - while (($entry = readdir($directoryHandle))!==false) { - if (substru($entry, 0, 1)==".") continue; - $entry = $this->normaliseUnicode($entry); - if (preg_match($regex, $entry)) { - if ($directories) { - if (is_dir("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); - } else { - if (is_file("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); - } - } + // Detect terminal width and height + public function detectTerminalInformation() { + $width = $height = 0; + if (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") { + exec("powershell \$Host.UI.RawUI.WindowSize.Width", $outputLines, $returnStatus); + if ($returnStatus==0 && !is_array_empty($outputLines)) { + $width = intval(end($outputLines)); } - if ($sort) natcasesort($entries); - closedir($directoryHandle); - } - return $entries; - } - - // Return files and directories recursively - public function getDirectoryEntriesRecursive($path, $regex = "/.*/", $sort = true, $directories = true, $levelMax = 0) { - --$levelMax; - $entries = $this->getDirectoryEntries($path, $regex, $sort, $directories); - if ($levelMax!=0) { - foreach ($this->getDirectoryEntries($path, "/.*/", $sort, true) as $entry) { - $entries = array_merge($entries, $this->getDirectoryEntriesRecursive($entry, $regex, $sort, $directories, $levelMax)); + exec("powershell \$Host.UI.RawUI.WindowSize.Height", $outputLines, $returnStatus); + if ($returnStatus==0 && !is_array_empty($outputLines)) { + $height = intval(end($outputLines)); + } + } else { + exec("stty size", $outputLines, $returnStatus); + if ($returnStatus==0 && preg_match("/^(\d+)\s+(\d+)/", implode("\n", $outputLines), $matches)) { + $width = intval($matches[2]); + $height = intval($matches[1]); } } - return $entries; + return array($width, $height); } - // Read file, empty string if not found - public function readFile($fileName, $sizeMax = 0) { - $fileData = ""; + // Detect image width, height, orientation and type for GIF/JPG/PNG/SVG + public function detectImageInformation($fileName, $fileType = "") { + $width = $height = $orientation = 0; + $type = ""; $fileHandle = @fopen($fileName, "rb"); if ($fileHandle) { - clearstatcache(true, $fileName); - $fileSize = $sizeMax ? $sizeMax : filesize($fileName); - if ($fileSize) $fileData = fread($fileHandle, $fileSize); - fclose($fileHandle); - } - return $fileData; - } - - // Create file - public function createFile($fileName, $fileData, $mkdir = false) { - $ok = false; - if ($mkdir) { - $path = dirname($fileName); - if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true); - } - $fileHandle = @fopen($fileName, "wb"); - if ($fileHandle) { - clearstatcache(true, $fileName); - if (flock($fileHandle, LOCK_EX)) { - ftruncate($fileHandle, 0); - fwrite($fileHandle, $fileData); - flock($fileHandle, LOCK_UN); + if (is_string_empty($fileType)) $fileType = $this->getFileType($fileName); + if ($fileType=="gif") { + $dataSignature = fread($fileHandle, 6); + $dataHeader = fread($fileHandle, 7); + if (!feof($fileHandle) && ($dataSignature=="GIF87a" || $dataSignature=="GIF89a")) { + $width = (ord($dataHeader[1])<<8) + ord($dataHeader[0]); + $height = (ord($dataHeader[3])<<8) + ord($dataHeader[2]); + $type = $fileType; + } + } elseif ($fileType=="jpg") { + $dataBufferSizeMax = filesize($fileName); + $dataBufferSize = min($dataBufferSizeMax, 4096); + if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); + $dataSignature = substrb($dataBuffer, 0, 2); + if (!feof($fileHandle) && $dataSignature=="\xff\xd8") { + for ($pos=2; $pos+8<$dataBufferSize; $pos+=$length) { + if ($dataBuffer[$pos]!="\xff") break; + $dataMarker = $dataBuffer[$pos+1]; + if ($dataMarker=="\xe1") { + $orientation = $this->getImageOrientationFromBuffer($dataBuffer, $pos+4, $dataBufferSize); + } + if (($dataMarker>="\xc0" && $dataMarker<="\xc3") || + ($dataMarker>="\xc5" && $dataMarker<="\xc7") || + ($dataMarker>="\xc9" && $dataMarker<="\xcb") || + ($dataMarker>="\xcd" && $dataMarker<="\xcf")) { + $width = (ord($dataBuffer[$pos+7])<<8) + ord($dataBuffer[$pos+8]); + $height = (ord($dataBuffer[$pos+5])<<8) + ord($dataBuffer[$pos+6]); + $type = $fileType; + break; + } + $length = (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]) + 2; + while ($pos+$length+8>=$dataBufferSize) { + if ($dataBufferSize==$dataBufferSizeMax) break; + $dataBufferDiff = min($dataBufferSizeMax, $dataBufferSize*2) - $dataBufferSize; + $dataBufferSize += $dataBufferDiff; + $dataBufferChunk = fread($fileHandle, $dataBufferDiff); + if (feof($fileHandle) || $dataBufferChunk===false) { + $dataBufferSize = 0; + break; + } + $dataBuffer .= $dataBufferChunk; + } + } + } + } elseif ($fileType=="png") { + $dataSignature = fread($fileHandle, 8); + $dataHeader = fread($fileHandle, 16); + if (!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n") { + $width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]); + $height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]); + $type = $fileType; + } + } elseif ($fileType=="svg") { + $dataBufferSizeMax = filesize($fileName); + $dataBufferSize = min($dataBufferSizeMax, 4096); + if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); + if (!feof($fileHandle) && preg_match("/<svg(\s.*?)>/s", $dataBuffer, $matches)) { + if (preg_match("/\swidth=\"(\d+)\"/s", $matches[1], $tokens)) $width = $tokens[1]; + if (preg_match("/\sheight=\"(\d+)\"/s", $matches[1], $tokens)) $height = $tokens[1]; + $type = $fileType; + } } fclose($fileHandle); - $ok = true; - } - return $ok; - } - - // Append file - public function appendFile($fileName, $fileData, $mkdir = false) { - $ok = false; - if ($mkdir) { - $path = dirname($fileName); - if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true); } - $fileHandle = @fopen($fileName, "ab"); - if ($fileHandle) { - clearstatcache(true, $fileName); - if (flock($fileHandle, LOCK_EX)) { - fwrite($fileHandle, $fileData); - flock($fileHandle, LOCK_UN); + return array($width, $height, $orientation, $type); + } + + // Return image orientation from Exif + public function getImageOrientationFromBuffer($dataBuffer, $pos, $size) { + $orientation = 0; + $dataSignature = substrb($dataBuffer, $pos, 6); + if ($dataSignature=="\x45\x78\x69\x66\x00\x00" && $pos+14<=$size) { + $startPos = $pos+6; + $bigEndian = $dataBuffer[$startPos]=="M"; + $ifdOffset = $this->getLongFromBuffer($dataBuffer, $startPos+4, $bigEndian); + $ifdStartPos = $startPos+$ifdOffset; + $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0; + $pos = $ifdStartPos+2; + while ($ifdCount && $pos+12<=$size) { + $ifdTag = $this->getShortFromBuffer($dataBuffer, $pos, $bigEndian); + $ifdFormat = $this->getShortFromBuffer($dataBuffer, $pos+2, $bigEndian); + if ($ifdTag==0x8769 && $ifdFormat==4) { + $ifdOffset = $this->getLongFromBuffer($dataBuffer, $pos+8, $bigEndian); + $ifdStartPos = $startPos+$ifdOffset; + $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0; + $pos = $ifdStartPos+2; + continue; + } + if ($ifdTag==0x0112 && $ifdFormat==3) { + $orientation = $this->getShortFromBuffer($dataBuffer, $pos+8, $bigEndian); + break; + } + --$ifdCount; + $pos += 12; } - fclose($fileHandle); - $ok = true; } - return $ok; + return $orientation; } - // Copy file - public function copyFile($fileNameSource, $fileNameDestination, $mkdir = false) { - clearstatcache(); - if ($mkdir) { - $path = dirname($fileNameDestination); - if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + // Return unsigned short value from buffer + public function getShortFromBuffer($dataBuffer, $pos, $bigEndian) { + if ($bigEndian) { + $value = (ord($dataBuffer[$pos])<<8) + ord($dataBuffer[$pos+1]); + } else { + $value = (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]); } - return @copy($fileNameSource, $fileNameDestination); + return $value; } - // Rename file - public function renameFile($fileNameSource, $fileNameDestination, $mkdir = false) { - clearstatcache(); - if ($mkdir) { - $path = dirname($fileNameDestination); - if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + // Return unsigned long value from buffer + public function getLongFromBuffer($dataBuffer, $pos, $bigEndian) { + if ($bigEndian) { + $value = (ord($dataBuffer[$pos])<<24) + (ord($dataBuffer[$pos+1])<<16) + + (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]); + } else { + $value = (ord($dataBuffer[$pos+3])<<24) + (ord($dataBuffer[$pos+2])<<16) + + (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]); } - return @rename($fileNameSource, $fileNameDestination); - } - - // Rename directory - public function renameDirectory($pathSource, $pathDestination, $mkdir = false) { - return $pathSource==$pathDestination || $this->renameFile($pathSource, $pathDestination, $mkdir); + return $value; } - // Delete file - public function deleteFile($fileName, $pathTrash = "") { - clearstatcache(); - if (empty($pathTrash)) { - $ok = @unlink($fileName); - } else { - if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); - $fileNameDestination = $pathTrash; - $fileNameDestination .= pathinfo($fileName, PATHINFO_FILENAME); - $fileNameDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s", filemtime($fileName))); - $fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION); - $ok = @rename($fileName, $fileNameDestination); + // Send email message + public function mail($action, $headers, $message) { + $statusCode = 0; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onMail")) { + $statusCode = $value["object"]->onMail($action, $headers, $message); + if ($statusCode!=0) break; + } } - return $ok; - } - - // Delete directory - public function deleteDirectory($path, $pathTrash = "") { - clearstatcache(); - if (empty($pathTrash)) { - $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); - $files = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST); - foreach ($files as $file) { - if ($file->getType()=="dir") { - @rmdir($file->getPathname()); + if ($statusCode==0) { + $text = $this->yellow->lookup->normaliseHeaders($headers, "mime"); + $to = $subject = $remaining = $key = ""; + foreach (preg_split("/\r\n/", $text) as $line) { + if (preg_match("/^(.*?):\s*(.*?)$/", $line, $matches) && !is_string_empty($matches[1])) { + $key = $matches[1]; + $fragment = $matches[2]; } else { - @unlink($file->getPathname()); + $fragment = $line; } + if ($key=="To") { $to .= $fragment; continue; } + if ($key=="Subject") { $subject .= $fragment; continue; } + $remaining .= $line."\r\n"; } - $ok = @rmdir($path); - } else { - if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); - $pathDestination = $pathTrash; - $pathDestination .= basename($path); - $pathDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s", filemtime($path))); - $ok = @rename($path, $pathDestination); + $statusCode = mail($to, $subject, $message, $remaining) ? 200 : 500; } - return $ok; + return $statusCode==200; + } + + // Write information to log file + public function log($action, $message) { + $statusCode = 0; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onLog")) { + $statusCode = $value["object"]->onLog($action, $message); + if ($statusCode!=0) break; + } + } + if ($statusCode==0) { + $line = date("Y-m-d H:i:s")." ".trim($action)." ".trim($message)."\n"; + $this->appendFile($this->yellow->system->get("coreServerInstallDirectory"). + $this->yellow->system->get("coreExtensionDirectory"). + $this->yellow->system->get("coreWebsiteFile"), $line); + } + } + + // Start timer + public function timerStart(&$time) { + $time = microtime(true); } - // Set file modification date, Unix time - public function modifyFile($fileName, $modified) { - clearstatcache(true, $fileName); - return @touch($fileName, $modified); + // Stop timer and calculate elapsed time in milliseconds + public function timerStop(&$time) { + $time = intval((microtime(true)-$time) * 1000); } - // Return file modification date, Unix time - public function getFileModified($fileName) { - return is_file($fileName) ? filemtime($fileName) : 0; + // Check if there are location arguments in current HTTP request + public function isLocationArguments($location = "") { + if (is_string_empty($location)) $location = $this->getServer("LOCATION").$this->getServer("LOCATION_ARGUMENTS"); + $separator = $this->getLocationArgumentsSeparator(); + return preg_match("/[^\/]+$separator.*$/", $location); } - // Return lines from text string, including newline - public function getTextLines($text) { - $lines = preg_split("/\n/", $text); - foreach ($lines as &$line) { - $line = $line."\n"; - } - if (strempty($text) || substru($text, -1, 1)=="\n") array_pop($lines); - return $lines; + // Check if there are pagination arguments in current HTTP request + public function isLocationArgumentsPagination($location) { + $separator = $this->getLocationArgumentsSeparator(); + return preg_match("/^(.*\/)?page$separator.*$/", $location); + } + + // Check if unmodified since last HTTP request + public function isNotModified($lastModifiedFormatted) { + return $this->getServer("HTTP_IF_MODIFIED_SINCE")==$lastModifiedFormatted; } - // Return attributes from text string - public function getTextAttributes($text) { - $tokens = array(); - $posStart = $posQuote = 0; - $textLength = strlenb($text); - for ($pos=0; $pos<$textLength; ++$pos) { - if ($text[$pos]==" " && !$posQuote) { - if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); - $posStart = $pos+1; - } - if ($text[$pos]=="=" && !$posQuote) { - if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); - array_push($tokens, "="); - $posStart = $pos+1; - } - if ($text[$pos]=="\"") { - if ($posQuote) { - if ($pos>$posQuote) array_push($tokens, substrb($text, $posQuote+1, $pos-$posQuote-1)); - $posQuote = 0; - $posStart = $pos+1; - } else { - if ($pos==$posStart) $posQuote = $pos; - } - } - } - if ($pos>$posStart && !$posQuote) { - array_push($tokens, substrb($text, $posStart, $pos-$posStart)); - } - $attributes = array(); - for ($i=0; $i<count($tokens); ++$i) { - if ($i+2<count($tokens) && $tokens[$i+1]=="=") { - $key = $tokens[$i]; - $value = $tokens[$i+2]; - $i += 2; - } else { - $key = $value = $tokens[$i]; - } - if (!strempty($key) && !strempty($value)) { - $attributes[$key] = $value; - } - } - return $attributes; + // TODO: remove later, for backwards compatibility + public function normaliseArguments($text, $appendSlash = true, $filterStrict = true) { return $this->yellow->lookup->normaliseArguments($text, $appendSlash, $filterStrict); } + public function normalisePath($text) { return $this->yellow->lookup->normalisePath($text); } +} + +class YellowPage { + public $yellow; // access to API + public $scheme; // server scheme + public $address; // server address + public $base; // base location + public $location; // page location + public $fileName; // content file name + public $rawData; // raw data of page + public $metaDataOffsetBytes; // meta data offset + public $metaData; // meta data + public $pageCollections; // additional pages + public $sharedPages; // shared pages + public $headerData; // response header + public $outputData; // response output + public $parser; // content parser + public $parserData; // content data of page + public $statusCode; // status code + public $errorMessage; // error message + public $lastModified; // last modification date + public $available; // page is available? (boolean) + public $visible; // page is visible location? (boolean) + public $active; // page is active location? (boolean) + public $cacheable; // page is cacheable? (boolean) + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->scheme = ""; + $this->address = ""; + $this->base = ""; + $this->location = ""; + $this->fileName = ""; + $this->metaData = new YellowArray(); + $this->pageCollections = array(); + $this->sharedPages = array(); + $this->headerData = array(); + } + + // Set request information + public function setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable) { + $this->scheme = $scheme; + $this->address = $address; + $this->base = $base; + $this->location = $location; + $this->fileName = $fileName; + $this->cacheable = $cacheable; } - // Return array of specific size from text string - public function getTextList($text, $separator, $size) { - $tokens = explode($separator, $text, $size); - return array_pad($tokens, $size, null); + // Parse page meta + public function parseMeta($rawData, $statusCode = 0, $errorMessage = "") { + $this->rawData = $rawData; + $this->parser = null; + $this->parserData = ""; + $this->statusCode = $statusCode; + $this->errorMessage = $errorMessage; + $this->lastModified = 0; + $this->available = true; + $this->visible = true; + $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location); + $this->parseMetaData(); } - // Return arguments from text string, space separated - public function getTextArguments($text, $optional = "-", $sizeMin = 9) { - $text = preg_replace("/\s+/s", " ", trim($text)); - $tokens = str_getcsv($text, " ", "\""); - foreach ($tokens as $key=>$value) { - if ($value==$optional) $tokens[$key] = ""; + // Parse page meta update + public function parseMetaUpdate() { + if ($this->statusCode==0) { + $this->rawData = $this->yellow->toolbox->readFile($this->fileName); + $this->statusCode = 200; + $this->parseMetaData(); } - return array_pad($tokens, $sizeMin, null); } - // Return text string from arguments, space separated - public function getTextString($tokens, $optional = "-") { - $text = ""; - foreach ($tokens as $token) { - if (preg_match("/\s/", $token)) $token = "\"$token\""; - if (empty($token)) $token = $optional; - if (!empty($text)) $text .= " "; - $text .= $token; + // Parse page meta data + public function parseMetaData() { + $this->metaData = new YellowArray(); + $this->metaDataOffsetBytes = 0; + if (!is_null($this->rawData)) { + $this->set("title", $this->yellow->toolbox->createTextTitle($this->location)); + $this->set("language", $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language"))); + $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); + $this->parseMetaDataRaw(array("sitename", "author", "layout", "theme", "parser", "status")); + $this->parseMetaDataShared(); + $titleHeader = ($this->location==$this->yellow->content->getHomeLocation($this->location)) ? + $this->get("sitename") : $this->get("title")." - ".$this->get("sitename"); + if (!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title")); + if (!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title")); + if (!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader); + if ($this->get("status")=="unlisted") $this->visible = false; + if ($this->get("status")=="shared") $this->available = false; + } else { + $this->set("size", filesize($this->fileName)); + $this->set("type", $this->yellow->toolbox->getFileType($this->fileName)); + $this->set("group", $this->yellow->toolbox->getFileGroup($this->fileName, $this->yellow->system->get("coreMediaDirectory"))); + $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); + } + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParseMetaData")) $value["object"]->onParseMetaData($this); } - return $text; - } - - // Return number of words in text string - public function getTextWords($text) { - $text = preg_replace("/([\p{Han}\p{Hiragana}\p{Katakana}]{3})/u", "$1 ", $text); - $text = preg_replace("/(\pL|\p{N})/u", "x", $text); - return str_word_count($text); } - // Return text string truncated at word boundary - public function getTextTruncated($text, $lengthMax) { - if (strlenu($text)>$lengthMax-1) { - $text = substru($text, 0, $lengthMax); - $pos = strrposu($text, " "); - $text = substru($text, 0, $pos ? $pos : $lengthMax-1)."…"; + // Parse page meta data from raw data + public function parseMetaDataRaw($defaultKeys) { + foreach ($defaultKeys as $key) { + $value = $this->yellow->system->get($key); + if (!is_string_empty($key) && !is_string_empty($value)) $this->set($key, $value); + } + if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+/s", $this->rawData, $parts)) { + $this->metaDataOffsetBytes = strlenb($parts[0]); + foreach (preg_split("/[\r\n]+/", $parts[2]) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) $this->set($matches[1], $matches[2]); + } + } + } elseif (preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parts)) { + $this->metaDataOffsetBytes = strlenb($parts[0]); + $this->set("title", $parts[2]); } - return $text; } - // Create description from text string - public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") { - $output = ""; - $elementsBlock = array("blockquote", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "li", "ol", "p", "pre", "ul"); - $elementsVoid = array("area", "br", "col", "embed", "hr", "img", "input", "param", "source", "wbr"); - if ($lengthMax==0) $lengthMax = strlenu($text); - if ($removeHtml) { - $offsetBytes = 0; - while (true) { - $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); - $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); - $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; - $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; - $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; - if (!strempty($elementBefore)) { - $rawText = preg_replace("/\s+/s", " ", html_entity_decode($elementBefore, ENT_QUOTES, "UTF-8")); - if (empty($elementStart) && in_array(strtolower($elementName), $elementsBlock)) $rawText = rtrim($rawText)." "; - if (substru($rawText, 0, 1)==" " && (empty($output) || substru($output, -1)==" ")) $rawText = ltrim($rawText); - $output .= $this->getTextTruncated($rawText, $lengthMax); - $lengthMax -= strlenu($rawText); - } - if (!empty($elementRawData) && $elementRawData==$endMarker) { - $output .= $endMarkerText; - $lengthMax = 0; - } - if ($lengthMax<=0 || !$elementFound) break; - $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + // Parse page meta data for shared pages + public function parseMetaDataShared() { + $this->sharedPages["main"] = $this; + if (!$this->yellow->lookup->isSharedLocation($this->location) && $this->statusCode!=0) { + foreach ($this->yellow->content->getShared($this->location) as $page) { + $this->sharedPages[basename($page->location)] = $page; + $page->sharedPages["main"] = $this; } - $output = preg_replace("/\s+\…$/s", "…", $output); - } else { - $elementsOpen = array(); - $offsetBytes = 0; - while (true) { - $elementFound = preg_match("/&.*?\;|<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); - $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); - $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; - $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; - $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; - $elementEnd = isset($matches[4][0]) ? $matches[4][0] : ""; - if (!strempty($elementBefore)) { - $output .= $this->getTextTruncated($elementBefore, $lengthMax); - $lengthMax -= strlenu($elementBefore); - } - if (!empty($elementRawData) && $elementRawData==$endMarker) { - $output .= $endMarkerText; - $lengthMax = 0; - } - if ($lengthMax<=0 || !$elementFound) break; - if (!empty($elementName) && empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { - if (empty($elementStart)) { - array_push($elementsOpen, $elementName); - } else { - array_pop($elementsOpen); + } + if ($this->yellow->lookup->isSharedLocation($this->location)) { + $this->set("status", "shared"); + } + } + + // Parse page content on demand + public function parseContent() { + if (!is_null($this->rawData) && !is_object($this->parser)) { + if ($this->yellow->extension->isExisting($this->get("parser"))) { + $value = $this->yellow->extension->data[$this->get("parser")]; + if (method_exists($value["object"], "onParseContentRaw")) { + $this->parser = $value["object"]; + $this->parserData = $this->getContentRaw(); + $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData); + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParseContentHtml")) { + $output = $value["object"]->onParseContentHtml($this, $this->parserData); + if (!is_null($output)) $this->parserData = $output; + } } } - $output .= $elementRawData; - if ($elementRawData[0]=="&") --$lengthMax; - $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + } else { + $this->parserData = $this->getContentRaw(); + $this->parserData = preg_replace("/\[yellow error\]/i", $this->errorMessage, $this->parserData); } - $output = preg_replace("/\s+\…$/s", "…", $output); - for ($i=count($elementsOpen)-1; $i>=0; --$i) { - $output .= "</".$elementsOpen[$i].">"; + if (!$this->isExisting("description")) { + $description = $this->yellow->toolbox->createTextDescription($this->parserData, 150); + $this->set("description", !is_string_empty($description) ? $description : $this->get("title")); + } + if ($this->yellow->system->get("coreDebugMode")>=3) { + echo "YellowPage::parseContent location:".$this->location."<br/>\n"; } } - return trim($output); } - // Create title from text string - public function createTextTitle($text) { - if (preg_match("/^.*\/([\pL\d\-\_]+)/u", $text, $matches)) $text = strreplaceu("-", " ", ucfirst($matches[1])); - return $text; + // Parse page content shortcut + public function parseContentShortcut($name, $text, $type) { + $output = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParseContentShortcut")) { + $output = $value["object"]->onParseContentShortcut($this, $name, $text, $type); + if (!is_null($output)) break; + } + } + if (is_null($output)) { + if ($name=="yellow" && $type=="inline" && $text=="error") { + $output = $this->errorMessage; + } + } + if ($this->yellow->system->get("coreDebugMode")>=3 && !is_string_empty($name)) { + echo "YellowPage::parseContentShortcut name:$name type:$type<br/>\n"; + } + return $output; } - - // Create random text for cryptography - public function createSalt($length, $bcryptFormat = false) { - $dataBuffer = $salt = ""; - $dataBufferSize = $bcryptFormat ? intval(ceil($length/4) * 3) : intval(ceil($length/2)); - if (empty($dataBuffer) && function_exists("random_bytes")) { - $dataBuffer = @random_bytes($dataBufferSize); + + // Parse page + public function parsePage() { + $this->parsePageLayout($this->get("layout")); + if (!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, no-store"); + if (!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8"); + if (!$this->isHeader("Content-Modified")) $this->setHeader("Content-Modified", $this->getModified(true)); + if (!$this->isHeader("Last-Modified")) $this->setHeader("Last-Modified", $this->getLastModified(true)); + $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; + if (!is_file($fileNameTheme)) { + $this->error(500, "Theme '".$this->get("theme")."' does not exist!"); } - if (empty($dataBuffer) && function_exists("mcrypt_create_iv")) { - $dataBuffer = @mcrypt_create_iv($dataBufferSize, MCRYPT_DEV_URANDOM); + if (!$this->yellow->language->isExisting($this->get("language"))) { + $this->error(500, "Language '".$this->get("language")."' does not exist!"); } - if (empty($dataBuffer) && function_exists("openssl_random_pseudo_bytes")) { - $dataBuffer = @openssl_random_pseudo_bytes($dataBufferSize); + if (!is_object($this->parser)) { + $this->error(500, "Parser '".$this->get("parser")."' does not exist!"); } - if (strlenb($dataBuffer)==$dataBufferSize) { - if ($bcryptFormat) { - $salt = substrb(base64_encode($dataBuffer), 0, $length); - $base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - $bcrypt64Chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - $salt = strtr($salt, $base64Chars, $bcrypt64Chars); - } else { - $salt = substrb(bin2hex($dataBuffer), 0, $length); + if ($this->yellow->lookup->isNestedLocation($this->location, $this->fileName, true)) { + $this->error(500, "Folder '".dirname($this->fileName)."' may not contain subfolders!"); + } + if ($this->yellow->lookup->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200) { + $location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->location); + $location = $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, "", $location); + $this->status(301, $location); + } + if ($this->yellow->lookup->getRequestHandler()=="core" && !$this->isAvailable() && $this->statusCode==200) { + $this->error(404); + } + if ($this->isExisting("pageClean")) $this->outputData = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParsePageOutput")) { + $output = $value["object"]->onParsePageOutput($this, $this->outputData); + if (!is_null($output)) $this->outputData = $output; } } - return $salt; } - // Create hash with random salt, bcrypt or sha256 - public function createHash($text, $algorithm, $cost = 0) { - $hash = ""; - switch ($algorithm) { - case "bcrypt": $prefix = sprintf("$2y$%02d$", $cost); - $salt = $this->createSalt(22, true); - $hash = crypt($text, $prefix.$salt); - if (empty($salt) || strlenb($hash)!=60) $hash = ""; - break; - case "sha256": $prefix = "$5y$"; - $salt = $this->createSalt(32); - $hash = "$prefix$salt".hash("sha256", $salt.$text); - if (empty($salt) || strlenb($hash)!=100) $hash = ""; - break; + // Parse page layout + public function parsePageLayout($name) { + $this->outputData = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParsePageLayout")) { + $value["object"]->onParsePageLayout($this, $name); + } + } + if (is_null($this->outputData)) { + ob_start(); + $this->includeLayout($name); + $this->outputData = ob_get_contents(); + ob_end_clean(); } - return $hash; } - // Verify that text matches hash - public function verifyHash($text, $algorithm, $hash) { - $hashCalculated = ""; - switch ($algorithm) { - case "bcrypt": if (substrb($hash, 0, 4)=="$2y$" || substrb($hash, 0, 4)=="$2a$") { - $hashCalculated = crypt($text, $hash); - } - break; - case "sha256": if (substrb($hash, 0, 4)=="$5y$") { - $prefix = "$5y$"; - $salt = substrb($hash, 4, 32); - $hashCalculated = "$prefix$salt".hash("sha256", $salt.$text); - } - break; + // Include page layout + public function includeLayout($name) { + $fileNameLayoutNormal = $this->yellow->system->get("coreLayoutDirectory").$this->yellow->lookup->normaliseName($name).".html"; + $fileNameLayoutTheme = $this->yellow->system->get("coreLayoutDirectory"). + $this->yellow->lookup->normaliseName($this->get("theme"))."-".$this->yellow->lookup->normaliseName($name).".html"; + if (is_file($fileNameLayoutTheme)) { + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowPage::includeLayout file:$fileNameLayoutTheme<br/>\n"; + } + $this->setLastModified(filemtime($fileNameLayoutTheme)); + require($fileNameLayoutTheme); + } elseif (is_file($fileNameLayoutNormal)) { + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowPage::includeLayout file:$fileNameLayoutNormal<br/>\n"; + } + $this->setLastModified(filemtime($fileNameLayoutNormal)); + require($fileNameLayoutNormal); + } else { + $this->error(500, "Layout '$name' does not exist!"); + echo "Layout error<br/>\n"; } - return $this->verifyToken($hashCalculated, $hash); } - // Verify that token is not empty and identical, timing attack safe text string comparison - public function verifyToken($tokenExpected, $tokenReceived) { - $ok = false; - $lengthExpected = strlenb($tokenExpected); - $lengthReceived = strlenb($tokenReceived); - if ($lengthExpected!=0 && $lengthReceived!=0) { - $ok = $lengthExpected==$lengthReceived; - for ($i=0; $i<$lengthReceived; ++$i) { - $ok &= $tokenExpected[$i<$lengthExpected ? $i : 0]==$tokenReceived[$i]; + // Set page setting + public function set($key, $value) { + $this->metaData[$key] = $value; + } + + // Return page setting + public function get($key) { + return $this->isExisting($key) ? $this->metaData[$key] : ""; + } + + // Return page setting, HTML encoded + public function getHtml($key) { + return htmlspecialchars($this->get($key)); + } + + // Return page setting as language specific date + public function getDate($key, $format = "") { + if (!is_string_empty($format)) { + $format = $this->yellow->language->getText($format); + } else { + $format = $this->yellow->language->getText("coreDateFormatMedium"); + } + return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format); + } + + // Return page setting as language specific date, HTML encoded + public function getDateHtml($key, $format = "") { + return htmlspecialchars($this->getDate($key, $format)); + } + + // Return page setting as language specific date, relative to today + public function getDateRelative($key, $format = "", $daysLimit = 30) { + if (!is_string_empty($format)) { + $format = $this->yellow->language->getText($format); + } else { + $format = $this->yellow->language->getText("coreDateFormatMedium"); + } + return $this->yellow->language->getDateRelative(strtotime($this->get($key)), $format, $daysLimit); + } + + // Return page setting as language specific date, relative to today, HTML encoded + public function getDateRelativeHtml($key, $format = "", $daysLimit = 30) { + return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit)); + } + + // Return page setting as date + public function getDateFormatted($key, $format) { + return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format); + } + + // Return page setting as date, HTML encoded + public function getDateFormattedHtml($key, $format) { + return htmlspecialchars($this->getDateFormatted($key, $format)); + } + + // Return page content data, raw format + public function getContentRaw() { + $this->parseMetaUpdate(); + return substrb($this->rawData, $this->metaDataOffsetBytes); + } + + // Return page content data, HTML encoded or raw format + public function getContentHtml() { + $this->parseContent(); + return $this->parserData; + } + + // Return page extra data, HTML encoded + public function getExtraHtml($name) { + $output = ""; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParsePageExtra")) { + $outputExtension = $value["object"]->onParsePageExtra($this, $name); + if (!is_null($outputExtension)) $output .= $outputExtension; } } - return $ok; + if ($name=="header") { + $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; + if (is_file($fileNameTheme)) { + $locationTheme = $this->yellow->system->get("coreServerBase"). + $this->yellow->system->get("coreThemeLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; + $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"$locationTheme\" />\n"; + } + $fileNameScript = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; + if (is_file($fileNameScript)) { + $locationScript = $this->yellow->system->get("coreServerBase"). + $this->yellow->system->get("coreThemeLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; + $output .= "<script type=\"text/javascript\" src=\"$locationScript\"></script>\n"; + } + $fileNameFavicon = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".png"; + if (is_file($fileNameFavicon)) { + $locationFavicon = $this->yellow->system->get("coreServerBase"). + $this->yellow->system->get("coreThemeLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".png"; + $output .= "<link rel=\"icon\" type=\"image/png\" href=\"$locationFavicon\" />\n"; + } + } + return $output; } - // Return meta data from raw data - public function getMetaData($rawData, $key) { - $value = ""; - if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { - $key = lcfirst($key); - foreach ($this->getTextLines($parts[2]) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])==$key && !strempty($matches[2])) { - $value = $matches[2]; + // Return parent page, null if none + public function getParent() { + $parentLocation = $this->yellow->content->getParentLocation($this->location); + return $this->yellow->content->find($parentLocation); + } + + // Return top-level parent page, null if none + public function getParentTop($homeFallback = false) { + $parentTopLocation = $this->yellow->content->getParentTopLocation($this->location); + if (!$this->yellow->content->find($parentTopLocation) && $homeFallback) { + $parentTopLocation = $this->yellow->content->getHomeLocation($this->location); + } + return $this->yellow->content->find($parentTopLocation); + } + + // Return page collection with pages on the same level + public function getSiblings($showInvisible = false) { + $parentLocation = $this->yellow->content->getParentLocation($this->location); + return $this->yellow->content->getChildren($parentLocation, $showInvisible); + } + + // Return page collection with child pages + public function getChildren($showInvisible = false) { + return $this->yellow->content->getChildren($this->location, $showInvisible); + } + + // Return page collection with child pages recursively + public function getChildrenRecursive($showInvisible = false, $levelMax = 0) { + return $this->yellow->content->getChildrenRecursive($this->location, $showInvisible, $levelMax); + } + + // Set page collection with additional pages + public function setPages($key, $pages) { + $this->pageCollections[$key] = $pages; + } + + // Return page collection with additional pages + public function getPages($key) { + return isset($this->pageCollections[$key]) ? $this->pageCollections[$key] : new YellowPageCollection($this->yellow); + } + + // Set shared page + public function setPage($key, $page) { + $this->sharedPages[$key] = $page; + } + + // Return shared page + public function getPage($key) { + return isset($this->sharedPages[$key]) ? $this->sharedPages[$key] : new YellowPage($this->yellow); + } + + // Return page URL + public function getUrl() { + return $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, $this->base, $this->location); + } + + // Return page base + public function getBase($multiLanguage = false) { + return $multiLanguage ? rtrim($this->base.$this->yellow->content->getHomeLocation($this->location), "/") : $this->base; + } + + // Return page location + public function getLocation($absoluteLocation = false) { + return $absoluteLocation ? $this->base.$this->location : $this->location; + } + + // Set page request argument + public function setRequest($key, $value) { + $_REQUEST[$key] = $value; + } + + // Return page request argument + public function getRequest($key) { + return isset($_REQUEST[$key]) ? $_REQUEST[$key] : ""; + } + + // Return page request argument, HTML encoded + public function getRequestHtml($key) { + return htmlspecialchars($this->getRequest($key)); + } + + // Set page response header + public function setHeader($key, $value) { + $this->headerData[$key] = $value; + } + + // Return page response header + public function getHeader($key) { + return $this->isHeader($key) ? $this->headerData[$key] : ""; + } + + // Set page response output + public function setOutput($output) { + $this->outputData = $output; + } + + // Return page modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + $modified = strtotime($this->get("modified")); + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; + } + + // Set last modification date, Unix time + public function setLastModified($modified) { + $this->lastModified = max($this->lastModified, $modified); + } + + // Return last modification date, Unix time or HTTP format + public function getLastModified($httpFormat = false) { + $lastModified = max($this->lastModified, $this->getModified(), $this->yellow->system->getModified(), + $this->yellow->language->getModified(), $this->yellow->extension->getModified()); + foreach ($this->pageCollections as $pages) $lastModified = max($lastModified, $pages->getModified()); + foreach ($this->sharedPages as $page) $lastModified = max($lastModified, $page->getModified()); + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($lastModified) : $lastModified; + } + + // Return raw data for error page + public function getRawDataError() { + $statusCode = $this->statusCode; + $sharedLocation = $this->yellow->content->getHomeLocation($this->location)."shared/"; + $fileNameError = $this->yellow->lookup->findFileFromContentLocation($sharedLocation, true).$this->yellow->system->get("coreContentErrorFile"); + $fileNameError = str_replace("(.*)", $statusCode, $fileNameError); + $languageError = $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language")); + if (is_file($fileNameError)) { + $rawData = $this->yellow->toolbox->readFile($fileNameError); + } elseif ($this->yellow->language->isText("coreError{$statusCode}Title", $languageError)) { + $rawData = "---\nTitle: ".$this->yellow->language->getText("coreError{$statusCode}Title", $languageError)."\n"; + $rawData .= "Layout: error\n---\n".$this->yellow->language->getText("coreError{$statusCode}Text", $languageError); + } else { + $rawData = "---\nTitle:".$this->yellow->toolbox->getHttpStatusFormatted($statusCode, true)."\n"; + $rawData .= "Layout:error\n---\n".$this->errorMessage; + } + return $rawData; + } + + // Return page status code, number or HTTP format + public function getStatusCode($httpFormat = false) { + $statusCode = $this->statusCode; + if ($httpFormat) { + $statusCode = $this->yellow->toolbox->getHttpStatusFormatted($statusCode); + if (!is_string_empty($this->errorMessage)) $statusCode .= ": ".$this->errorMessage; + } + return $statusCode; + } + + // Respond with status code, no page content + public function status($statusCode, $location = "") { + if ($statusCode>0 && !$this->isExisting("pageClean")) { + $this->statusCode = $statusCode; + $this->lastModified = 0; + $this->headerData = array(); + if (!is_string_empty($location)) { + $this->setHeader("Location", $location); + $this->setHeader("Cache-Control", "no-cache, no-store"); + } + $this->set("pageClean", (string)$statusCode); + } + } + + // Respond with error page + public function error($statusCode, $errorMessage = "") { + if ($statusCode>=400 && is_string_empty($this->errorMessage)) { + $this->statusCode = $statusCode; + $this->errorMessage = is_string_empty($errorMessage) ? "Page error!" : $errorMessage; + } + } + + // Check if page is available + public function isAvailable() { + return $this->available; + } + + // Check if page is visible + public function isVisible() { + return $this->visible; + } + + // Check if page is within current HTTP request + public function isActive() { + return $this->active; + } + + // Check if page is cacheable + public function isCacheable() { + return $this->cacheable; + } + + // Check if page with error + public function isError() { + return $this->statusCode>=400; + } + + // Check if page setting exists + public function isExisting($key) { + return isset($this->metaData[$key]); + } + + // Check if request argument exists + public function isRequest($key) { + return isset($_REQUEST[$key]); + } + + // Check if response header exists + public function isHeader($key) { + return isset($this->headerData[$key]); + } + + // Check if shared page exists + public function isPage($key) { + return isset($this->sharedPages[$key]); + } + + // TODO: remove later, for backwards compatibility + public function getContent($rawFormat = false) { return $rawFormat ? $this->getContentRaw() : $this->getContentHtml(); } + public function getExtra($name) { return $this->getExtraHtml($name); } +} + +class YellowPageCollection extends ArrayObject { + public $yellow; // access to API + public $filterValue; // current page filter value + public $paginationNumber; // current page number in pagination + public $paginationCount; // highest page number in pagination + + public function __construct($yellow) { + parent::__construct(array()); + $this->yellow = $yellow; + } + + // Append page to end of page collection + #[\ReturnTypeWillChange] + public function append($page) { + parent::append($page); + } + + // Prepend page to start of page collection + #[\ReturnTypeWillChange] + public function prepend($page) { + $array = $this->getArrayCopy(); + array_unshift($array, $page); + $this->exchangeArray($array); + } + + // Remove page from page collection + public function remove($page): YellowPageCollection { + $array = array(); + $location = $page->location; + foreach ($this->getArrayCopy() as $page) { + if ($page->location!=$location) array_push($array, $page); + } + $this->exchangeArray($array); + return $this; + } + + // Filter page collection by page setting + public function filter($key, $value, $exactMatch = true): YellowPageCollection { + $array = array(); + $value = str_replace(" ", "-", strtoloweru($value)); + $valueLength = strlenu($value); + $this->filterValue = ""; + foreach ($this->getArrayCopy() as $page) { + if ($page->isExisting($key)) { + foreach (preg_split("/\s*,\s*/", $page->get($key)) as $pageValue) { + $pageValueLength = $exactMatch ? strlenu($pageValue) : $valueLength; + if ($value==substru(str_replace(" ", "-", strtoloweru($pageValue)), 0, $pageValueLength)) { + if (is_string_empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength); + array_push($array, $page); break; } } } } - return $value; + $this->exchangeArray($array); + return $this; + } + + // Filter page collection by location or file + public function match($regex = "/.*/", $filterByLocation = true): YellowPageCollection { + $array = array(); + $this->filterValue = $regex; + foreach ($this->getArrayCopy() as $page) { + if (preg_match($regex, $filterByLocation ? $page->location : $page->fileName)) array_push($array, $page); + } + $this->exchangeArray($array); + return $this; + } + + // Sort page collection by settings similarity + public function similar($page): YellowPageCollection { + $location = $page->location; + $keywords = strtoloweru($page->get("title").",".$page->get("tag").",".$page->get("author")); + $tokens = array_unique(array_filter(preg_split("/[,\s\(\)\+\-]/", $keywords), "strlen")); + if (!is_array_empty($tokens)) { + $array = array(); + foreach ($this->getArrayCopy() as $page) { + $sortScore = 0; + foreach ($tokens as $token) { + if (stristr($page->get("title"), $token)) $sortScore += 50; + if (stristr($page->get("tag"), $token)) $sortScore += 5; + if (stristr($page->get("author"), $token)) $sortScore += 2; + } + if ($page->location!=$location) { + $page->set("sortScore", $sortScore); + array_push($array, $page); + } + } + $this->exchangeArray($array); + $this->sort("modified", false)->sort("sortScore", false); + } + return $this; + } + + // Sort page collection by page setting + public function sort($key, $ascendingOrder = true): YellowPageCollection { + $array = $this->getArrayCopy(); + $sortIndex = 0; + foreach ($array as $page) { + $page->set("sortIndex", ++$sortIndex); + } + $callback = function ($a, $b) use ($key, $ascendingOrder) { + $result = $ascendingOrder ? + strnatcasecmp($a->get($key), $b->get($key)) : + strnatcasecmp($b->get($key), $a->get($key)); + return $result==0 ? $a->get("sortIndex") - $b->get("sortIndex") : $result; + }; + usort($array, $callback); + $this->exchangeArray($array); + return $this; + } + + // Group page collection by page setting, return array with multiple collections + public function group($key, $ascendingOrder = true, $format = ""): array { + $array = array(); + $groupByInitial = $format=="initial"; + $groupByDate = !is_string_empty($format) && $format!="count" && $format!="initial"; + foreach ($this->getIterator() as $page) { + if ($page->isExisting($key)) { + foreach (preg_split("/\s*,\s*/", $page->get($key)) as $group) { + if ($groupByInitial) { + $group = strtoupperu(substru($group, 0, 1)); + } elseif ($groupByDate) { + $group = $this->yellow->language->getDateFormatted(strtotime($group), $format); + } + if (!is_string_empty($group)) { + if (!isset($array[$group])) { + $groupSearch = strtoloweru($group); + foreach (array_keys($array) as $groupFound) { + if (strtoloweru($groupFound)==$groupSearch) { + $group = $groupFound; + break; + } + } + if (!isset($array[$group])) $array[$group] = new YellowPageCollection($this->yellow); + } + $array[$group]->append($page); + } + } + } + } + $callbackString = function ($a, $b) use ($ascendingOrder) { + return $ascendingOrder ? strnatcasecmp($a, $b) : strnatcasecmp($b, $a); + }; + $callbackCollection = function ($a, $b) use ($ascendingOrder) { + return $ascendingOrder ? count($a)-count($b) : count($b)-count($a); + }; + if ($format!="count") { + uksort($array, $callbackString); + } else { + uasort($array, $callbackCollection); + } + return $array; + } + + // Calculate union, merge page collection + public function merge($input): YellowPageCollection { + $this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input)); + return $this; + } + + // Calculate intersection, remove pages that are not present in another page collection + public function intersect($input): YellowPageCollection { + $callback = function ($a, $b) { + return strcmp($a->location, $b->location); + }; + $this->exchangeArray(array_uintersect($this->getArrayCopy(), (array)$input, $callback)); + return $this; + } + + // Calculate difference, remove pages that are present in another page collection + public function diff($input): YellowPageCollection { + $callback = function ($a, $b) { + return strcmp($a->location, $b->location); + }; + $this->exchangeArray(array_udiff($this->getArrayCopy(), (array)$input, $callback)); + return $this; + } + + // Limit the number of pages in page collection + public function limit($pagesMax): YellowPageCollection { + $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax)); + return $this; + } + + // Reverse page collection + public function reverse(): YellowPageCollection { + $this->exchangeArray(array_reverse($this->getArrayCopy())); + return $this; + } + + // Randomize page collection + public function shuffle(): YellowPageCollection { + $array = $this->getArrayCopy(); + shuffle($array); + $this->exchangeArray($array); + return $this; + } + + // Paginate page collection + public function paginate($limit): YellowPageCollection { + if (!$this->isPagination() && $limit!=0) { + $this->paginationNumber = 1; + $this->paginationCount = ceil($this->count() / $limit); + if ($this->yellow->page->isRequest("page")) { + $this->paginationNumber = intval($this->yellow->page->getRequest("page")); + } + if ($this->paginationNumber<0 || $this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0; + if ($this->paginationNumber) { + $this->exchangeArray(array_slice($this->getArrayCopy(), ($this->paginationNumber - 1) * $limit, $limit)); + } else { + $this->yellow->page->error(404); + } + } + return $this; } - // Set meta data in raw data - public function setMetaData($rawData, $key, $value) { - if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { - $found = false; - $key = lcfirst($key); - foreach ($this->getTextLines($parts[2]) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])==$key) { - $rawDataNew .= "$matches[1]: $value\n"; - $found = true; - continue; - } - } - $rawDataNew .= $line; - } - if (!$found) $rawDataNew .= ucfirst($key).": $value\n"; - $rawDataNew = $parts[1]."---\n".$rawDataNew."---\n".$parts[3]; - } else { - $rawDataNew = $rawData; - } - return $rawDataNew; + // Return current page number in pagination + public function getPaginationNumber() { + return $this->paginationNumber; } - // Detect server URL - public function detectServerUrl() { - $scheme = !empty($this->getServer("HTTPS")) && $this->getServer("HTTPS")!="off" ? "https" : "http"; - $address = $this->getServer("SERVER_NAME"); - $port = $this->getServer("SERVER_PORT"); - if ($port!=80 && $port!=443) $address .= ":$port"; - $base = ""; - if (preg_match("/^(.*)\/.*\.php$/", $this->getServer("SCRIPT_NAME"), $matches)) $base = $matches[1]; - return "$scheme://$address$base/"; + // Return highest page number in pagination + public function getPaginationCount() { + return $this->paginationCount; } - // Detect server location - public function detectServerLocation() { - if (isset($_SERVER["REQUEST_URI"])) { - $location = $_SERVER["REQUEST_URI"]; - $location = rawurldecode(($pos = strposu($location, "?")) ? substru($location, 0, $pos) : $location); - $location = $this->normaliseTokens($location, true); - $separator = $this->getLocationArgumentsSeparator(); - if (preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches)) { - $_SERVER["LOCATION"] = $location = $matches[1]; - $_SERVER["LOCATION_ARGUMENTS"] = $matches[2]; - foreach (explode("/", $matches[2]) as $token) { - if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { - if (!empty($matches[1]) && !strempty($matches[2])) { - $matches[1] = strreplaceu(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[1]); - $matches[2] = strreplaceu(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[2]); - $_REQUEST[$matches[1]] = $matches[2]; - } - } - } - } else { - $_SERVER["LOCATION"] = $location; - $_SERVER["LOCATION_ARGUMENTS"] = ""; - } + // Return location for a page in pagination + public function getPaginationLocation($absoluteLocation = true, $pageNumber = 1) { + $location = $locationArguments = ""; + if ($pageNumber>=1 && $pageNumber<=$this->paginationCount) { + $location = $this->yellow->page->getLocation($absoluteLocation); + $locationArguments = $this->yellow->toolbox->getLocationArgumentsNew("page", $pageNumber>1 ? "$pageNumber" : ""); } - return $this->getServer("LOCATION"); + return $location.$locationArguments; } - // Detect server timezone - public function detectServerTimezone() { - $timezone = @date_default_timezone_get(); - if (PHP_OS=="Darwin" && $timezone=="UTC") { - if (preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1]; - } - return $timezone; + // Return location for previous page in pagination + public function getPaginationPrevious($absoluteLocation = true) { + $pageNumber = $this->paginationNumber-1; + return $this->getPaginationLocation($absoluteLocation, $pageNumber); } - // Detect server name and version - public function detectServerInformation() { - if (preg_match("/^(\S+)\/(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) { - $name = $matches[1]; - $version = $matches[2]; - } elseif (preg_match("/^(\pL+)/u", $this->getServer("SERVER_SOFTWARE"), $matches)) { - $name = $matches[1]; - $version = "x.x.x"; - } else { - $name = "CLI"; - $version = PHP_VERSION; - } - return array($name, $version); + // Return location for next page in pagination + public function getPaginationNext($absoluteLocation = true) { + $pageNumber = $this->paginationNumber+1; + return $this->getPaginationLocation($absoluteLocation, $pageNumber); } - // Detect browser language - public function detectBrowserLanguage($languages, $languageDefault) { - $languageFound = $languageDefault; - foreach (preg_split("/\s*,\s*/", $this->getServer("HTTP_ACCEPT_LANGUAGE")) as $string) { - list($language, $dummy) = $this->getTextList($string, ";", 2); - if (!empty($language) && in_array($language, $languages)) { - $languageFound = $language; + // Return current page number in collection + public function getPageNumber($page) { + $pageNumber = 0; + foreach ($this->getIterator() as $key=>$value) { + if ($page->getLocation()==$value->getLocation()) { + $pageNumber = $key+1; break; } } - return $languageFound; + return $pageNumber; } - // Detect image dimensions and type for gif/jpg/png/svg - public function detectImageInformation($fileName, $fileType = "") { - $width = $height = 0; - $type = ""; - $fileHandle = @fopen($fileName, "rb"); - if ($fileHandle) { - if (empty($fileType)) $fileType = $this->getFileType($fileName); - if ($fileType=="gif") { - $dataSignature = fread($fileHandle, 6); - $dataHeader = fread($fileHandle, 7); - if (!feof($fileHandle) && ($dataSignature=="GIF87a" || $dataSignature=="GIF89a")) { - $width = (ord($dataHeader[1])<<8) + ord($dataHeader[0]); - $height = (ord($dataHeader[3])<<8) + ord($dataHeader[2]); - $type = $fileType; - } - } elseif ($fileType=="jpg") { - $dataBufferSizeMax = filesize($fileName); - $dataBufferSize = min($dataBufferSizeMax, 4096); - if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); - $dataSignature = substrb($dataBuffer, 0, 4); - if (!feof($fileHandle) && ($dataSignature=="\xff\xd8\xff\xe0" || $dataSignature=="\xff\xd8\xff\xe1")) { - for ($pos=2; $pos+8<$dataBufferSize; $pos+=$length) { - if ($dataBuffer[$pos]!="\xff") break; - if ($dataBuffer[$pos+1]=="\xc0" || $dataBuffer[$pos+1]=="\xc2") { - $width = (ord($dataBuffer[$pos+7])<<8) + ord($dataBuffer[$pos+8]); - $height = (ord($dataBuffer[$pos+5])<<8) + ord($dataBuffer[$pos+6]); - $type = $fileType; - break; - } - $length = (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]) + 2; - while ($pos+$length+8>=$dataBufferSize) { - if ($dataBufferSize==$dataBufferSizeMax) break; - $dataBufferDiff = min($dataBufferSizeMax, $dataBufferSize*2) - $dataBufferSize; - $dataBufferSize += $dataBufferDiff; - $dataBufferChunk = fread($fileHandle, $dataBufferDiff); - if (feof($fileHandle) || $dataBufferChunk===false) { - $dataBufferSize = 0; - break; - } - $dataBuffer .= $dataBufferChunk; - } - } - } - } elseif ($fileType=="png") { - $dataSignature = fread($fileHandle, 8); - $dataHeader = fread($fileHandle, 16); - if (!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n") { - $width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]); - $height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]); - $type = $fileType; - } - } elseif ($fileType=="svg") { - $dataBufferSizeMax = filesize($fileName); - $dataBufferSize = min($dataBufferSizeMax, 4096); - if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); - if (!feof($fileHandle) && preg_match("/<svg(\s.*?)>/s", $dataBuffer, $matches)) { - if (preg_match("/\swidth=\"(\d+)\"/s", $matches[1], $tokens)) $width = $tokens[1]; - if (preg_match("/\sheight=\"(\d+)\"/s", $matches[1], $tokens)) $height = $tokens[1]; - $type = $fileType; - } - } - fclose($fileHandle); - } - return array($width, $height, $type); + // Return page in collection, null if none + public function getPage($pageNumber = 1) { + return ($pageNumber>=1 && $pageNumber<=$this->count()) ? $this->offsetGet($pageNumber-1) : null; } - // Start timer - public function timerStart(&$time) { - $time = microtime(true); + // Return previous page in collection, null if none + public function getPagePrevious($page) { + $pageNumber = $this->getPageNumber($page)-1; + return $this->getPage($pageNumber); } - // Stop timer and calculate elapsed time in milliseconds - public function timerStop(&$time) { - $time = intval((microtime(true)-$time) * 1000); + // Return next page in collection, null if none + public function getPageNext($page) { + $pageNumber = $this->getPageNumber($page)+1; + return $this->getPage($pageNumber); } - // Check if there are location arguments in current HTTP request - public function isLocationArguments($location = "") { - if (empty($location)) $location = $this->getServer("LOCATION").$this->getServer("LOCATION_ARGUMENTS"); - $separator = $this->getLocationArgumentsSeparator(); - return preg_match("/[^\/]+$separator.*$/", $location); + // Return current page filter + public function getFilter() { + return $this->filterValue; } - // Check if there are pagination arguments in current HTTP request - public function isLocationArgumentsPagination($location) { - $separator = $this->getLocationArgumentsSeparator(); - return preg_match("/^(.*\/)?page$separator.*$/", $location); + // Return page collection modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + $modified = 0; + foreach ($this->getIterator() as $page) { + $modified = max($modified, $page->getModified()); + } + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; } - - // Check if unmodified since last HTTP request - public function isNotModified($lastModifiedFormatted) { - return $this->getServer("HTTP_IF_MODIFIED_SINCE")==$lastModifiedFormatted; + + // Check if there is a pagination + public function isPagination() { + return $this->paginationCount>1; } - //TODO: remove later, for backwards compatibility - public function getLocationArgs() { return $this->getLocationArguments(); } - public function getLocationArgsNew($key, $value) { return $this->getLocationArgumentsNew($key, $value); } - public function getLocationArgsCleanUrl() { return $this->getLocationArgumentsCleanUrl(); } - public function getLocationArgsSeparator() { return $this->getLocationArgumentsSeparator(); } - public function getTextArgs($text, $optional = "-", $sizeMin = 9) { return $this->getTextArguments($text, $optional, $sizeMin); } - public function normaliseArgs($text, $appendSlash = true, $filterStrict = true) { return $this->normaliseArguments($text, $appendSlash, $filterStrict); } - public function isLocationArgs($location = "") { return $this->isLocationArguments($location); } - public function isLocationArgsPagination($location) { return $this->isLocationArgumentsPagination($location); } + // Check if page collection is empty + public function isEmpty() { + return empty($this->getArrayCopy()); + } } -class YellowExtensions { - public $yellow; //access to API - public $modified; //extension modification date - public $extensions; //registered extensions - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->modified = 0; - $this->extensions = array(); +class YellowArray extends ArrayObject { + public function __construct($array = []) { + parent::__construct($array); } - // Load extensions - public function load($path) { - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) { - if (defined("DEBUG") && DEBUG>=3) echo "YellowExtensions::load file:$entry<br/>\n"; - $this->modified = max($this->modified, filemtime($entry)); - require_once($entry); - $name = $this->yellow->lookup->normaliseName(basename($entry), true, true); - $this->register(lcfirst($name), "Yellow".ucfirst($name)); - } - $callback = function ($a, $b) { - return $a["priority"] - $b["priority"]; - }; - uasort($this->extensions, $callback); - foreach ($this->extensions as $key=>$value) { - if (method_exists($this->extensions[$key]["obj"], "onLoad")) $this->extensions[$key]["obj"]->onLoad($this->yellow); - } - $this->yellow->system->set("mediaLocation", "/media/"); //TODO: remove later, for backwards compatibility - $this->yellow->system->set("downloadLocation", "/media/downloads/"); - $this->yellow->system->set("imageLocation", "/media/images/"); - $this->yellow->system->set("extensionLocation", "/media/extensions/"); - $this->yellow->system->set("resourceLocation", "/media/resources/"); - $this->yellow->system->set("mediaDir", "media/"); - $this->yellow->system->set("downloadDir", "media/downloads/"); - $this->yellow->system->set("imageDir", "media/images/"); - $this->yellow->system->set("systemDir", "system/"); - $this->yellow->system->set("extensionDir", "system/extensions/"); - $this->yellow->system->set("layoutDir", "system/layouts/"); - $this->yellow->system->set("resourceDir", "system/resources/"); - $this->yellow->system->set("settingDir", "system/settings/"); - $this->yellow->system->set("trashDir", "system/trash/"); - $this->yellow->system->set("contentDir", "content/"); - $this->yellow->system->set("contentPagination", "page"); - $this->yellow->system->set("coreStaticDir", "public/"); - $this->yellow->system->set("coreCacheDir", "cache/"); - $this->yellow->system->set("coreTrashDir", "system/trash/"); - $this->yellow->system->set("coreMediaDir", "media/"); - $this->yellow->system->set("coreDownloadDir", "media/downloads/"); - $this->yellow->system->set("coreImageDir", "media/images/"); - $this->yellow->system->set("coreSystemDir", "system/"); - $this->yellow->system->set("coreExtensionDir", "system/extensions/"); - $this->yellow->system->set("coreLayoutDir", "system/layouts/"); - $this->yellow->system->set("coreResourceDir", "system/resources/"); - $this->yellow->system->set("coreSettingDir", "system/settings/"); - $this->yellow->system->set("coreContentDir", "content/"); - $this->yellow->system->set("coreContentRootDir", "default/"); - $this->yellow->system->set("coreContentHomeDir", "home/"); - $this->yellow->system->set("coreContentSharedDir", "shared/"); + // Set array element + public function set($key, $value) { + $this->offsetSet($key, $value); } - // Register extension - public function register($name, $class) { - if (!$this->isExisting($name) && class_exists($class)) { - $this->extensions[$name] = array(); - $this->extensions[$name]["obj"] = new $class; - $this->extensions[$name]["class"] = $class; - $this->extensions[$name]["version"] = defined("$class::VERSION") ? $class::VERSION : 0; - $this->extensions[$name]["type"] = defined("$class::TYPE") ? $class::TYPE : "feature"; - $this->extensions[$name]["priority"] = defined("$class::PRIORITY") ? $class::PRIORITY : count($this->extensions) + 10; - } + // Return array element + public function get($key) { + return $this->offsetExists($key) ? $this->offsetGet($key) : ""; } - // Return extension - public function get($name) { - return $this->extensions[$name]["obj"]; + // Check if array element exists + public function isExisting($key) { + return $this->offsetExists($key); } - // Return extensions version - public function getData($type = "") { - $data = array(); - foreach ($this->extensions as $key=>$value) { - if (empty($type) || $value["type"]==$type) { - $data[$key] = $value["version"]; - } - } - uksort($data, "strnatcasecmp"); - return $data; + // Return array element + #[\ReturnTypeWillChange] + public function offsetGet($key) { + if (is_string($key)) $key = lcfirst($key); + return parent::offsetGet($key); } - // Return extensions modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; + // Set array element + #[\ReturnTypeWillChange] + public function offsetSet($key, $value) { + if (is_string($key)) $key = lcfirst($key); + parent::offsetSet($key, $value); } - - // Return extensions - public function getExtensions($type = "") { - $extensions = array(); - foreach ($this->extensions as $key=>$value) { - if (empty($type) || $value["type"]==$type) { - array_push($extensions, $key); - } - } - return $extensions; + + // Remove array element + #[\ReturnTypeWillChange] + public function offsetUnset($key) { + if (is_string($key)) $key = lcfirst($key); + parent::offsetUnset($key); } - // Check if extension exists - public function isExisting($name) { - return isset($this->extensions[$name]); + // Check if array element exists + #[\ReturnTypeWillChange] + public function offsetExists($key) { + if (is_string($key)) $key = lcfirst($key); + return parent::offsetExists($key); } -} -// Check if string is empty -function strempty($string) { - return is_null($string) || $string===""; + // Check if array is empty + public function isEmpty() { + return empty($this->getArrayCopy()); + } } // Make string lowercase, UTF-8 compatible @@ -3264,47 +3844,53 @@ function strtoupperu() { return call_user_func_array("mb_strtoupper", func_get_args()); } -// Replace string, UTF-8 compatible -function strreplaceu() { - return call_user_func_array("str_replace", func_get_args()); -} - -// Return string length in characters, UTF-8 compatible +// Return string length, UTF-8 characters function strlenu() { return call_user_func_array("mb_strlen", func_get_args()); } -// Return string length in bytes, ASCII compatible +// Return string length, bytes function strlenb() { return call_user_func_array("strlen", func_get_args()); } -// Return string positon in characters, UTF-8 compatible +// Return string position of first match, UTF-8 characters function strposu() { return call_user_func_array("mb_strpos", func_get_args()); } -// Return string position in bytes, ASCII compatible +// Return string position of first match, bytes function strposb() { return call_user_func_array("strpos", func_get_args()); } -// Return reverse string position in characters, UTF-8 compatible +// Return string position of last match, UTF-8 characters function strrposu() { return call_user_func_array("mb_strrpos", func_get_args()); } -// Return reverse string position in bytes, ASCII compatible +// Return string position of last match, bytes function strrposb() { return call_user_func_array("strrpos", func_get_args()); } -// Return part of a string, UTF-8 compatible +// Return part of a string, UTF-8 characters function substru() { return call_user_func_array("mb_substr", func_get_args()); } -// Return part of a string, ASCII compatible +// Return part of a string, bytes function substrb() { return call_user_func_array("substr", func_get_args()); } + +// Check if string is empty +function is_string_empty($string) { + return is_null($string) || $string===""; +} +function strempty($string) { return is_null($string) || $string===""; } //TODO: remove later, for backwards compatibility + +// Check if array is empty +function is_array_empty($array) { + return is_null($array) || (is_array($array) ? empty($array) : empty($array->getArrayCopy())); +} diff --git a/system/extensions/edit-stack.svg b/system/extensions/edit-stack.svg @@ -0,0 +1,61 @@ +<svg xmlns="http://www.w3.org/2000/svg"><defs><style>.stack{display:none;}.stack:target{display:inline;}</style></defs> + +<g class="stack" id="preview"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M8 5.509c1.362 0 2.491 1.129 2.491 2.491s-1.129 2.491-2.491 2.491c-1.362 0-2.491-1.129-2.491-2.491s1.129-2.491 2.491-2.491zM8 12.164c2.296 0 4.164-1.868 4.164-4.164s-1.868-4.164-4.164-4.164c-2.296 0-4.164 1.868-4.164 4.164s1.868 4.164 4.164 4.164zM8 1.774c4.164 0 6.562 2.568 8.002 6.226-1.44 3.658-3.838 6.226-8.002 6.226s-6.562-2.568-8.002-6.226c1.44-3.658 3.838-6.226 8.002-6.226z"></path></svg></g> + +<g class="stack" id="format"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M5 1h8v2h-2v12.017h-2v-12.017h-2v12.017h-2v-6.017c-2.209 0-4-1.791-4-4s1.791-4 4-4z"></path></svg></g> + +<g class="stack" id="heading"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M0.006 0.998h11.996v2.035h-4.982v11.998h-2.033v-11.998h-4.982z"></path><path fill="currentColor" d="M13.59 14.977v-6.602h2.341v-1.383h-6.353v1.383h2.341v6.602q0 0 0.249 0 0.255 0 0.587 0t0.581 0q0.255 0 0.255 0z"></path></svg></g> + +<g class="stack" id="h1"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M0.006 0.998h11.996v2.035h-4.982v11.998h-2.033v-11.998h-4.982z"></path><path fill="currentColor" d="M13.557 14.977h1.66v-7.985h-1.666q0 0-0.31 0.216t-0.719 0.504q-0.409 0.282-0.725 0.498-0.31 0.216-0.31 0.216v1.511q0 0 0.304-0.21 0.31-0.216 0.714-0.493 0.404-0.282 0.708-0.493 0.31-0.216 0.31-0.216h0.033q0 0 0 0.47 0 0.465 0 1.201 0 0.73 0 1.555t0 1.561 0 1.201 0 0.465z"></path></svg></g> + +<g class="stack" id="h2"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M0.006 0.998h11.996v2.035h-4.982v11.998h-2.033v-11.998h-4.982z"></path><path fill="currentColor" d="M10.076 9.398v0.022h1.549v-0.028q0-0.553 0.382-0.913 0.382-0.365 0.963-0.365 0.553 0 0.908 0.315 0.354 0.31 0.354 0.764 0 0.409-0.227 0.786-0.221 0.371-0.825 0.974l-3.010 2.9v1.123h5.805v-1.328h-3.603v-0.033l1.948-1.876q0.664-0.653 1.101-1.262 0.443-0.609 0.443-1.35 0-1.007-0.791-1.66-0.786-0.653-2.048-0.653-1.306 0-2.131 0.725-0.819 0.725-0.819 1.859z"></path></svg></g> + +<g class="stack" id="h3"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M0.006 0.998h11.996v2.035h-4.982v11.998h-2.033v-11.998h-4.982z"></path><path fill="currentColor" d="M11.825 11.49h0.985q0.675 0 1.063 0.315 0.387 0.31 0.387 0.858 0 0.509-0.398 0.83-0.398 0.315-1.024 0.315-0.659 0-1.051-0.304-0.393-0.31-0.432-0.813h-1.599q0.055 1.101 0.88 1.782 0.83 0.681 2.18 0.681 1.383 0 2.263-0.653 0.88-0.659 0.88-1.732 0-0.83-0.537-1.339-0.531-0.509-1.422-0.587v-0.033q0.736-0.133 1.19-0.631 0.459-0.498 0.459-1.234 0-0.957-0.775-1.544t-2.020-0.587q-1.295 0-2.097 0.67-0.802 0.664-0.847 1.771h1.538q0.033-0.52 0.393-0.836 0.365-0.315 0.941-0.315 0.62 0 0.952 0.293t0.332 0.775q0 0.493-0.349 0.802t-0.924 0.31h-0.968q0 0 0 0.304 0 0.299 0 0.603 0 0.299 0 0.299z"></path></svg></g> + +<g class="stack" id="bold"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M11.061 7.573c0.586-0.696 0.939-1.594 0.939-2.573 0-2.206-1.794-4-4-4h-5v14h6c2.206 0 4-1.794 4-4 0-1.452-0.778-2.726-1.939-3.427zM6 3h1.586c0.874 0 1.586 0.897 1.586 2s-0.711 2-1.586 2h-1.586v-4zM8.484 13h-2.484v-4h2.484c0.913 0 1.656 0.897 1.656 2s-0.743 2-1.656 2z"></path></svg></g> + +<g class="stack" id="italic"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M8.413 0.996h1.905l-2.73 14.008h-1.905z"></path></svg></g> + +<g class="stack" id="strikethrough"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M8.316 0.719c-2.093-0.024-4 0.687-4.87 2.207-0.752 1.348-0.556 3.247 0.681 4.247 0.42 0.359 0.889 0.62 1.384 0.826h-5.512v1h8.986c0.882 0.266 1.801 0.668 2.162 1.562 0.379 0.955-0.102 2.077-0.989 2.558-1.145 0.686-2.607 0.69-3.849 0.282-0.655-0.227-1.207-0.649-1.62-1.189l-1.293 1.136c0.397 0.536 0.923 0.979 1.528 1.272 1.559 0.775 3.413 0.778 5.071 0.35 1.46-0.389 2.79-1.549 3.021-3.095 0.187-0.976 0.029-2.077-0.57-2.876h3.553v-1h-4.813c-1.47-0.79-3.211-0.948-4.767-1.539-0.768-0.265-1.578-0.851-1.587-1.744-0.101-1.135 0.878-2.042 1.917-2.277 0.745-0.221 2.107-0.196 2.776-0.071s1.36 0.419 1.823 0.937l1.209-1.139c-0.341-0.357-0.745-0.654-1.189-0.869-0.812-0.417-1.983-0.58-2.889-0.579-0.054-0.001-0.108-0.001-0.163-0.001z"></path></svg></g> + +<g class="stack" id="quote"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M3.508 6.5c1.933 0 3.5 1.567 3.5 3.5s-1.567 3.5-3.5 3.5-3.5-1.567-3.5-3.5l-0.016-0.5c0-3.866 3.134-7 7-7v2c-1.336 0-2.591 0.52-3.536 1.464-0.182 0.182-0.348 0.375-0.497 0.578 0.179-0.028 0.362-0.043 0.548-0.043zM12.508 6.5c1.933 0 3.5 1.567 3.5 3.5s-1.567 3.5-3.5 3.5-3.5-1.567-3.5-3.5l-0.016-0.5c0-3.866 3.134-7 7-7v2c-1.336 0-2.591 0.52-3.536 1.464-0.182 0.182-0.348 0.375-0.497 0.578 0.179-0.028 0.362-0.043 0.549-0.043z"></path></svg></g> + +<g class="stack" id="code"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M9.907 11.284l1.408 1.408 4.692-4.692-4.692-4.692-1.408 1.408 3.284 3.284z"></path><path fill="currentColor" d="M6.095 4.716l-1.408-1.408-4.692 4.692 4.692 4.692 1.408-1.408-3.284-3.284z"></path></svg></g> + +<g class="stack" id="link"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M6.879 9.934c-0.208 0-0.416-0.079-0.575-0.238-1.486-1.486-1.486-3.905 0-5.392l3-3c0.72-0.72 1.678-1.117 2.696-1.117s1.976 0.397 2.696 1.117c1.486 1.487 1.486 3.905 0 5.392l-1.371 1.371c-0.317 0.317-0.832 0.317-1.149 0s-0.317-0.832 0-1.149l1.371-1.371c0.853-0.853 0.853-2.241 0-3.094-0.413-0.413-0.963-0.641-1.547-0.641s-1.134 0.228-1.547 0.641l-3 3c-0.853 0.853-0.853 2.241 0 3.094 0.317 0.317 0.317 0.832 0 1.149-0.159 0.159-0.367 0.238-0.575 0.238z"></path><path fill="currentColor" d="M4 15.813c-1.018 0-1.976-0.397-2.696-1.117-1.486-1.486-1.486-3.905 0-5.392l1.371-1.371c0.317-0.317 0.832-0.317 1.149 0s0.317 0.832 0 1.149l-1.371 1.371c-0.853 0.853-0.853 2.241 0 3.094 0.413 0.413 0.962 0.641 1.547 0.641s1.134-0.228 1.547-0.641l3-3c0.853-0.853 0.853-2.241 0-3.094-0.317-0.317-0.317-0.832 0-1.149s0.832-0.317 1.149 0c1.486 1.486 1.486 3.905 0 5.392l-3 3c-0.72 0.72-1.678 1.117-2.696 1.117z"></path></svg></g> + +<g class="stack" id="file"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M1 1c-0.55 0-1 0.45-1 1v12c0 0.55 0.45 1 1 1h14c0.55 0 1-0.45 1-1v-12c0-0.55-0.45-1-1-1zM2.007 2.487l11.975 0.022c0.577 0.017 0.527 0.485 0.527 0.485l-0.008 3.462-4 3.544h-1l-4-5-3.978 7.012-0.019-9.034c0.001-0.001-0.039-0.508 0.504-0.491z"></path><path fill="currentColor" d="M12.375 5.125c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5z"></path></svg></g> + +<g class="stack" id="list"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M6 1h10v2h-10zM6 7h10v2h-10zM6 13h10v2h-10zM-0.017 2c0-1.105-0.105-1.017 1-1.017s1-0.105 1 1c0 1.105 0.138 1.017-0.966 1.017s-1.034 0.105-1.034-1zM-0.005 8c0-1.105-0.099-1.005 1.005-1.005s1.017-0.116 1.017 0.988c0 1.105 0.088 1.022-1.017 1.022s-1.005 0.099-1.005-1.005zM0.008 14c0-1.105-0.113-1 0.992-1s1-0.113 1 0.992c0 1.105 0.122 1-0.983 1s-1.008 0.113-1.008-0.992z"></path></svg></g> + +<g class="stack" id="ol"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M7 13h9v2h-9zM7 7h9v2h-9zM7 1h9v2h-9z"></path><path fill="currentColor" d="M1.456 5.52h1.147v-5.519h-1.151q0 0-0.214 0.149t-0.497 0.348q-0.283 0.195-0.501 0.344-0.214 0.149-0.214 0.149v1.044q0 0 0.21-0.145 0.214-0.149 0.493-0.34 0.279-0.195 0.49-0.34 0.214-0.149 0.214-0.149h0.023q0 0 0 0.325 0 0.321 0 0.83 0 0.505 0 1.075t0 1.079 0 0.83 0 0.321z"></path><path fill="currentColor" d="M-0.016 11.15v0.015h1.071v-0.019q0-0.382 0.264-0.631 0.264-0.252 0.666-0.252 0.382 0 0.627 0.218 0.245 0.214 0.245 0.528 0 0.283-0.157 0.543-0.153 0.256-0.57 0.673l-2.081 2.004v0.776h4.012v-0.918h-2.49v-0.023l1.346-1.297q0.459-0.451 0.761-0.872 0.306-0.421 0.306-0.933 0-0.696-0.547-1.147-0.543-0.451-1.415-0.451-0.903 0-1.473 0.501-0.566 0.501-0.566 1.285z"></path></svg></g> + +<g class="stack" id="tl"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M8.945 1.034l7.055-0.034v2l-7.033-0.012zM7 7h9v2h-9zM7 13h9v2h-9zM2.017 3c-1.345 8.667-0.672 4.333 0 0z"></path><path fill="currentColor" d="M6.5 0l-3.517 3.517-1.534-1.534-1.449 1.517 3.017 3 4.992-4.958z"></path></svg></g> + +<g class="stack" id="hr"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M1 7h14v2h-14z"></path></svg></g> + +<g class="stack" id="table"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M0 1v14h16v-14zM7.5 3v5h-6.5v-5zM8.5 8v-5h6.5v5zM1 9h6.5v5h-6.5zM8.5 14v-5h6.5v5z"></path></svg></g> + +<g class="stack" id="emoji"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M10.006 6c0-0.498 0.487-1.005 0.988-1.005 0.493 0 1.005 0.516 1.005 1.005s-0.521 0.998-1.005 0.988c-0.501-0.010-0.988-0.507-0.988-0.988zM8 16c4.418 0 8-3.582 8-8s-3.582-8-8-8c-4.418 0-8 3.582-8 8s3.582 8 8 8zM8 1.5c3.59 0 6.5 2.91 6.5 6.5s-2.91 6.5-6.5 6.5c-3.59 0-6.5-2.91-6.5-6.5s2.91-6.5 6.5-6.5zM12.5 8.966c-0.445 2.035-1.893 3.534-4.5 3.534s-3.987-1.433-4.5-3.536c4.022-0.028 4.969-0.013 9 0.002zM4 6c0-0.583 0.499-1 1-1 0.493 0 1 0.511 1 1s-0.516 1.009-1 1c-0.501-0.010-1-0.417-1-1z"></path></svg></g> + +<g class="stack" id="icon"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M16 6.204l-5.528-0.803-2.472-5.009-2.472 5.009-5.528 0.803 4 3.899-0.944 5.505 4.944-2.599 4.944 2.599-0.944-5.505 4-3.899z"></path></svg></g> + +<g class="stack" id="status"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M3.985 0.985l-1.984 0.016v13.999l1.984-0.016v-5.669c0.597-0.403 1.323-0.677 2.64-0.677 1.046 0 1.972 0.238 2.625 0.445v0c0.622 0.271 1.495 0.677 2.625 0.677s1.613-0.336 2.125-0.739v-6.999c-0.512 0.404-0.995 0.739-2.125 0.739s-2.047-0.439-2.625-0.677c-0.641-0.195-1.579-0.445-2.625-0.445-1.318 0-2.043 0.273-2.64 0.677z"></path></svg></g> + +<g class="stack" id="undo"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M11.904 16c1.777-3.219 2.076-8.13-4.904-7.966v3.966l-6-6 6-6v3.881c8.359-0.218 9.29 7.378 4.904 12.119z"></path></svg></g> + +<g class="stack" id="redo"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M9 3.881v-3.881l6 6-6 6v-3.966c-6.98-0.164-6.681 4.747-4.904 7.966-4.386-4.741-3.455-12.337 4.904-12.119z"></path></svg></g> + +<g class="stack" id="spinner"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M8.044 0.771c-0.249 0-0.451 0.202-0.451 0.451v2.705c0 0.249 0.202 0.451 0.451 0.451s0.451-0.202 0.451-0.451v-2.705c0-0.249-0.202-0.451-0.451-0.451zM4.651 1.677c-0.073 0.002-0.146 0.021-0.213 0.060-0.216 0.125-0.289 0.4-0.165 0.616l1.352 2.343c0.125 0.216 0.4 0.29 0.616 0.165 0.145-0.083 0.226-0.235 0.226-0.391 0-0.077-0.019-0.154-0.060-0.225l-1.352-2.343c-0.086-0.148-0.243-0.23-0.403-0.225zM11.437 1.677c-0.16-0.004-0.317 0.077-0.403 0.225l-1.352 2.343c-0.124 0.216-0.051 0.491 0.165 0.616s0.491 0.051 0.616-0.165l1.352-2.342c0.041-0.071 0.060-0.149 0.060-0.225 0-0.156-0.081-0.307-0.225-0.391-0.067-0.039-0.141-0.058-0.213-0.060zM13.887 4.096c-0.082 0.002-0.164 0.024-0.24 0.068l-2.342 1.352c-0.243 0.14-0.326 0.45-0.186 0.693s0.45 0.326 0.693 0.186l2.343-1.352c0.163-0.094 0.254-0.265 0.254-0.44h-0c0-0.086-0.022-0.173-0.068-0.253-0.096-0.167-0.273-0.258-0.453-0.254zM2.2 4.152c-0.16-0.004-0.317 0.077-0.403 0.225-0.125 0.216-0.051 0.491 0.165 0.616l2.343 1.353c0.216 0.125 0.491 0.051 0.616-0.165 0.041-0.071 0.060-0.149 0.060-0.225 0-0.156-0.081-0.307-0.225-0.391l-2.343-1.352c-0.067-0.039-0.141-0.058-0.213-0.060zM1.282 7.082c-0.498 0-0.902 0.404-0.902 0.902s0.404 0.902 0.902 0.902h2.705c0.498 0 0.902-0.404 0.902-0.902s-0.404-0.902-0.902-0.902zM12.101 7.421c-0.311 0-0.564 0.252-0.564 0.564s0.252 0.564 0.564 0.564h2.705c0.311 0 0.564-0.252 0.564-0.564s-0.252-0.564-0.564-0.564zM4.548 9.168c-0.15-0.003-0.302 0.033-0.441 0.113l-2.343 1.352c-0.404 0.233-0.543 0.75-0.309 1.155s0.75 0.543 1.155 0.309l2.343-1.352c0.271-0.157 0.423-0.441 0.423-0.733 0-0.143-0.037-0.289-0.113-0.422-0.153-0.265-0.428-0.416-0.714-0.423zM11.574 9.393c-0.22-0.006-0.436 0.106-0.553 0.31-0.171 0.296-0.070 0.676 0.227 0.847l2.343 1.352c0.296 0.171 0.676 0.070 0.847-0.227 0.056-0.098 0.083-0.204 0.083-0.309l-0 0c0-0.214-0.111-0.423-0.31-0.537l-2.343-1.352c-0.093-0.053-0.193-0.080-0.293-0.083zM5.998 10.709c-0.266 0.006-0.523 0.147-0.666 0.394l-1.352 2.343c-0.218 0.377-0.089 0.86 0.289 1.078s0.86 0.089 1.078-0.289l1.352-2.343c0.072-0.124 0.106-0.26 0.106-0.394l0-0c0-0.273-0.142-0.538-0.395-0.684-0.13-0.075-0.272-0.109-0.411-0.106zM10.087 10.822c-0.12-0.003-0.241 0.026-0.353 0.091-0.323 0.187-0.434 0.6-0.248 0.924l1.352 2.343c0.187 0.323 0.6 0.434 0.924 0.248 0.217-0.125 0.338-0.353 0.338-0.586l-0 0c0-0.115-0.029-0.231-0.091-0.338l-1.352-2.343c-0.123-0.212-0.343-0.333-0.571-0.338zM8.044 11.309c-0.405 0-0.733 0.328-0.733 0.733v2.705c0 0.405 0.328 0.733 0.733 0.733s0.733-0.328 0.733-0.733v-2.705c0-0.405-0.328-0.733-0.733-0.733z"></path></svg></g> + +<g class="stack" id="select"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M12.78 6.22c0.136 0.136 0.219 0.323 0.219 0.53s-0.084 0.394-0.219 0.53l-4.25 4.25c-0.136 0.136-0.323 0.219-0.53 0.219s-0.394-0.084-0.53-0.219l-4.25-4.25c-0.125-0.134-0.201-0.313-0.201-0.511 0-0.414 0.336-0.75 0.75-0.75 0.198 0 0.378 0.077 0.512 0.202l-0-0 3.72 3.72 3.72-3.72c0.136-0.136 0.323-0.219 0.53-0.219s0.394 0.084 0.53 0.219l-0-0z"></path></svg></g> + +<g class="stack" id="search"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M15.504 13.616l-3.79-3.223c-0.392-0.353-0.811-0.514-1.149-0.499 0.895-1.048 1.435-2.407 1.435-3.893 0-3.314-2.686-6-6-6s-6 2.686-6 6 2.686 6 6 6c1.486 0 2.845-0.54 3.893-1.435-0.016 0.338 0.146 0.757 0.499 1.149l3.223 3.79c0.552 0.613 1.453 0.665 2.003 0.115s0.498-1.452-0.115-2.003zM6 10c-2.209 0-4-1.791-4-4s1.791-4 4-4 4 1.791 4 4-1.791 4-4 4z"></path></svg></g> + +<g class="stack" id="close"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M6.309 7.945l-4.364-5.231 1.854-1.691 4.161 4.999 4.288-5.026 1.71 1.467-4.486 5.32 4.584 5.63-1.603 1.567-4.483-5.287-4.176 5.312-1.744-1.591z"></path></svg></g> + +<g class="stack" id="help"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M6.852 10.89v-1.053q0-0.421 0.038-0.708 0.057-0.306 0.172-0.536 0.134-0.249 0.325-0.459 0.211-0.23 0.498-0.536l1.474-1.493q0.478-0.478 0.478-1.244 0-0.746-0.498-1.206-0.478-0.478-1.225-0.478-0.804 0-1.32 0.555-0.517 0.536-0.593 1.32l-2.449-0.191q0.115-0.919 0.478-1.627 0.364-0.727 0.938-1.225 0.593-0.498 1.359-0.746 0.765-0.268 1.684-0.268 0.861 0 1.588 0.249 0.746 0.249 1.282 0.727 0.555 0.459 0.861 1.167 0.306 0.689 0.306 1.588 0 0.631-0.172 1.11t-0.459 0.88-0.67 0.765q-0.364 0.364-0.785 0.765-0.268 0.249-0.459 0.44t-0.325 0.402q-0.115 0.191-0.172 0.459-0.057 0.249-0.057 0.612v0.727zM6.469 13.55q0-0.612 0.44-1.053 0.459-0.44 1.091-0.44 0.612 0 1.072 0.421t0.459 1.033-0.459 1.053q-0.44 0.44-1.072 0.44-0.306 0-0.593-0.115-0.268-0.115-0.478-0.306t-0.344-0.459q-0.115-0.268-0.115-0.574z"></path></svg></g> + +<g class="stack" id="logo"><svg viewBox="0 0 16 16"><path fill="currentColor" d="M8-0.005l-7.623 4.003c2.818 1.48 2.818 1.48 0 0-0.029 1.334-0.013 2.668 0 4.003-0.029 1.334-0.013 2.668 0 4.003l3.811 2.001c0-0.001 0-0.002 0-0.003 0 0.001-0 0.002 0 0.003l3.811 2.001 7.623-4.003c0.013-1.334 0.029-2.668 0-4.003 0.013-1.334 0.029-2.668 0-4.003-8.681 4.558 0-0 0-0l-3.811-2.001z"></path></svg></g> + +</svg> +\ No newline at end of file diff --git a/system/extensions/edit.css b/system/extensions/edit.css @@ -1,6 +1,4 @@ -/* Edit extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/edit */ -/* Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se */ -/* This file may be used and distributed under the terms of the public license. */ +/* Edit extension, https://github.com/annaesvensson/yellow-edit */ .yellow-bar { position: relative; @@ -250,6 +248,12 @@ border-color: #666; color: #fff; } +.yellow-toolbar .yellow-toolbar-disabled, +.yellow-toolbar .yellow-toolbar-disabled:hover { + background-color: inherit; + border-color: #c1c1c1 #c1c1c1 #aaa; + color: #aaa; +} .yellow-toolbar-tooltip { position: relative; } @@ -342,7 +346,7 @@ color: #333333; background-image: linear-gradient(to bottom, #f8f8f8, #e1e1e1); border: 1px solid #bbb; - border-color: #c1c1c1 #c1c1c1 #aaaaaa; + border-color: #c1c1c1 #c1c1c1 #aaa; border-radius: 4px; outline-offset: -2px; font-size: 0.9em; @@ -419,44 +423,39 @@ font-size: 0.9em; line-height: 1.8; } -#yellow-popup-emojiawesome { +#yellow-popup-emoji { padding: 10px; width: 14em; } -#yellow-popup-emojiawesome a { +#yellow-popup-emoji a { padding: 0.2em; } -#yellow-popup-emojiawesome .yellow-dropdown li { +#yellow-popup-emoji .yellow-dropdown li { display: inline-block; } -#yellow-popup-fontawesome { +#yellow-popup-icon { padding: 10px; width: 13em; } -#yellow-popup-fontawesome a { +#yellow-popup-icon a { padding: 0.18em 0.3em; min-width: 1em; text-align: center; } -#yellow-popup-fontawesome .yellow-dropdown li { +#yellow-popup-icon .yellow-dropdown li { display: inline-block; } /* Icons */ -@font-face { - font-family: "Edit"; - font-weight: normal; - font-style: normal; - src: url("edit.woff") format("woff"); -} .yellow-icon { display: inline-block; - font-family: Edit; - font-style: normal; - font-weight: normal; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + width: 1em; + height: 1em; + background-color: currentcolor; + background-size: 1em 1em; + background-repeat: no-repeat; + background-position: center center; } .yellow-spin { -webkit-animation: yellow-spin 1s infinite steps(16); @@ -482,100 +481,122 @@ transform: rotate(359deg); } } - -.yellow-icon-preview:before { - content: "\f100"; -} -.yellow-icon-format:before { - content: "\f101"; -} -.yellow-icon-paragraph:before { - content: "\f101"; -} -.yellow-icon-heading:before { - content: "\f102"; -} -.yellow-icon-h1:before { - content: "\f103"; -} -.yellow-icon-h2:before { - content: "\f104"; -} -.yellow-icon-h3:before { - content: "\f105"; -} -.yellow-icon-bold:before { - content: "\f106"; +.yellow-icon-preview { + -webkit-mask: url("edit-stack.svg#preview"); + mask: url("edit-stack.svg#preview"); } -.yellow-icon-italic:before { - content: "\f0f7"; +.yellow-icon-format, +.yellow-icon-paragraph { + -webkit-mask: url("edit-stack.svg#format"); + mask: url("edit-stack.svg#format"); } -.yellow-icon-strikethrough:before { - content: "\f108"; +.yellow-icon-heading { + -webkit-mask: url("edit-stack.svg#heading"); + mask: url("edit-stack.svg#heading"); } -.yellow-icon-quote:before { - content: "\f109"; +.yellow-icon-h1 { + -webkit-mask: url("edit-stack.svg#h1"); + mask: url("edit-stack.svg#h1"); } -.yellow-icon-code:before { - content: "\f10a"; +.yellow-icon-h2 { + -webkit-mask: url("edit-stack.svg#h2"); + mask: url("edit-stack.svg#h2"); } -.yellow-icon-pre:before { - content: "\f10a"; +.yellow-icon-h3 { + -webkit-mask: url("edit-stack.svg#h3"); + mask: url("edit-stack.svg#h3"); } -.yellow-icon-link:before { - content: "\f10b"; +.yellow-icon-bold { + -webkit-mask: url("edit-stack.svg#bold"); + mask: url("edit-stack.svg#bold"); } -.yellow-icon-file:before { - content: "\f10c"; +.yellow-icon-italic { + -webkit-mask: url("edit-stack.svg#italic"); + mask: url("edit-stack.svg#italic"); } -.yellow-icon-list:before { - content: "\f10d"; +.yellow-icon-strikethrough { + -webkit-mask: url("edit-stack.svg#strikethrough"); + mask: url("edit-stack.svg#strikethrough"); } -.yellow-icon-ul:before { - content: "\f10d"; +.yellow-icon-quote { + -webkit-mask: url("edit-stack.svg#quote"); + mask: url("edit-stack.svg#quote"); } -.yellow-icon-ol:before { - content: "\f10e"; +.yellow-icon-code, +.yellow-icon-pre { + -webkit-mask: url("edit-stack.svg#code"); + mask: url("edit-stack.svg#code"); } -.yellow-icon-tl:before { - content: "\f10f"; +.yellow-icon-link { + -webkit-mask: url("edit-stack.svg#link"); + mask: url("edit-stack.svg#link"); } -.yellow-icon-hr:before { - content: "\f110"; +.yellow-icon-file { + -webkit-mask: url("edit-stack.svg#file"); + mask: url("edit-stack.svg#file"); } -.yellow-icon-table:before { - content: "\f111"; +.yellow-icon-list, +.yellow-icon-ul { + -webkit-mask: url("edit-stack.svg#list"); + mask: url("edit-stack.svg#list"); } -.yellow-icon-emojiawesome:before { - content: "\f112"; +.yellow-icon-ol { + -webkit-mask: url("edit-stack.svg#ol"); + mask: url("edit-stack.svg#ol"); } -.yellow-icon-fontawesome:before { - content: "\f113"; +.yellow-icon-tl { + -webkit-mask: url("edit-stack.svg#tl"); + mask: url("edit-stack.svg#tl"); } -.yellow-icon-status:before { - content: "\f114"; +.yellow-icon-hr { + -webkit-mask: url("edit-stack.svg#hr"); + mask: url("edit-stack.svg#hr"); } -.yellow-icon-undo:before { - content: "\f115"; +.yellow-icon-table { + -webkit-mask: url("edit-stack.svg#table"); + mask: url("edit-stack.svg#table"); } -.yellow-icon-redo:before { - content: "\f116"; +.yellow-icon-emoji { + -webkit-mask: url("edit-stack.svg#emoji"); + mask: url("edit-stack.svg#emoji"); } -.yellow-icon-spinner:before { - content: "\f200"; +.yellow-icon-icon { + -webkit-mask: url("edit-stack.svg#icon"); + mask: url("edit-stack.svg#icon"); } -.yellow-icon-search:before { - content: "\f201"; +.yellow-icon-status { + -webkit-mask: url("edit-stack.svg#status"); + mask: url("edit-stack.svg#status"); } -.yellow-icon-close:before { - content: "\f202"; +.yellow-icon-undo { + -webkit-mask: url("edit-stack.svg#undo"); + mask: url("edit-stack.svg#undo"); } -.yellow-icon-help:before { - content: "\f203"; +.yellow-icon-redo { + -webkit-mask: url("edit-stack.svg#redo"); + mask: url("edit-stack.svg#redo"); } -.yellow-icon-markdown:before { - content: "\f203"; +.yellow-icon-spinner { + -webkit-mask: url("edit-stack.svg#spinner"); + mask: url("edit-stack.svg#spinner"); } -.yellow-icon-logo:before { - content: "\f8ff"; +.yellow-icon-select { + -webkit-mask: url("edit-stack.svg#select"); + mask: url("edit-stack.svg#select"); +} +.yellow-icon-search { + -webkit-mask: url("edit-stack.svg#search"); + mask: url("edit-stack.svg#search"); +} +.yellow-icon-close { + -webkit-mask: url("edit-stack.svg#close"); + mask: url("edit-stack.svg#close"); +} +.yellow-icon-help { + -webkit-mask: url("edit-stack.svg#help"); + mask: url("edit-stack.svg#help"); +} +.yellow-icon-logo { + -webkit-mask: url("edit-stack.svg#logo"); + mask: url("edit-stack.svg#logo"); } diff --git a/system/extensions/edit.js b/system/extensions/edit.js @@ -1,6 +1,4 @@ -// Edit extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/edit -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. +// Edit extension, https://github.com/annaesvensson/yellow-edit var yellow = { onLoad: function(e) { yellow.edit.load(e); }, @@ -16,11 +14,11 @@ var yellow = { }; yellow.edit = { - paneId: 0, //visible pane ID - paneAction: 0, //current pane action - paneStatus: 0, //current pane status - popupId: 0, //visible popup ID - intervalId: 0, //timer interval ID + paneId: 0, // visible pane ID + paneAction: 0, // current pane action + paneStatus: 0, // current pane status + popupId: 0, // visible popup ID + intervalId: 0, // timer interval ID // Handle initialisation load: function(e) { @@ -76,7 +74,7 @@ yellow.edit = { // Handle page cache pageShow: function(e) { - if (e.persisted && yellow.system.userEmail && !this.getCookie("csrftoken")) { + if (e.persisted && yellow.user.email && !this.getCookie("csrftoken")) { window.location.reload(); } }, @@ -94,7 +92,7 @@ yellow.edit = { } var elementDiv = document.createElement("div"); elementDiv.setAttribute("id", barId+"-content"); - if (yellow.system.userName) { + if (yellow.user.name) { elementDiv.innerHTML = "<div class=\"yellow-bar-left\">"+ this.getRawDataPaneAction("edit")+ @@ -102,7 +100,7 @@ yellow.edit = { "<div class=\"yellow-bar-right\">"+ this.getRawDataPaneAction("create")+ this.getRawDataPaneAction("delete")+ - this.getRawDataPaneAction("menu", yellow.system.userName, true)+ + this.getRawDataPaneAction("menu", yellow.user.name, true)+ "</div>"+ "<div class=\"yellow-bar-banner\"></div>"; } else { @@ -132,7 +130,7 @@ yellow.edit = { // Create pane createPane: function(paneId, paneAction, paneStatus) { - if (yellow.system.debug) console.log("yellow.edit.createPane id:"+paneId); + if (yellow.system.coreDebugMode) console.log("yellow.edit.createPane id:"+paneId); var elementPane = document.createElement("div"); elementPane.className = "yellow-pane"; elementPane.setAttribute("id", paneId); @@ -156,7 +154,7 @@ yellow.edit = { case "yellow-pane-login": elementDiv.innerHTML = "<form method=\"post\">"+ - "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+ "<div class=\"yellow-title\"><h1>"+this.getText("LoginTitle")+"</h1></div>"+ "<div class=\"yellow-fields\">"+ "<input type=\"hidden\" name=\"action\" value=\"login\" />"+ @@ -170,7 +168,7 @@ yellow.edit = { case "yellow-pane-signup": elementDiv.innerHTML = "<form method=\"post\">"+ - "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+ "<div class=\"yellow-title\"><h1>"+this.getText("SignupTitle")+"</h1></div>"+ "<div class=\"yellow-status\"><p id=\"yellow-pane-signup-status\" class=\""+paneStatus+"\">"+this.getText("SignupStatus", "", paneStatus)+"</p></div>"+ "<div class=\"yellow-fields\">"+ @@ -186,7 +184,7 @@ yellow.edit = { case "yellow-pane-forgot": elementDiv.innerHTML = "<form method=\"post\">"+ - "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+ "<div class=\"yellow-title\"><h1>"+this.getText("ForgotTitle")+"</h1></div>"+ "<div class=\"yellow-status\"><p id=\"yellow-pane-forgot-status\" class=\""+paneStatus+"\">"+this.getText("ForgotStatus", "", paneStatus)+"</p></div>"+ "<div class=\"yellow-fields\">"+ @@ -199,7 +197,7 @@ yellow.edit = { case "yellow-pane-recover": elementDiv.innerHTML = "<form method=\"post\">"+ - "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+ "<div class=\"yellow-title\"><h1>"+this.getText("RecoverTitle")+"</h1></div>"+ "<div class=\"yellow-status\"><p id=\"yellow-pane-recover-status\" class=\""+paneStatus+"\">"+this.getText("RecoverStatus", "", paneStatus)+"</p></div>"+ "<div class=\"yellow-fields\">"+ @@ -211,7 +209,7 @@ yellow.edit = { case "yellow-pane-quit": elementDiv.innerHTML = "<form method=\"post\">"+ - "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+ "<div class=\"yellow-title\"><h1>"+this.getText("QuitTitle")+"</h1></div>"+ "<div class=\"yellow-status\"><p id=\"yellow-pane-quit-status\" class=\""+paneStatus+"\">"+this.getText("QuitStatus", "", paneStatus)+"</p></div>"+ "<div class=\"yellow-fields\">"+ @@ -225,7 +223,7 @@ yellow.edit = { case "yellow-pane-account": elementDiv.innerHTML = "<form method=\"post\">"+ - "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+ "<div class=\"yellow-title\"><h1 id=\"yellow-pane-account-title\">"+this.getText("AccountTitle")+"</h1></div>"+ "<div class=\"yellow-status\"><p id=\"yellow-pane-account-status\" class=\""+paneStatus+"\">"+this.getText("AccountStatus", "", paneStatus)+"</p></div>"+ "<div class=\"yellow-settings\">"+ @@ -245,22 +243,22 @@ yellow.edit = { "</div>"+ "</form>"; break; - case "yellow-pane-system": + case "yellow-pane-configure": elementDiv.innerHTML = "<form method=\"post\">"+ - "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ - "<div class=\"yellow-title\"><h1 id=\"yellow-pane-system-title\">"+this.getText("SystemTitle")+"</h1></div>"+ - "<div class=\"yellow-status\"><p id=\"yellow-pane-system-status\" class=\""+paneStatus+"\">"+this.getText("SystemStatus", "", paneStatus)+"</p></div>"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+ + "<div class=\"yellow-title\"><h1 id=\"yellow-pane-configure-title\">"+this.getText("ConfigureTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-configure-status\" class=\""+paneStatus+"\">"+this.getText("ConfigureStatus", "", paneStatus)+"</p></div>"+ "<div class=\"yellow-settings\">"+ - "<div id=\"yellow-pane-system-settings-actions\" class=\"yellow-settings-left\"><p>"+this.getRawDataSettingsActions(paneAction)+"</p></div>"+ - "<div id=\"yellow-pane-system-settings-separator\" class=\"yellow-settings-left yellow-settings-separator\">&nbsp;</div>"+ - "<div id=\"yellow-pane-system-settings-fields\" class=\"yellow-settings-right yellow-fields\">"+ - "<input type=\"hidden\" name=\"action\" value=\"system\" />"+ + "<div id=\"yellow-pane-configure-settings-actions\" class=\"yellow-settings-left\"><p>"+this.getRawDataSettingsActions(paneAction)+"</p></div>"+ + "<div id=\"yellow-pane-configure-settings-separator\" class=\"yellow-settings-left yellow-settings-separator\">&nbsp;</div>"+ + "<div id=\"yellow-pane-configure-settings-fields\" class=\"yellow-settings-right yellow-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"configure\" />"+ "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+ - "<p><label for=\"yellow-pane-system-sitename\">"+this.getText("SystemSitename")+"</label><br /><input class=\"yellow-form-control\" name=\"sitename\" id=\"yellow-pane-system-sitename\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("sitename"))+"\" /></p>"+ - "<p><label for=\"yellow-pane-system-author\">"+this.getText("SystemAuthor")+"</label><br /><input class=\"yellow-form-control\" name=\"author\" id=\"yellow-pane-system-author\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("author"))+"\" /></p>"+ - "<p><label for=\"yellow-pane-system-email\">"+this.getText("SystemEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-system-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ - "<p>"+this.getText("SystemInformation")+"</p>"+ + "<p><label for=\"yellow-pane-configure-sitename\">"+this.getText("ConfigureSitename")+"</label><br /><input class=\"yellow-form-control\" name=\"sitename\" id=\"yellow-pane-configure-sitename\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("sitename"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-configure-author\">"+this.getText("ConfigureAuthor")+"</label><br /><input class=\"yellow-form-control\" name=\"author\" id=\"yellow-pane-configure-author\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("author"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-configure-email\">"+this.getText("ConfigureEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-configure-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ + "<p>"+this.getText("ConfigureInformation")+"</p>"+ "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("ChangeButton")+"\" /></p>"+ "</div>"+ "<div class=\"yellow-settings yellow-settings-banner\"></div>"+ @@ -270,12 +268,12 @@ yellow.edit = { case "yellow-pane-update": elementDiv.innerHTML = "<form method=\"post\">"+ - "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ - "<div class=\"yellow-title\"><h1 id=\"yellow-pane-update-title\">"+yellow.toolbox.encodeHtml(yellow.system.coreVersion)+"</h1></div>"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+ + "<div class=\"yellow-title\"><h1 id=\"yellow-pane-update-title\">"+yellow.toolbox.encodeHtml(yellow.system.coreProductRelease)+"</h1></div>"+ "<div class=\"yellow-status\"><p id=\"yellow-pane-update-status\" class=\""+paneStatus+"\">"+this.getText("UpdateStatus", "", paneStatus)+"</p></div>"+ "<div class=\"yellow-output\" id=\"yellow-pane-update-output\">"+yellow.page.rawDataOutput+"</div>"+ "<div class=\"yellow-buttons\" id=\"yellow-pane-update-buttons\">"+ - "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ + "<p><a href=\"#\" id=\"yellow-pane-update-submit\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ "</div>"+ "</form>"; break; @@ -330,7 +328,7 @@ yellow.edit = { case "yellow-pane-menu": elementDiv.innerHTML = "<ul class=\"yellow-dropdown\">"+ - "<li><span>"+yellow.toolbox.encodeHtml(yellow.system.userEmail)+"</span></li>"+ + "<li><span>"+yellow.toolbox.encodeHtml(yellow.user.email)+"</span></li>"+ "<li><a href=\"#\" data-action=\"settings\">"+this.getText("MenuSettings")+"</a></li>" + "<li><a href=\"#\" data-action=\"help\">"+this.getText("MenuHelp")+"</a></li>" + "<li><a href=\"#\" data-action=\"submit\" data-arguments=\"action:logout\">"+this.getText("MenuLogout")+"</a></li>"+ @@ -339,7 +337,7 @@ yellow.edit = { case "yellow-pane-information": elementDiv.innerHTML = "<form method=\"post\">"+ - "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+ "<div class=\"yellow-title\"><h1 id=\"yellow-pane-information-title\">"+this.getText(paneAction+"Title")+"</h1></div>"+ "<div class=\"yellow-status\"><p id=\"yellow-pane-information-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ "<div class=\"yellow-buttons\" id=\"yellow-pane-information-buttons\">"+ @@ -348,7 +346,7 @@ yellow.edit = { "</form>"; break; default: elementDiv.innerHTML = - "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+ + "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\" aria-label=\""+this.getText("CancelButton")+"\"></i></a>"+ "<div class=\"yellow-error\">Pane '"+paneId+"' was not found. Oh no...</div>"; } elementPane.appendChild(elementDiv); @@ -376,18 +374,20 @@ yellow.edit = { } if (paneStatus=="none") { document.getElementById("yellow-pane-account-status").innerHTML = this.getText("AccountStatusNone"); - document.getElementById("yellow-pane-account-name").value = yellow.system.userName; - document.getElementById("yellow-pane-account-email").value = yellow.system.userEmail; + document.getElementById("yellow-pane-account-name").value = yellow.user.name; + document.getElementById("yellow-pane-account-email").value = yellow.user.email; document.getElementById("yellow-pane-account-password").value = ""; - document.getElementById("yellow-pane-account-"+yellow.system.userLanguage).checked = true; + if (document.getElementById("yellow-pane-account-"+yellow.user.language)) { + document.getElementById("yellow-pane-account-"+yellow.user.language).checked = true; + } } break; - case "yellow-pane-system": + case "yellow-pane-configure": if (paneStatus=="none") { - document.getElementById("yellow-pane-system-status").innerHTML = this.getText("SystemStatusNone"); - document.getElementById("yellow-pane-system-sitename").value = yellow.system.sitename; - document.getElementById("yellow-pane-system-author").value = yellow.system.author; - document.getElementById("yellow-pane-system-email").value = yellow.system.email; + document.getElementById("yellow-pane-configure-status").innerHTML = this.getText("ConfigureStatusNone"); + document.getElementById("yellow-pane-configure-sitename").value = yellow.system.sitename; + document.getElementById("yellow-pane-configure-author").value = yellow.system.author; + document.getElementById("yellow-pane-configure-email").value = yellow.system.email; } break; case "yellow-pane-update": @@ -397,7 +397,9 @@ yellow.edit = { setTimeout("yellow.action('submit', '', 'action:update/option:check/');", 500); } if (paneStatus=="updates") { - document.getElementById("yellow-pane-update-status").innerHTML = "<a href=\"#\" data-action=\"submit\" data-arguments=\"action:update\">"+this.getText("UpdateStatusUpdates")+"</a>"; + document.getElementById(paneId+"-submit").innerHTML = this.getText("UpdateButton"); + document.getElementById(paneId+"-submit").setAttribute("data-action", "submit"); + document.getElementById(paneId+"-submit").setAttribute("data-arguments", "action:update"); } break; case "yellow-pane-create": @@ -417,8 +419,12 @@ yellow.edit = { this.updateToolbar(0, "yellow-toolbar-checked"); } if (!this.isUserAccess(paneAction, yellow.page.location) || (yellow.page.rawDataReadonly && paneId!="yellow-pane-create")) { - yellow.toolbox.setVisible(document.getElementById(paneId+"-submit"), false); document.getElementById(paneId+"-text").readOnly = true; + var elements = document.getElementsByClassName("yellow-toolbar-btn-icon"); + for (var i=0, l=elements.length; i<l; i++) { + yellow.toolbox.addClass(elements[i], "yellow-toolbar-disabled"); + } + yellow.toolbox.setVisible(document.getElementById(paneId+"-submit"), false); } } if (!document.getElementById(paneId+"-text").readOnly) { @@ -445,7 +451,7 @@ yellow.edit = { var paneHeight = yellow.toolbox.getWindowHeight() - paneTop - Math.min(yellow.toolbox.getOuterHeight(elementBar) + 10, (yellow.toolbox.getWindowWidth()-yellow.toolbox.getOuterWidth(elementBar))/2); switch (paneId) { case "yellow-pane-account": - case "yellow-pane-system": + case "yellow-pane-configure": yellow.toolbox.setOuterLeft(document.getElementById(paneId), paneLeft); yellow.toolbox.setOuterTop(document.getElementById(paneId), paneTop); yellow.toolbox.setOuterWidth(document.getElementById(paneId), paneWidth); @@ -508,7 +514,7 @@ yellow.edit = { if (!document.getElementById(paneId)) this.createPane(paneId, paneAction, paneStatus); var element = document.getElementById(paneId); if (!yellow.toolbox.isVisible(element)) { - if (yellow.system.debug) console.log("yellow.edit.showPane id:"+paneId); + if (yellow.system.coreDebugMode) console.log("yellow.edit.showPane id:"+paneId); yellow.toolbox.setVisible(element, true); if (paneModal) { yellow.toolbox.addClass(document.body, "yellow-body-modal-open"); @@ -530,7 +536,7 @@ yellow.edit = { hidePane: function(paneId, fadeout) { var element = document.getElementById(paneId); if (yellow.toolbox.isVisible(element)) { - if (yellow.system.debug) console.log("yellow.edit.hidePane id:"+paneId); + if (yellow.system.coreDebugMode) console.log("yellow.edit.hidePane id:"+paneId); yellow.toolbox.removeClass(document.body, "yellow-body-modal-open"); yellow.toolbox.removeValue("meta[name=viewport]", "content", ", maximum-scale=1, user-scalable=0"); yellow.toolbox.setVisible(element, false, fadeout); @@ -548,7 +554,7 @@ yellow.edit = { status = status ? status : "none"; arguments = arguments ? arguments : "none"; if (action!="none") { - if (yellow.system.debug) console.log("yellow.edit.processAction action:"+action+" status:"+status); + if (yellow.system.coreDebugMode) console.log("yellow.edit.processAction action:"+action+" status:"+status); var paneId = (status!="next" && status!="done") ? "yellow-pane-"+action : "yellow-pane-information"; switch(action) { case "login": this.showPane(paneId, action, status); break; @@ -563,24 +569,25 @@ yellow.edit = { case "quit": this.showPane(paneId, action, status); break; case "remove": this.showPane(paneId, action, status); break; case "account": this.showPane(paneId, action, status); break; - case "system": this.showPane(paneId, action, status); break; + case "configure": this.showPane(paneId, action, status); break; case "update": this.showPane(paneId, action, status); break; case "create": this.showPane(paneId, action, status, true); break; case "edit": this.showPane(paneId, action, status, true); break; case "delete": this.showPane(paneId, action, status, true); break; case "menu": this.showPane(paneId, action, status); break; - case "close": this.hidePane(this.paneId); break; case "toolbar": this.processToolbar(status, arguments); break; case "settings": this.processSettings(arguments); break; case "submit": this.processSubmit(arguments); break; + case "restore": this.processSubmit("action:"+action); break; case "help": this.processHelp(); break; + case "close": this.processClose(); break; } } }, // Process toolbar processToolbar: function(status, arguments) { - if (yellow.system.debug) console.log("yellow.edit.processToolbar status:"+status); + if (yellow.system.coreDebugMode) console.log("yellow.edit.processToolbar status:"+status); var elementText = document.getElementById(this.paneId+"-text"); var elementPreview = document.getElementById(this.paneId+"-preview"); if (!yellow.toolbox.isVisible(elementPreview) && !elementText.readOnly) { @@ -607,14 +614,16 @@ yellow.edit = { case "undo": yellow.editor.undo(); break; case "redo": yellow.editor.redo(); break; } + if (this.isExpandable(status)) { + this.showPopup("yellow-popup-"+status, status); + } else { + this.hidePopup(this.popupId); + } } - if (status=="preview" && !elementText.readOnly) this.showPreview(elementText, elementPreview); - if (status=="save" && !elementText.readOnly && this.paneAction!="delete") this.processSubmit("action:"+this.paneAction); - if (status=="help") window.open(this.getText("YellowHelpUrl"), "_blank"); - if (this.isExpandable(status)) { - this.showPopup("yellow-popup-"+status, status); - } else { - this.hidePopup(this.popupId); + if (!elementText.readOnly) { + if (status=="preview") this.showPreview(elementText, elementPreview); + if (status=="save" && this.paneAction!="delete") this.processSubmit("action:"+this.paneAction); + if (status=="help") window.open(this.getText("YellowHelpUrl"), "_blank"); } }, @@ -643,7 +652,7 @@ yellow.edit = { for (var i=0; i<tokens.length; i++) { var pair = tokens[i].split(" "); if (shortcut==pair[0] || shortcut.replace("meta+", "ctrl+")==pair[0]) { - if (yellow.system.debug) console.log("yellow.edit.processShortcut shortcut:"+shortcut); + if (yellow.system.coreDebugMode) console.log("yellow.edit.processShortcut shortcut:"+shortcut); e.stopPropagation(); e.preventDefault(); this.processToolbar(pair[1]); @@ -681,9 +690,21 @@ yellow.edit = { window.open(this.getText("YellowHelpUrl"), "_self"); }, + // Process close + processClose: function() { + this.hidePane(this.paneId); + if (yellow.page.action=="login") { + var url = yellow.system.coreServerScheme+"://"+ + yellow.system.coreServerAddress+ + yellow.system.coreServerBase+ + yellow.page.location; + window.open(url, "_self"); + } + }, + // Create popup createPopup: function(popupId) { - if (yellow.system.debug) console.log("yellow.edit.createPopup id:"+popupId); + if (yellow.system.coreDebugMode) console.log("yellow.edit.createPopup id:"+popupId); var elementPopup = document.createElement("div"); elementPopup.className = "yellow-popup"; elementPopup.setAttribute("id", popupId); @@ -719,25 +740,25 @@ yellow.edit = { "<li><a href=\"#\" id=\"yellow-popup-list-tl\" data-action=\"toolbar\" data-status=\"tl\">"+this.getText("ToolbarTl")+"</a></li>"+ "</ul>"; break; - case "yellow-popup-emojiawesome": + case "yellow-popup-emoji": var rawDataEmojis = ""; - if (yellow.system.emojiawesomeToolbarButtons && yellow.system.emojiawesomeToolbarButtons!="none") { - var tokens = yellow.system.emojiawesomeToolbarButtons.split(" "); + if (yellow.system.emojiToolbarButtons && yellow.system.emojiToolbarButtons!="none") { + var tokens = yellow.system.emojiToolbarButtons.split(" "); for (var i=0; i<tokens.length; i++) { var token = tokens[i].replace(/[\:]/g,""); var className = token.replace("+1", "plus1").replace("-1", "minus1").replace(/_/g, "-"); - rawDataEmojis += "<li><a href=\"#\" id=\"yellow-popup-list-"+yellow.toolbox.encodeHtml(token)+"\" data-action=\"toolbar\" data-status=\"text\" data-arguments=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"ea ea-"+yellow.toolbox.encodeHtml(className)+"\"></i></a></li>"; + rawDataEmojis += "<li><a href=\"#\" id=\"yellow-popup-list-"+yellow.toolbox.encodeHtml(token)+"\" data-action=\"toolbar\" data-status=\"text\" data-arguments=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"emoji emoji-"+yellow.toolbox.encodeHtml(className)+"\"></i></a></li>"; } } elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataEmojis+"</ul>"; break; - case "yellow-popup-fontawesome": + case "yellow-popup-icon": var rawDataIcons = ""; - if (yellow.system.fontawesomeToolbarButtons && yellow.system.fontawesomeToolbarButtons!="none") { - var tokens = yellow.system.fontawesomeToolbarButtons.split(" "); + if (yellow.system.iconToolbarButtons && yellow.system.iconToolbarButtons!="none") { + var tokens = yellow.system.iconToolbarButtons.split(" "); for (var i=0; i<tokens.length; i++) { var token = tokens[i].replace(/[\:]/g,""); - rawDataIcons += "<li><a href=\"#\" id=\"yellow-popup-list-"+yellow.toolbox.encodeHtml(token)+"\" data-action=\"toolbar\" data-status=\"text\" data-arguments=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"fa "+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>"; + rawDataIcons += "<li><a href=\"#\" id=\"yellow-popup-list-"+yellow.toolbox.encodeHtml(token)+"\" data-action=\"toolbar\" data-status=\"text\" data-arguments=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"icon "+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>"; } } elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataIcons+"</ul>"; @@ -754,7 +775,7 @@ yellow.edit = { this.hidePopup(this.popupId); if (!document.getElementById(popupId)) this.createPopup(popupId); var element = document.getElementById(popupId); - if (yellow.system.debug) console.log("yellow.edit.showPopup id:"+popupId); + if (yellow.system.coreDebugMode) console.log("yellow.edit.showPopup id:"+popupId); yellow.toolbox.setVisible(element, true); this.popupId = popupId; this.updateToolbar(status, "yellow-toolbar-selected"); @@ -772,7 +793,7 @@ yellow.edit = { hidePopup: function(popupId, fadeout) { var element = document.getElementById(popupId); if (yellow.toolbox.isVisible(element)) { - if (yellow.system.debug) console.log("yellow.edit.hidePopup id:"+popupId); + if (yellow.system.coreDebugMode) console.log("yellow.edit.hidePopup id:"+popupId); yellow.toolbox.setVisible(element, false, fadeout); this.popupId = 0; this.updateToolbar(0, "yellow-toolbar-selected"); @@ -805,7 +826,7 @@ yellow.edit = { if (showPreview) { this.updateToolbar("preview", "yellow-toolbar-checked"); elementPreview.innerHTML = responseText; - dispatchEvent(new Event("load")); + dispatchEvent(new Event("DOMContentLoaded")); } else { this.updateToolbar(0, "yellow-toolbar-checked"); elementText.focus(); @@ -829,7 +850,7 @@ yellow.edit = { var extension = (file.name.lastIndexOf(".")!=-1 ? file.name.substring(file.name.lastIndexOf("."), file.name.length) : "").toLowerCase(); var extensions = yellow.system.editUploadExtensions.split(/\s*,\s*/); if (file.size<=yellow.system.coreFileSizeMax && extensions.indexOf(extension)!=-1) { - var text = this.getText("UploadProgress")+"\u200b"; + var text = "["+this.getText("UploadProgress")+"]\u200b"; yellow.editor.setMarkdown(elementText, text, "insert"); var thisObject = this; var formData = new FormData(); @@ -855,7 +876,7 @@ yellow.edit = { uploadFileDone: function(elementText, responseText) { var result = JSON.parse(responseText); if (result) { - var textOld = this.getText("UploadProgress")+"\u200b"; + var textOld = "["+this.getText("UploadProgress")+"]\u200b"; var textNew; if (result.location.substring(0, yellow.system.coreImageLocation.length)==yellow.system.coreImageLocation) { textNew = "[image "+result.location.substring(yellow.system.coreImageLocation.length)+"]"; @@ -870,7 +891,7 @@ yellow.edit = { uploadFileError: function(elementText, responseText) { var result = JSON.parse(responseText); if (result) { - var textOld = this.getText("UploadProgress")+"\u200b"; + var textOld = "["+this.getText("UploadProgress")+"]\u200b"; var textNew = "["+result.error+"]"; yellow.editor.replace(elementText, textOld, textNew); } @@ -880,8 +901,11 @@ yellow.edit = { bindActions: function(element) { var elements = element.getElementsByTagName("a"); for (var i=0, l=elements.length; i<l; i++) { - if (elements[i].getAttribute("href") && elements[i].getAttribute("href").substring(0, 13)=="#data-action-") { - elements[i].setAttribute("data-action", elements[i].getAttribute("href").substring(13)); + if (elements[i].getAttribute("href") && elements[i].getAttribute("href").indexOf("#data-action-")!=-1) { + var position = elements[i].getAttribute("href").indexOf("#data-action-"); + var action = elements[i].getAttribute("href").substring(position+13); + var href = elements[i].getAttribute("href").substring(0, position); + if (href=="" || href==yellow.page.base+yellow.page.location) elements[i].setAttribute("data-action", action); } if (elements[i].getAttribute("data-action")) elements[i].onclick = yellow.onClickAction; if (elements[i].getAttribute("data-action")=="toolbar") elements[i].onmousedown = function(e) { e.preventDefault(); }; @@ -894,7 +918,7 @@ yellow.edit = { var paneAction = paneId.substring(panePrefix.length); if (paneAction=="edit") { if (document.getElementById("yellow-pane-edit-text").value.length==0) paneAction = "delete"; - if (yellow.page.statusCode==434) paneAction = "create"; + if (yellow.page.statusCode==434 || yellow.page.statusCode==435) paneAction = "create"; } return paneAction; }, @@ -931,7 +955,7 @@ yellow.edit = { rawDataLanguages += "<label for=\""+paneId+"-"+language+"\"><input type=\"radio\" name=\"language\" id=\""+paneId+"-"+language+"\" value=\""+language+"\""+checked+"> "+yellow.toolbox.encodeHtml(yellow.system.coreLanguages[language])+"</label><br />"; } } - return rawDataLanguages + return rawDataLanguages; }, // Return raw data for buttons @@ -954,14 +978,14 @@ yellow.edit = { return rawDataButtons; }, - // Return request string + // Return request data getRequest: function(key, prefix) { if (!prefix) prefix = "request"; key = prefix + yellow.toolbox.toUpperFirst(key); return (key in yellow.page) ? yellow.page[key] : ""; }, - // Return shortcut string + // Return shortcut setting getShortcut: function(key) { var shortcut = ""; var tokens = yellow.system.editKeyboardShortcuts.split(/\s*,\s*/); @@ -972,7 +996,7 @@ yellow.edit = { break; } } - var labels = yellow.text.editKeyboardLabels.split(/\s*,\s*/); + var labels = yellow.language.editKeyboardLabels.split(/\s*,\s*/); if (navigator.platform.indexOf("Mac")==-1) { shortcut = shortcut.toUpperCase().replace("CTRL+", labels[0]).replace("ALT+", labels[1]).replace("SHIFT+", labels[2]); } else { @@ -982,12 +1006,12 @@ yellow.edit = { return shortcut; }, - // Return text string + // Return text setting getText: function(key, prefix, postfix) { if (!prefix) prefix = "edit"; if (!postfix) postfix = ""; key = prefix + yellow.toolbox.toUpperFirst(key) + yellow.toolbox.toUpperFirst(postfix); - return (key in yellow.text) ? yellow.text[key] : "["+key+"]"; + return (key in yellow.language) ? yellow.language[key] : "["+key+"]"; }, // Return browser cookie @@ -997,13 +1021,13 @@ yellow.edit = { // Check if user with access isUserAccess: function(action, location) { - var tokens = yellow.system.userAccess.split(/\s*,\s*/); - return tokens.indexOf(action)!=-1 && (!location || location.substring(0, yellow.system.userHome.length)==yellow.system.userHome); + var tokens = yellow.user.access.split(/\s*,\s*/); + return tokens.indexOf(action)!=-1 && (!location || location.substring(0, yellow.user.home.length)==yellow.user.home); }, // Check if element is expandable isExpandable: function(name) { - return (name=="format" || name=="heading" || name=="list" || name=="emojiawesome" || name=="fontawesome"); + return (name=="format" || name=="heading" || name=="list" || name=="emoji" || name=="icon"); }, // Check if extension exists @@ -1075,7 +1099,7 @@ yellow.editor = { element.value = textSelectionBefore + textSelectionNew + textSelectionAfter; element.setSelectionRange(selectionStartNew, selectionEndNew); } - if (yellow.system.debug) console.log("yellow.editor.setMarkdown type:"+information.type); + if (yellow.system.coreDebugMode) console.log("yellow.editor.setMarkdown type:"+information.type); }, // Return Markdown formatting information @@ -1209,7 +1233,7 @@ yellow.editor = { element.value = textSelectionBefore + textSelectionNew + textSelectionAfter; element.setSelectionRange(selectionStartNew, selectionEndNew); element.scrollTop = 0; - if (yellow.system.debug) console.log("yellow.editor.setMetaData key:"+key); + if (yellow.system.coreDebugMode) console.log("yellow.editor.setMetaData key:"+key); } }, diff --git a/system/extensions/edit.php b/system/extensions/edit.php @@ -1,69 +1,114 @@ <?php -// Edit extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/edit -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. +// Edit extension, https://github.com/annaesvensson/yellow-edit class YellowEdit { - const VERSION = "0.8.27"; - const TYPE = "feature"; - public $yellow; //access to API - public $response; //web response - public $users; //user accounts - public $merge; //text merge + const VERSION = "0.8.77"; + public $yellow; // access to API + public $response; // web response + public $merge; // text merge + public $editable; // page can be edited? (boolean) // Handle initialisation public function onLoad($yellow) { $this->yellow = $yellow; $this->response = new YellowEditResponse($yellow); - $this->users = new YellowEditUsers($yellow); $this->merge = new YellowEditMerge($yellow); + $this->yellow->system->setDefault("editSiteEmail", "noreply"); $this->yellow->system->setDefault("editLocation", "/edit/"); $this->yellow->system->setDefault("editUploadNewLocation", "/media/@group/@filename"); - $this->yellow->system->setDefault("editUploadExtensions", ".gif, .jpg, .pdf, .png, .svg, .zip"); + $this->yellow->system->setDefault("editUploadExtensions", ".gif, .jpg, .mp3, .ogg, .pdf, .png, .svg, .zip"); $this->yellow->system->setDefault("editKeyboardShortcuts", "ctrl+b bold, ctrl+i italic, ctrl+k strikethrough, ctrl+e code, ctrl+s save, ctrl+alt+p preview"); $this->yellow->system->setDefault("editToolbarButtons", "auto"); $this->yellow->system->setDefault("editEndOfLine", "auto"); $this->yellow->system->setDefault("editNewFile", "page-new-(.*).md"); - $this->yellow->system->setDefault("editUserFile", "user.ini"); $this->yellow->system->setDefault("editUserPasswordMinLength", "8"); $this->yellow->system->setDefault("editUserHashAlgorithm", "bcrypt"); $this->yellow->system->setDefault("editUserHashCost", "10"); + $this->yellow->system->setDefault("editUserAccess", "create, edit, delete, restore, upload"); $this->yellow->system->setDefault("editUserHome", "/"); - $this->yellow->system->setDefault("editUserAccess", "create, edit, delete, upload"); $this->yellow->system->setDefault("editLoginRestriction", "0"); $this->yellow->system->setDefault("editLoginSessionTimeout", "2592000"); $this->yellow->system->setDefault("editBruteForceProtection", "25"); - $this->users->load($this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile")); + } + + // Handle update + public function onUpdate($action) { + if ($action=="clean" || $action=="daily") { + $cleanup = false; + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $fileData = $this->yellow->toolbox->readFile($fileNameUser); + $fileDataNew = ""; + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])=="email" && !is_string_empty($matches[2])) { + $status = $this->yellow->user->getUser("status", $matches[2]); + $reserved = strtotime($this->yellow->user->getUser("modified", $matches[2])) + 60*60*24; + $cleanup = $status!="active" && $status!="inactive" && $reserved<=time(); + } + } + if (!$cleanup) $fileDataNew .= $line; + } + $fileDataNew = rtrim($fileDataNew)."\n"; + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileNameUser, $fileDataNew)) { + $this->yellow->toolbox->log("error", "Can't write file '$fileNameUser'!"); + } + } } // Handle request public function onRequest($scheme, $address, $base, $location, $fileName) { $statusCode = 0; - if ($this->checkRequest($location)) { + if ($this->isEditLocation($location)) { + $this->editable = true; $scheme = $this->yellow->system->get("coreServerScheme"); $address = $this->yellow->system->get("coreServerAddress"); $base = rtrim($this->yellow->system->get("coreServerBase").$this->yellow->system->get("editLocation"), "/"); - list($scheme, $address, $base, $location, $fileName) = $this->yellow->getRequestInformation($scheme, $address, $base); - $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName); + list($scheme, $address, $base, $location, $fileName) = $this->yellow->lookup->getRequestInformation($scheme, $address, $base); + $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName); } return $statusCode; } + // Handle command + public function onCommand($command, $text) { + switch ($command) { + case "user": $statusCode = $this->processCommandUser($command, $text); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + return "user [option email password]"; + } + + // Handle page meta data + public function onParseMetaData($page) { + $page->set("editPageUrl", $this->yellow->lookup->normaliseUrl( + $this->yellow->system->get("coreServerScheme"), + $this->yellow->system->get("coreServerAddress"), + $this->yellow->system->get("coreServerBase"), + rtrim($this->yellow->system->get("editLocation"), "/").$page->location)); + } + // Handle page content of shortcut public function onParseContentShortcut($page, $name, $text, $type) { $output = null; if ($name=="edit" && $type=="inline") { - $editText = "$name $text"; - if (substru($text, 0, 2)=="- ") $editText = trim(substru($text, 2)); - $output = "<a href=\"".$page->get("pageEdit")."\">".htmlspecialchars($editText)."</a>"; + list($target, $description) = $this->yellow->toolbox->getTextList($text, " ", 2); + if (is_string_empty($target) || $target=="-") $target = "main"; + if (is_string_empty($description)) $description = ucfirst($name); + $pageTarget = $target=="main" ? $page->getPage("main") : $page->getPage("main")->getPage($target); + $output = "<a href=\"".$pageTarget->get("editPageUrl")."\">".htmlspecialchars($description)."</a>"; } return $output; } // Handle page layout public function onParsePageLayout($page, $name) { - if ($this->response->isActive()) { + if ($this->editable) { $this->response->processPageData($page); } } @@ -71,7 +116,7 @@ class YellowEdit { // Handle page extra data public function onParsePageExtra($page, $name) { $output = null; - if ($name=="header" && $this->response->isActive()) { + if ($this->editable && $name=="header") { $extensionLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreExtensionLocation"); $output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"{$extensionLocation}edit.css\" />\n"; $output .= "<script type=\"text/javascript\" src=\"{$extensionLocation}edit.js\"></script>\n"; @@ -79,50 +124,14 @@ class YellowEdit { $output .= "// <![CDATA[\n"; $output .= "yellow.page = ".json_encode($this->response->getPageData($page)).";\n"; $output .= "yellow.system = ".json_encode($this->response->getSystemData()).";\n"; - $output .= "yellow.text = ".json_encode($this->response->getTextData()).";\n"; + $output .= "yellow.user = ".json_encode($this->response->getUserData()).";\n"; + $output .= "yellow.language = ".json_encode($this->response->getLanguageData()).";\n"; $output .= "// ]]>\n"; $output .= "</script>\n"; } return $output; } - // Handle command - public function onCommand($command, $text) { - switch ($command) { - case "user": $statusCode = $this->processCommandUser($command, $text); break; - default: $statusCode = 0; - } - return $statusCode; - } - - // Handle command help - public function onCommandHelp() { - return "user [option email password name]\n"; - } - - // Handle update - public function onUpdate($action) { - if ($action=="update") { - $cleanup = false; - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); - $fileData = $this->yellow->toolbox->readFile($fileNameUser); - $fileDataNew = ""; - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])=="email" && !strempty($matches[2])) { - $status = $this->users->getUser($matches[2], "status"); - $cleanup = !empty($status) && $status!="active" && $status!="inactive"; - } - } - if (!$cleanup) $fileDataNew .= $line; - } - $fileDataNew = rtrim($fileDataNew)."\n"; - if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileNameUser, $fileDataNew)) { - $this->yellow->log("error", "Can't write file '$fileNameUser'!"); - } - } - } - // Process command to update user account public function processCommandUser($command, $text) { list($option) = $this->yellow->toolbox->getTextArguments($text); @@ -138,21 +147,23 @@ class YellowEdit { // Show user accounts public function userShow($command, $text) { - foreach ($this->users->getData() as $line) { - echo "$line\n"; + $data = array(); + foreach ($this->yellow->user->settings as $key=>$value) { + $data[$key] = "$value[email] - User account by $value[name]."; } - if (!$this->users->getNumber()) echo "Yellow $command: No user accounts\n"; + uksort($data, "strnatcasecmp"); + foreach ($data as $line) echo "$line\n"; + if (is_array_empty($data)) echo "Yellow $command: No user accounts\n"; return 200; } // Add user account public function userAdd($command, $text) { $status = "ok"; - list($option, $email, $password, $name) = $this->yellow->toolbox->getTextArguments($text); - if (empty($email) || empty($password)) $status = $this->response->status = "incomplete"; - if (empty($name)) $name = $this->yellow->system->get("sitename"); - if ($status=="ok") $status = $this->getUserAccount($email, $password, "add"); - if ($status=="ok" && $this->users->isTaken($email)) $status = "taken"; + list($option, $email, $password) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($email) || is_string_empty($password)) $status = $this->response->status = "incomplete"; + if ($status=="ok") $status = $this->getUserAccount("add", $email, $password); + if ($status=="ok" && $this->isUserAccountTaken($email)) $status = "taken"; switch ($status) { case "incomplete": echo "ERROR updating settings: Please enter email and password!\n"; break; case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; @@ -161,25 +172,28 @@ class YellowEdit { case "short": echo "ERROR updating settings: Please enter a longer password!\n"; break; } if ($status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $name = $this->yellow->system->get("sitename"); + $userLanguage = $this->yellow->system->get("language"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array( "name" => $name, - "language" => $this->yellow->system->get("language"), - "home" => $this->yellow->system->get("editUserHome"), + "description" => $this->yellow->language->getText("editUserDescription", $userLanguage), + "language" => $userLanguage, "access" => $this->yellow->system->get("editUserAccess"), - "hash" => $this->users->createHash($password), - "stamp" => $this->users->createStamp(), + "home" => $this->yellow->system->get("editUserHome"), + "hash" => $this->response->createHash($password), + "stamp" => $this->response->createStamp(), "pending" => "none", "failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "active"); - $status = $this->users->save($fileNameUser, $email, $settings) ? "ok" : "error"; + $status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; - $this->yellow->log($status=="ok" ? "info" : "error", "Add user '".strtok($name, " ")."'"); + $this->yellow->toolbox->log($status=="ok" ? "info" : "error", "Add user '".strtok($name, " ")."'"); } if ($status=="ok") { $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); - $status = substru($this->users->getUser($email, "hash"), 0, 10)!="error-hash" ? "ok" : "error"; + $status = substru($this->yellow->user->getUser("hash", $email), 0, 10)!="error-hash" ? "ok" : "error"; if ($status=="error") echo "ERROR updating settings: Hash algorithm '$algorithm' not supported!\n"; } $statusCode = $status=="ok" ? 200 : 500; @@ -190,10 +204,10 @@ class YellowEdit { // Change user account public function userChange($command, $text) { $status = "ok"; - list($option, $email, $password, $name) = $this->yellow->toolbox->getTextArguments($text); - if (empty($email)) $status = $this->response->status = "invalid"; - if ($status=="ok") $status = $this->getUserAccount($email, $password, "change"); - if ($status=="ok" && !$this->users->isExisting($email)) $status = "unknown"; + list($option, $email, $password) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($email)) $status = $this->response->status = "invalid"; + if ($status=="ok") $status = $this->getUserAccount("change", $email, $password); + if ($status=="ok" && !$this->yellow->user->isExisting($email)) $status = "unknown"; switch ($status) { case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; case "unknown": echo "ERROR updating settings: Can't find email '$email'!\n"; break; @@ -201,13 +215,12 @@ class YellowEdit { case "short": echo "ERROR updating settings: Please enter a longer password!\n"; break; } if ($status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array( - "name" => empty($name) ? $this->users->getUser($email, "name") : $name, - "hash" => empty($password) ? $this->users->getUser($email, "hash") : $this->users->createHash($password), + "hash" => is_string_empty($password) ? $this->yellow->user->getUser("hash", $email) : $this->response->createHash($password), "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); - $status = $this->users->save($fileNameUser, $email, $settings) ? "ok" : "error"; + $status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; } $statusCode = $status=="ok" ? 200 : 500; @@ -219,20 +232,19 @@ class YellowEdit { public function userRemove($command, $text) { $status = "ok"; list($option, $email) = $this->yellow->toolbox->getTextArguments($text); - $name = $this->users->getUser($email, "name"); - if (empty($email)) $status = $this->response->status = "invalid"; - if (empty($name)) $name = $this->yellow->system->get("sitename"); - if ($status=="ok") $status = $this->getUserAccount($email, "", "remove"); - if ($status=="ok" && !$this->users->isExisting($email)) $status = "unknown"; + if (is_string_empty($email)) $status = $this->response->status = "invalid"; + if ($status=="ok") $status = $this->getUserAccount("remove", $email, ""); + if ($status=="ok" && !$this->yellow->user->isExisting($email)) $status = "unknown"; switch ($status) { case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; case "unknown": echo "ERROR updating settings: Can't find email '$email'!\n"; break; } if ($status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); - $status = $this->users->remove($fileNameUser, $email) ? "ok" : "error"; + $name = $this->yellow->user->getUser("name", $email); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $status = $this->yellow->user->remove($fileNameUser, $email) ? "ok" : "error"; if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; - $this->yellow->log($status=="ok" ? "info" : "error", "Remove user '".strtok($name, " ")."'"); + $this->yellow->toolbox->log($status=="ok" ? "info" : "error", "Remove user '".strtok($name, " ")."'"); } $statusCode = $status=="ok" ? 200 : 500; echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."removed\n"; @@ -249,11 +261,12 @@ class YellowEdit { case "logout": $statusCode = $this->processRequestLogout($scheme, $address, $base, $location, $fileName); break; case "quit": $statusCode = $this->processRequestQuit($scheme, $address, $base, $location, $fileName); break; case "account": $statusCode = $this->processRequestAccount($scheme, $address, $base, $location, $fileName); break; - case "system": $statusCode = $this->processRequestSystem($scheme, $address, $base, $location, $fileName); break; + case "configure": $statusCode = $this->processRequestConfigure($scheme, $address, $base, $location, $fileName); break; case "update": $statusCode = $this->processRequestUpdate($scheme, $address, $base, $location, $fileName); break; case "create": $statusCode = $this->processRequestCreate($scheme, $address, $base, $location, $fileName); break; case "edit": $statusCode = $this->processRequestEdit($scheme, $address, $base, $location, $fileName); break; case "delete": $statusCode = $this->processRequestDelete($scheme, $address, $base, $location, $fileName); break; + case "restore": $statusCode = $this->processRequestRestore($scheme, $address, $base, $location, $fileName); break; case "preview": $statusCode = $this->processRequestPreview($scheme, $address, $base, $location, $fileName); break; case "upload": $statusCode = $this->processRequestUpload($scheme, $address, $base, $location, $fileName); break; } @@ -284,11 +297,16 @@ class YellowEdit { $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); } else { if ($this->yellow->lookup->isRedirectLocation($location)) { - $location = $this->yellow->lookup->isFileLocation($location) ? "$location/" : "/".$this->yellow->getRequestLanguage()."/"; + $location = $this->yellow->lookup->getRedirectLocation($location); $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); $statusCode = $this->yellow->sendStatus(301, $location); } else { - $this->yellow->page->error($this->response->isUserAccess("create", $location) ? 434 : 404); + $statusCode = 404; + if ($this->response->isUserAccess("create", $location)) $statusCode = 434; + if ($this->response->isUserAccess("restore", $location) && $this->response->isDeletedLocation($location)) { + $statusCode = 435; + } + $this->yellow->page->error($statusCode); $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); } } @@ -297,10 +315,10 @@ class YellowEdit { // Process request for user login public function processRequestLogin($scheme, $address, $base, $location, $fileName) { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time())); - if ($this->users->save($fileNameUser, $this->response->userEmail, $settings)) { - $home = $this->users->getUser($this->response->userEmail, "home"); + if ($this->yellow->user->save($fileNameUser, $this->response->userEmail, $settings)) { + $home = $this->yellow->user->getUser("home", $this->response->userEmail); if (substru($location, 0, strlenu($home))==$home) { $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); $statusCode = $this->yellow->sendStatus(303, $location); @@ -336,29 +354,31 @@ class YellowEdit { $email = trim($this->yellow->page->getRequest("email")); $password = trim($this->yellow->page->getRequest("password")); $consent = trim($this->yellow->page->getRequest("consent")); - if (empty($name) || empty($email) || empty($password) || empty($consent)) $this->response->status = "incomplete"; - if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, $password, $this->response->action); + if (is_string_empty($name) || is_string_empty($email) || is_string_empty($password) || is_string_empty($consent)) $this->response->status = "incomplete"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); if ($this->response->status=="ok" && $this->response->isLoginRestriction()) $this->response->status = "next"; - if ($this->response->status=="ok" && $this->users->isTaken($email)) $this->response->status = "next"; + if ($this->response->status=="ok" && $this->isUserAccountTaken($email)) $this->response->status = "next"; if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $userLanguage = $this->yellow->lookup->findContentLanguage($fileName, $this->yellow->system->get("language")); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array( "name" => $name, - "language" => $this->yellow->lookup->findLanguageFromFile($fileName, $this->yellow->system->get("language")), - "home" => $this->yellow->system->get("editUserHome"), + "description" => $this->yellow->language->getText("editUserDescription", $userLanguage), + "language" => $userLanguage, "access" => $this->yellow->system->get("editUserAccess"), - "hash" => $this->users->createHash($password), - "stamp" => $this->users->createStamp(), + "home" => $this->yellow->system->get("editUserHome"), + "hash" => $this->response->createHash($password), + "stamp" => $this->response->createStamp(), "pending" => "none", "failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "unconfirmed"); - $this->response->status = $this->users->save($fileNameUser, $email, $settings) ? "ok" : "error"; + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } if ($this->response->status=="ok") { $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); - $this->response->status = substru($this->users->getUser($email, "hash"), 0, 10)!="error-hash" ? "ok" : "error"; + $this->response->status = substru($this->yellow->user->getUser("hash", $email), 0, 10)!="error-hash" ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Hash algorithm '$algorithm' not supported!"); } if ($this->response->status=="ok") { @@ -376,9 +396,9 @@ class YellowEdit { $email = $this->yellow->page->getRequest("email"); $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "unapproved"); - $this->response->status = $this->users->save($fileNameUser, $email, $settings) ? "ok" : "error"; + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } if ($this->response->status=="ok") { @@ -396,11 +416,12 @@ class YellowEdit { $email = $this->yellow->page->getRequest("email"); $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $name = $this->yellow->user->getUser("name", $email); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "active"); - $this->response->status = $this->users->save($fileNameUser, $email, $settings) ? "ok" : "error"; + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - $this->yellow->log($this->response->status=="ok" ? "info" : "error", "Add user '".strtok($this->users->getUser($email, "name"), " ")."'"); + $this->yellow->toolbox->log($this->response->status=="ok" ? "info" : "error", "Add user '".strtok($name, " ")."'"); } if ($this->response->status=="ok") { $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "welcome") ? "done" : "error"; @@ -416,7 +437,7 @@ class YellowEdit { $this->response->status = "ok"; $email = trim($this->yellow->page->getRequest("email")); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid"; - if ($this->response->status=="ok" && !$this->users->isExisting($email)) $this->response->status = "next"; + if ($this->response->status=="ok" && !$this->yellow->user->isExisting($email)) $this->response->status = "next"; if ($this->response->status=="ok") { $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "recover") ? "next" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); @@ -433,12 +454,12 @@ class YellowEdit { $password = trim($this->yellow->page->getRequest("password")); $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); if ($this->response->status=="ok") { - if (empty($password)) $this->response->status = "password"; - if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, $password, $this->response->action); + if (is_string_empty($password)) $this->response->status = "password"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); - $settings = array("hash" => $this->users->createHash($password), "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); - $this->response->status = $this->users->save($fileNameUser, $email, $settings) ? "ok" : "error"; + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("hash" => $this->response->createHash($password), "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } if ($this->response->status=="ok") { @@ -457,9 +478,9 @@ class YellowEdit { $email = $this->yellow->page->getRequest("email"); $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "active"); - $this->response->status = $this->users->save($fileNameUser, $email, $settings) ? "done" : "error"; + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "done" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); @@ -473,13 +494,13 @@ class YellowEdit { $email = $emailSource = $this->yellow->page->getRequest("email"); $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); if ($this->response->status=="ok") { - $emailSource = $this->users->getUser($email, "pending"); - if ($this->users->getUser($emailSource, "status")!="active") $this->response->status = "done"; + $emailSource = $this->yellow->user->getUser("pending", $email); + if ($this->yellow->user->getUser("status", $emailSource)!="active") $this->response->status = "done"; } if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "unchanged"); - $this->response->status = $this->users->save($fileNameUser, $email, $settings) ? "ok" : "error"; + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } if ($this->response->status=="ok") { @@ -497,23 +518,23 @@ class YellowEdit { $email = $emailSource = trim($this->yellow->page->getRequest("email")); $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); if ($this->response->status=="ok") { - list($email, $hash) = $this->yellow->toolbox->getTextList($this->users->getUser($email, "pending"), ":", 2); - if (!$this->users->isExisting($email) || empty($hash)) $this->response->status = "done"; + list($email, $hash) = $this->yellow->toolbox->getTextList($this->yellow->user->getUser("pending", $email), ":", 2); + if (!$this->yellow->user->isExisting($email) || is_string_empty($hash)) $this->response->status = "done"; } if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array( "hash" => $hash, "pending" => "none", "failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "active"); - $this->response->status = $this->users->save($fileNameUser, $email, $settings) ? "ok" : "error"; + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } if ($this->response->status=="ok" && $email!=$emailSource) { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); - $this->response->status = $this->users->remove($fileNameUser, $emailSource) ? "ok" : "error"; + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $this->response->status = $this->yellow->user->remove($fileNameUser, $emailSource) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } if ($this->response->status=="ok") { @@ -530,9 +551,9 @@ class YellowEdit { $this->response->status = "ok"; $name = trim($this->yellow->page->getRequest("name")); $email = $this->response->userEmail; - if (empty($name)) $this->response->status = "none"; - if ($this->response->status=="ok" && $name!=$this->users->getUser($email, "name")) $this->response->status = "mismatch"; - if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, "", $this->response->action); + if (is_string_empty($name)) $this->response->status = "none"; + if ($this->response->status=="ok" && $name!=$this->yellow->user->getUser("name", $email)) $this->response->status = "mismatch"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, ""); if ($this->response->status=="ok") { $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "remove") ? "next" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); @@ -548,19 +569,20 @@ class YellowEdit { $email = $this->yellow->page->getRequest("email"); $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $name = $this->yellow->user->getUser("name", $email); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "removed"); - $this->response->status = $this->users->save($fileNameUser, $email, $settings) ? "ok" : "error"; + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - $this->yellow->log($this->response->status=="ok" ? "info" : "error", "Remove user '".strtok($this->users->getUser($email, "name"), " ")."'"); + $this->yellow->toolbox->log($this->response->status=="ok" ? "info" : "error", "Remove user '".strtok($name, " ")."'"); } if ($this->response->status=="ok") { $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "goodbye") ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); } if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); - $this->response->status = $this->users->remove($fileNameUser, $email) ? "ok" : "error"; + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $this->response->status = $this->yellow->user->remove($fileNameUser, $email) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } if ($this->response->status=="ok") { @@ -580,35 +602,36 @@ class YellowEdit { $password = trim($this->yellow->page->getRequest("password")); $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $this->yellow->page->getRequest("name"))); $language = trim($this->yellow->page->getRequest("language")); - if ($email!=$emailSource || !empty($password)) { - if (empty($email)) $this->response->status = "invalid"; - if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, $password, $this->response->action); - if ($this->response->status=="ok" && $email!=$emailSource && $this->users->isTaken($email)) $this->response->status = "taken"; + if ($email!=$emailSource || !is_string_empty($password)) { + if (is_string_empty($email)) $this->response->status = "invalid"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); + if ($this->response->status=="ok" && $email!=$emailSource && $this->isUserAccountTaken($email)) $this->response->status = "taken"; if ($this->response->status=="ok" && $email!=$emailSource) { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array( "name" => $name, + "description" => $this->yellow->user->getUser("description", $emailSource), "language" => $language, - "home" => $this->users->getUser($emailSource, "home"), - "access" => $this->users->getUser($emailSource, "access"), - "hash" => $this->users->createHash("none"), - "stamp" => $this->users->createStamp(), + "access" => $this->yellow->user->getUser("access", $emailSource), + "home" => $this->yellow->user->getUser("home", $emailSource), + "hash" => $this->response->createHash("none"), + "stamp" => $this->response->createStamp(), "pending" => $emailSource, "failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "unverified"); - $this->response->status = $this->users->save($fileNameUser, $email, $settings) ? "ok" : "error"; + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array( "name" => $name, "language" => $language, - "pending" => $email.":".(empty($password) ? $this->users->getUser($emailSource, "hash") : $this->users->createHash($password)), + "pending" => $email.":".(is_string_empty($password) ? $this->yellow->user->getUser("hash", $emailSource) : $this->response->createHash($password)), "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); - $this->response->status = $this->users->save($fileNameUser, $emailSource, $settings) ? "ok" : "error"; + $this->response->status = $this->yellow->user->save($fileNameUser, $emailSource, $settings) ? "ok" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } if ($this->response->status=="ok") { @@ -618,9 +641,9 @@ class YellowEdit { } } else { if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); $settings = array("name" => $name, "language" => $language, "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); - $this->response->status = $this->users->save($fileNameUser, $email, $settings) ? "done" : "error"; + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "done" : "error"; if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } } @@ -633,24 +656,24 @@ class YellowEdit { return $statusCode; } - // Process request to change system settings - public function processRequestSystem($scheme, $address, $base, $location, $fileName) { + // Process request to change settings + public function processRequestConfigure($scheme, $address, $base, $location, $fileName) { $statusCode = 0; - if ($this->response->isUserAccess("system")) { - $this->response->action = "system"; + if ($this->response->isUserAccess("configure")) { + $this->response->action = "configure"; $this->response->status = "ok"; $sitename = trim($this->yellow->page->getRequest("sitename")); $author = trim($this->yellow->page->getRequest("author")); $email = trim($this->yellow->page->getRequest("email")); if ($email!=$this->yellow->system->get("email")) { - if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid"; + if (is_string_empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid"; } if ($this->response->status=="ok") { - $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + $fileNameSystem = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); $settings = array("sitename" => $sitename, "author" => $author, "email" => $email); - $file = $this->response->getFileSystem($scheme, $address, $base, $location, $fileName, $settings); - $this->response->status = (!$file->isError() && $this->yellow->system->save($fileName, $settings)) ? "done" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileName'!"); + $file = $this->response->getFileSystem($scheme, $address, $base, $location, $fileNameSystem, $settings); + $this->response->status = (!$file->isError() && $this->yellow->system->save($fileNameSystem, $settings)) ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameSystem'!"); } if ($this->response->status=="done") { $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); @@ -668,18 +691,16 @@ class YellowEdit { if ($this->response->isUserAccess("update")) { $this->response->action = "update"; $this->response->status = "ok"; - $extension = trim($this->yellow->page->getRequest("extension")); - $option = trim($this->yellow->page->getRequest("option")); - if ($option=="check") { - list($statusCode, $updates, $rawData) = $this->response->getUpdateInformation(); - $this->response->status = $updates ? "updates" : "ok"; + if ($this->yellow->page->getRequest("option")=="check") { + list($statusCode, $rawData) = $this->response->getUpdateInformation(); + $this->response->status = is_string_empty($rawData) ? "ok" : "updates"; $this->response->rawDataOutput = $rawData; if ($statusCode!=200) { $this->response->status = "error"; $this->response->rawDataOutput = ""; } } else { - $this->response->status = $this->yellow->command("update $extension $option")==0 ? "done" : "error"; + $this->response->status = $this->yellow->command("update all")==0 ? "done" : "error"; } if ($this->response->status=="done") { $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); @@ -694,7 +715,7 @@ class YellowEdit { // Process request to create page public function processRequestCreate($scheme, $address, $base, $location, $fileName) { $statusCode = 0; - if ($this->response->isUserAccess("create", $location) && !empty($this->yellow->page->getRequest("rawdataedit"))) { + if ($this->response->isUserAccess("create", $location) && !is_string_empty($this->yellow->page->getRequest("rawdataedit"))) { $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdatasource"); $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("rawdataendofline"); @@ -710,7 +731,7 @@ class YellowEdit { $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); } } else { - $this->yellow->page->error(500, $page->get("pageError")); + $this->yellow->page->error(500, $page->errorMessage); $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); } } @@ -720,7 +741,7 @@ class YellowEdit { // Process request to edit page public function processRequestEdit($scheme, $address, $base, $location, $fileName) { $statusCode = 0; - if ($this->response->isUserAccess("edit", $location) && !empty($this->yellow->page->getRequest("rawdataedit"))) { + if ($this->response->isUserAccess("edit", $location) && !is_string_empty($this->yellow->page->getRequest("rawdataedit"))) { $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdataedit"); $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("rawdataendofline"); @@ -729,26 +750,21 @@ class YellowEdit { $this->response->rawDataSource, $this->response->rawDataEdit, $rawDataFile, $this->response->rawDataEndOfLine); if (!$page->isError()) { if ($this->yellow->lookup->isFileLocation($location)) { - if ($this->yellow->toolbox->renameFile($fileName, $page->fileName, true) && - $this->yellow->toolbox->createFile($page->fileName, $page->rawData)) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $this->yellow->page->error(500, "Can't write file '$page->fileName'!"); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } + $ok = $this->yellow->toolbox->renameFile($fileName, $page->fileName, true) && + $this->yellow->toolbox->createFile($page->fileName, $page->rawData); } else { - if ($this->yellow->toolbox->renameDirectory(dirname($fileName), dirname($page->fileName), true) && - $this->yellow->toolbox->createFile($page->fileName, $page->rawData)) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $this->yellow->page->error(500, "Can't write file '$page->fileName'!"); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } + $ok = $this->yellow->toolbox->renameDirectory(dirname($fileName), dirname($page->fileName), true) && + $this->yellow->toolbox->createFile($page->fileName, $page->rawData); + } + if ($ok) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't write file '$page->fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); } } else { - $this->yellow->page->error(500, $page->get("pageError")); + $this->yellow->page->error(500, $page->errorMessage); $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); } } @@ -767,39 +783,61 @@ class YellowEdit { $rawDataFile, $this->response->rawDataEndOfLine); if (!$page->isError()) { if ($this->yellow->lookup->isFileLocation($location)) { - if ($this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $this->yellow->page->error(500, "Can't delete file '$fileName'!"); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } + $ok = $this->response->deleteFileLocation($location, $fileName); } else { - if ($this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->system->get("coreTrashDirectory"))) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $this->yellow->page->error(500, "Can't delete file '$fileName'!"); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } + $ok = $this->response->deleteDirectoryLocation($location, $fileName); + } + if ($ok) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't delete file '$fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); } } else { - $this->yellow->page->error(500, $page->get("pageError")); + $this->yellow->page->error(500, $page->errorMessage); $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); } } return $statusCode; } + // Process request to restore deleted page + public function processRequestRestore($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->response->isUserAccess("restore", $location) && !is_file($fileName)) { + $page = $this->response->getPageRestore($scheme, $address, $base, $location, $fileName); + if (!$page->isError()) { + if ($this->yellow->lookup->isFileLocation($location)) { + $ok = $this->response->restoreFileLocation($location); + } else { + $ok = $this->response->restoreDirectoryLocation($location); + } + if ($ok) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't restore file '$fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } else { + $this->yellow->page->error(500, $page->errorMessage); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + // Process request to show preview public function processRequestPreview($scheme, $address, $base, $location, $fileName) { $page = $this->response->getPagePreview($scheme, $address, $base, $location, $fileName, $this->yellow->page->getRequest("rawdataedit"), $this->yellow->page->getRequest("rawdataendofline")); - $statusCode = $this->yellow->sendData(200, $page->outputData, "", false); - if (defined("DEBUG") && DEBUG>=1) { - $parser = $page->get("parser"); - echo "YellowEdit::processRequestPreview parser:$parser<br/>\n"; - } + $page->headerData = array( + "Cache-Control"=>"no-cache, no-store", + "Content-Type"=>$this->yellow->toolbox->getMimeContentType("a.html"), + "Last-Modified"=>$this->yellow->toolbox->getHttpDateFormatted(time())); + $statusCode = $this->yellow->sendData($page->statusCode, $page->headerData, $page->outputData); + if ($this->yellow->system->get("coreDebugMode")>=1) echo "YellowEdit::processRequestPreview file:$fileName<br/>\n"; return $statusCode; } @@ -822,15 +860,11 @@ class YellowEdit { } else { $data["error"] = "Can't write file '$fileNameShort'!"; } - $statusCode = $this->yellow->sendData(isset($data["error"]) ? 500 : 200, json_encode($data), "a.json", false); - return $statusCode; - } - - // Check request - public function checkRequest($location) { - $locationLength = strlenu($this->yellow->system->get("editLocation")); - $this->response->active = substru($location, 0, $locationLength)==$this->yellow->system->get("editLocation"); - return $this->response->isActive(); + $headerData = array( + "Cache-Control"=>"no-cache, no-store", + "Content-Type"=>$this->yellow->toolbox->getMimeContentType("a.json"), + "Last-Modified"=>$this->yellow->toolbox->getHttpDateFormatted(time())); + return $this->yellow->sendData(isset($data["error"]) ? 500 : 200, $headerData, json_encode($data)); } // Check user authentication @@ -838,11 +872,11 @@ class YellowEdit { $action = $this->yellow->page->getRequest("action"); $authToken = $this->yellow->toolbox->getCookie("authtoken"); $csrfToken = $this->yellow->toolbox->getCookie("csrftoken"); - if (empty($action) || $this->isRequestSameSite("POST", $scheme, $address)) { + if (is_string_empty($action) || $this->isRequestSameSite("POST", $scheme, $address)) { if ($action=="login") { $email = $this->yellow->page->getRequest("email"); $password = $this->yellow->page->getRequest("password"); - if ($this->users->checkAuthLogin($email, $password)) { + if ($this->response->checkAuthLogin($email, $password)) { $this->response->createCookies($scheme, $address, $base, $email); $this->response->userEmail = $email; $this->response->language = $this->getUserLanguage($email); @@ -851,18 +885,19 @@ class YellowEdit { $this->response->userFailedEmail = $email; $this->response->userFailedExpire = PHP_INT_MAX; } - } elseif (!empty($authToken) && !empty($csrfToken)) { + } elseif (!is_string_empty($authToken) && !is_string_empty($csrfToken)) { $csrfTokenReceived = isset($_POST["csrftoken"]) ? $_POST["csrftoken"] : ""; - $csrfTokenIrrelevant = empty($action); - if ($this->users->checkAuthToken($authToken, $csrfToken, $csrfTokenReceived, $csrfTokenIrrelevant)) { - $this->response->userEmail = $email = $this->users->getAuthEmail($authToken); + $csrfTokenIrrelevant = is_string_empty($action); + if ($this->response->checkAuthToken($authToken, $csrfToken, $csrfTokenReceived, $csrfTokenIrrelevant)) { + $this->response->userEmail = $email = $this->response->getAuthEmail($authToken); $this->response->language = $this->getUserLanguage($email); } else { $this->response->userFailedError = "auth"; - $this->response->userFailedEmail = $this->users->getAuthEmail($authToken); - $this->response->userFailedExpire = $this->users->getAuthExpire($authToken); + $this->response->userFailedEmail = $this->response->getAuthEmail($authToken); + $this->response->userFailedExpire = $this->response->getAuthExpire($authToken); } } + $this->yellow->user->set($this->response->userEmail); } return $this->response->isUser(); } @@ -871,15 +906,15 @@ class YellowEdit { public function checkUserUnauth($scheme, $address, $base, $location, $fileName) { $ok = false; $action = $this->yellow->page->getRequest("action"); - if (empty($action) || $action=="signup" || $action=="forgot") { + if (is_string_empty($action) || $action=="signup" || $action=="forgot") { $ok = true; } elseif ($this->yellow->page->isRequest("actiontoken")) { $actionToken = $this->yellow->page->getRequest("actiontoken"); $email = $this->yellow->page->getRequest("email"); $action = $this->yellow->page->getRequest("action"); $expire = $this->yellow->page->getRequest("expire"); - $langauge = $this->yellow->page->getRequest("language"); - if ($this->users->checkActionToken($actionToken, $email, $action, $expire)) { + $language = $this->yellow->page->getRequest("language"); + if ($this->response->checkActionToken($actionToken, $email, $action, $expire)) { $ok = true; $this->response->language = $this->getActionLanguage($language); } else { @@ -893,18 +928,18 @@ class YellowEdit { // Check user failed public function checkUserFailed($scheme, $address, $base, $location, $fileName) { - if (!empty($this->response->userFailedError)) { - if ($this->response->userFailedExpire>time() && $this->users->isExisting($this->response->userFailedEmail)) { + if (!is_string_empty($this->response->userFailedError)) { + if ($this->response->userFailedExpire>time() && $this->yellow->user->isExisting($this->response->userFailedEmail)) { $email = $this->response->userFailedEmail; - $failed = $this->users->getUser($email, "failed")+1; - $fileNameUser = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("editUserFile"); - $status = $this->users->save($fileNameUser, $email, array("failed" => $failed)) ? "ok" : "error"; + $failed = $this->yellow->user->getUser("failed", $email)+1; + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $status = $this->yellow->user->save($fileNameUser, $email, array("failed" => $failed)) ? "ok" : "error"; if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); if ($failed==$this->yellow->system->get("editBruteForceProtection")) { - $statusBeforeProtection = $this->users->getUser($email, "status"); + $statusBeforeProtection = $this->yellow->user->getUser("status", $email); $statusAfterProtection = ($statusBeforeProtection=="active" || $statusBeforeProtection=="inactive") ? "inactive" : "failed"; if ($status=="ok") { - $status = $this->users->save($fileNameUser, $email, array("status" => $statusAfterProtection)) ? "ok" : "error"; + $status = $this->yellow->user->save($fileNameUser, $email, array("status" => $statusAfterProtection)) ? "ok" : "error"; if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); } if ($status=="ok" && $statusBeforeProtection=="active") { @@ -935,40 +970,51 @@ class YellowEdit { case "change": $statusExpected = "active"; break; case "remove": $statusExpected = "active"; break; } - return $this->users->getUser($email, "status")==$statusExpected ? "ok" : "done"; + return $this->yellow->user->getUser("status", $email)==$statusExpected ? "ok" : "done"; } // Return user account changes - public function getUserAccount($email, $password, $action) { + public function getUserAccount($action, $email, $password) { $status = null; - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onEditUserAccount")) { - $status = $value["obj"]->onEditUserAccount($email, $password, $action, $this->users); + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditUserAccount")) { + $status = $value["object"]->onEditUserAccount($action, $email, $password); if (!is_null($status)) break; } } if (is_null($status)) { $status = "ok"; - if (!empty($password) && strlenu($password)<$this->yellow->system->get("editUserPasswordMinLength")) $status = "short"; - if (!empty($password) && $password==$email) $status = "weak"; - if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) $status = "invalid"; + if (!is_string_empty($password) && strlenu($password)<$this->yellow->system->get("editUserPasswordMinLength")) $status = "short"; + if (!is_string_empty($password) && $password==$email) $status = "weak"; + if (!is_string_empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) $status = "invalid"; } return $status; } // Return user language public function getUserLanguage($email) { - $language = $this->users->getUser($email, "language"); - if (!$this->yellow->text->isLanguage($language)) $language = $this->yellow->system->get("language"); + $language = $this->yellow->user->getUser("language", $email); + if (!$this->yellow->language->isExisting($language)) $language = $this->yellow->system->get("language"); return $language; } // Return action language public function getActionLanguage($language) { - if (!$this->yellow->text->isLanguage($language)) $language = $this->yellow->system->get("language"); + if (!$this->yellow->language->isExisting($language)) $language = $this->yellow->system->get("language"); return $language; } + // Check if user account is taken + public function isUserAccountTaken($email) { + $taken = false; + if ($this->yellow->user->isExisting($email)) { + $status = $this->yellow->user->getUser("status", $email); + $reserved = strtotime($this->yellow->user->getUser("modified", $email)) + 60*60*24; + if ($status=="active" || $status=="inactive" || $reserved>time()) $taken = true; + } + return $taken; + } + // Check if request came from same site public function isRequestSameSite($method, $scheme, $address) { $origin = ""; @@ -976,69 +1022,75 @@ class YellowEdit { if ($this->yellow->toolbox->getServer("HTTP_ORIGIN")) $origin = $this->yellow->toolbox->getServer("HTTP_ORIGIN"); return $this->yellow->toolbox->getServer("REQUEST_METHOD")==$method && $origin=="$scheme://$address"; } + + // Check if edit location + public function isEditLocation($location) { + $locationLength = strlenu($this->yellow->system->get("editLocation")); + return substru($location, 0, $locationLength)==$this->yellow->system->get("editLocation"); + } } class YellowEditResponse { - public $yellow; //access to API - public $extension; //access to extension - public $active; //location is active? (boolean) - public $userEmail; //user email - public $userFailedError; //error of failed authentication - public $userFailedEmail; //email of failed authentication - public $userFailedExpire; //expiration time of failed authentication - public $rawDataSource; //raw data of page for comparison - public $rawDataEdit; //raw data of page for editing - public $rawDataOutput; //raw data of dynamic output - public $rawDataReadonly; //raw data is read only? (boolean) - public $rawDataEndOfLine; //end of line format for raw data - public $language; //response language - public $action; //response action - public $status; //response status + public $yellow; // access to API + public $extension; // access to extension + public $userEmail; // user email + public $userFailedError; // error of failed authentication + public $userFailedEmail; // email of failed authentication + public $userFailedExpire; // expiration time of failed authentication + public $rawDataSource; // raw data of page for comparison + public $rawDataEdit; // raw data of page for editing + public $rawDataOutput; // raw data of dynamic output + public $rawDataReadonly; // raw data is read only? (boolean) + public $rawDataEndOfLine; // end of line format for raw data + public $language; // response language + public $action; // response action + public $status; // response status public function __construct($yellow) { $this->yellow = $yellow; - $this->extension = $yellow->extensions->get("edit"); + $this->extension = $yellow->extension->get("edit"); + $this->userEmail = ""; } // Process page data public function processPageData($page) { if ($this->isUser()) { - if (empty($this->rawDataSource)) $this->rawDataSource = $page->rawData; - if (empty($this->rawDataEdit)) $this->rawDataEdit = $page->rawData; - if (empty($this->rawDataEndOfLine)) $this->rawDataEndOfLine = $this->getEndOfLine($page->rawData); + if (is_string_empty($this->rawDataSource)) $this->rawDataSource = $page->rawData; + if (is_string_empty($this->rawDataEdit)) $this->rawDataEdit = $page->rawData; + if (is_string_empty($this->rawDataEndOfLine)) $this->rawDataEndOfLine = $this->getEndOfLine($page->rawData); if ($page->statusCode==404 || $this->yellow->toolbox->isLocationArguments()) { $this->rawDataEdit = $this->getRawDataGenerated($page); $this->rawDataReadonly = true; } - if ($page->statusCode==434) { + if ($page->statusCode==434 || $page->statusCode==435) { $this->rawDataEdit = $this->getRawDataNew($page, true); $this->rawDataReadonly = false; } } - if (empty($this->language)) $this->language = $page->get("language"); - if (empty($this->action)) $this->action = $this->isUser() ? "none" : "login"; - if (empty($this->status)) $this->status = "none"; + if (is_string_empty($this->language)) $this->language = $page->get("language"); + if (is_string_empty($this->action)) $this->action = $this->isUser() ? "none" : "login"; + if (is_string_empty($this->status)) $this->status = "none"; if ($this->status=="error") $this->action = "error"; } // Return new page public function getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { - $rawData = $this->yellow->toolbox->normaliseLines($rawData, $endOfLine); + $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine); $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $page->parseData($rawData, false, 0); - $this->editContentFile($page, "create"); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $page->parseMeta($rawData); + $this->editContentFile($page, "create", $this->userEmail); if ($this->yellow->content->find($page->location)) { - $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation")); + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation")); $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); - while ($this->yellow->content->find($page->location) || empty($page->fileName)) { + while ($this->yellow->content->find($page->location) || is_string_empty($page->fileName)) { $page->rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData)); - $page->rawData = $this->yellow->toolbox->normaliseLines($page->rawData, $endOfLine); - $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation")); + $page->rawData = $this->yellow->lookup->normaliseLines($page->rawData, $endOfLine); + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation")); $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); if (++$pageCounter>999) break; } - if ($this->yellow->content->find($page->location) || empty($page->fileName)) { + if ($this->yellow->content->find($page->location) || is_string_empty($page->fileName)) { $page->error(500, "Page '".$page->get("title")."' is not possible!"); } } else { @@ -1052,25 +1104,25 @@ class YellowEditResponse { // Return modified page public function getPageEdit($scheme, $address, $base, $location, $fileName, $rawDataSource, $rawDataEdit, $rawDataFile, $endOfLine) { - $rawDataSource = $this->yellow->toolbox->normaliseLines($rawDataSource, $endOfLine); - $rawDataEdit = $this->yellow->toolbox->normaliseLines($rawDataEdit, $endOfLine); - $rawDataFile = $this->yellow->toolbox->normaliseLines($rawDataFile, $endOfLine); + $rawDataSource = $this->yellow->lookup->normaliseLines($rawDataSource, $endOfLine); + $rawDataEdit = $this->yellow->lookup->normaliseLines($rawDataEdit, $endOfLine); + $rawDataFile = $this->yellow->lookup->normaliseLines($rawDataFile, $endOfLine); $rawData = $this->extension->merge->merge($rawDataSource, $rawDataEdit, $rawDataFile); $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $page->parseData($rawData, false, 0); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $page->parseMeta($rawData); $pageSource = new YellowPage($this->yellow); - $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName); - $pageSource->parseData(($rawDataSource), false, 0); - $this->editContentFile($page, "edit"); + $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $pageSource->parseMeta($rawDataSource); + $this->editContentFile($page, "edit", $this->userEmail); if ($this->isMetaModified($pageSource, $page) && $page->location!=$this->yellow->content->getHomeLocation($page->location)) { - $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation"), true); + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation"), true); $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); - if ($page->location!=$pageSource->location && ($this->yellow->content->find($page->location) || empty($page->fileName))) { + if ($page->location!=$pageSource->location && ($this->yellow->content->find($page->location) || is_string_empty($page->fileName))) { $page->error(500, "Page '".$page->get("title")."' is not possible!"); } } - if (empty($page->rawData)) $page->error(500, "Page has been modified by someone else!"); + if (is_string_empty($page->rawData)) $page->error(500, "Page has been modified by someone else!"); if (!$this->isUserAccess("edit", $page->location) || !$this->isUserAccess("edit", $pageSource->location)) { $page->error(500, "Page '".$page->get("title")."' is restricted!"); @@ -1080,48 +1132,61 @@ class YellowEditResponse { // Return deleted page public function getPageDelete($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { - $rawData = $this->yellow->toolbox->normaliseLines($rawData, $endOfLine); + $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine); $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $page->parseData($rawData, false, 0); - $this->editContentFile($page, "delete"); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $page->parseMeta($rawData); + $this->editContentFile($page, "delete", $this->userEmail); if (!$this->isUserAccess("delete", $page->location)) { $page->error(500, "Page '".$page->get("title")."' is restricted!"); } return $page; } + // Return restored page + public function getPageRestore($scheme, $address, $base, $location, $fileName) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $page->parseMeta(""); + $this->editContentFile($page, "restore", $this->userEmail); + if (!$this->isUserAccess("restore", $page->location)) { + $page->error(500, "Page '".$page->get("title")."' is restricted!"); + } + return $page; + } + // Return preview page public function getPagePreview($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { - $rawData = $this->yellow->toolbox->normaliseLines($rawData, $endOfLine); + $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine); $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $page->parseData($rawData, false, 200); - $this->yellow->text->setLanguage($page->get("language")); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $page->parseMeta($rawData, 200); + $this->yellow->language->set($page->get("language")); $class = "page-preview layout-".$page->get("layout"); $output = "<div class=\"".htmlspecialchars($class)."\"><div class=\"content\"><div class=\"main\">"; if ($this->yellow->system->get("editToolbarButtons")!="none") $output .= "<h1>".$page->getHtml("titleContent")."</h1>\n"; - $output .= $page->getContent(); + $output .= $page->getContentHtml(); $output .= "</div></div></div>"; - $page->setOutput($output); + $page->statusCode = 200; + $page->outputData = $output; return $page; } // Return uploaded file public function getFileUpload($scheme, $address, $base, $pageLocation, $fileNameTemp, $fileNameShort) { $file = new YellowPage($this->yellow); - $file->setRequestInformation($scheme, $address, $base, "/".$fileNameTemp, $fileNameTemp); - $file->parseData(null, false, 0); + $file->setRequestInformation($scheme, $address, $base, "/".$fileNameTemp, $fileNameTemp, false); + $file->parseMeta(null); $file->set("fileNameShort", $fileNameShort); $file->set("type", $this->yellow->toolbox->getFileType($fileNameShort)); if ($file->get("type")=="html" || $file->get("type")=="svg") { $fileData = $this->yellow->toolbox->readFile($fileNameTemp); - $fileData = $this->yellow->toolbox->normaliseData($fileData, $file->get("type")); - if (empty($fileData) || !$this->yellow->toolbox->createFile($fileNameTemp, $fileData)) { + $fileData = $this->yellow->lookup->normaliseData($fileData, $file->get("type")); + if (is_string_empty($fileData) || !$this->yellow->toolbox->createFile($fileNameTemp, $fileData)) { $file->error(500, "Can't write file '$fileNameTemp'!"); } } - $this->editMediaFile($file, "upload"); + $this->editMediaFile($file, "upload", $this->userEmail); $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation")); $file->fileName = substru($file->location, 1); while (is_file($file->fileName)) { @@ -1135,18 +1200,22 @@ class YellowEditResponse { } // Return system file - public function getFileSystem($scheme, $address, $base, $pageLocation, $fileName, $settings) { + public function getFileSystem($scheme, $address, $base, $pageLocation, $fileNameSystem, $settings) { $file = new YellowPage($this->yellow); - $file->setRequestInformation($scheme, $address, $base, "/".$fileName, $fileName); - $file->parseData(null, false, 0); + $file->setRequestInformation($scheme, $address, $base, "/".$fileNameSystem, $fileNameSystem, false); + $file->parseMeta(null); foreach ($settings as $key=>$value) $file->set($key, $value); - $this->editSystemFile($file, "system"); + $this->editSystemFile($file, "configure", $this->userEmail); return $file; } // Return page data including status information public function getPageData($page) { $data = array(); + $data["scheme"] = $this->yellow->page->scheme; + $data["address"] = $this->yellow->page->address; + $data["base"] = $this->yellow->page->base; + $data["location"] = $this->yellow->page->location; if ($this->isUser()) { $data["title"] = $this->yellow->toolbox->getMetaData($this->rawDataEdit, "title"); $data["rawDataSource"] = $this->rawDataSource; @@ -1155,10 +1224,6 @@ class YellowEditResponse { $data["rawDataOutput"] = strval($this->rawDataOutput); $data["rawDataReadonly"] = intval($this->rawDataReadonly); $data["rawDataEndOfLine"] = $this->rawDataEndOfLine; - $data["scheme"] = $this->yellow->page->scheme; - $data["address"] = $this->yellow->page->address; - $data["base"] = $this->yellow->page->base; - $data["location"] = $this->yellow->page->location; } if ($this->action!="none") $data = array_merge($data, $this->getRequestData()); $data["action"] = $this->action; @@ -1167,37 +1232,33 @@ class YellowEditResponse { return $data; } - // Return system data including user information + // Return system data public function getSystemData() { - $data = $this->yellow->system->getData("", "Location"); + $data = array(); + $data["coreServerScheme"] = $this->yellow->system->get("coreServerScheme"); + $data["coreServerAddress"] = $this->yellow->system->get("coreServerAddress"); + $data["coreServerBase"] = $this->yellow->system->get("coreServerBase"); + $data["coreDebugMode"] = $this->yellow->system->get("coreDebugMode"); + $data = array_merge($data, $this->yellow->system->getSettings("", "Location")); if ($this->isUser()) { - $data["userEmail"] = $this->userEmail; - $data["userName"] = $this->extension->users->getUser($this->userEmail, "name"); - $data["userLanguage"] = $this->extension->users->getUser($this->userEmail, "language"); - $data["userStatus"] = $this->extension->users->getUser($this->userEmail, "status"); - $data["userHome"] = $this->extension->users->getUser($this->userEmail, "home"); - $data["userAccess"] = $this->extension->users->getUser($this->userEmail, "access"); - $data["coreServerScheme"] = $this->yellow->system->get("coreServerScheme"); - $data["coreServerAddress"] = $this->yellow->system->get("coreServerAddress"); - $data["coreServerBase"] = $this->yellow->system->get("coreServerBase"); $data["coreFileSizeMax"] = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize")); - $data["coreVersion"] = "Datenstrom Yellow ".YellowCore::VERSION; + $data["coreProductRelease"] = "Datenstrom Yellow ".YellowCore::RELEASE; $data["coreExtensions"] = array(); - foreach ($this->yellow->extensions->extensions as $key=>$value) { - $data["coreExtensions"][$key] = $value["type"]; + foreach ($this->yellow->extension->data as $key=>$value) { + $data["coreExtensions"][$key] = $value["class"]; } $data["coreLanguages"] = array(); - foreach ($this->yellow->text->getLanguages() as $language) { - $data["coreLanguages"][$language] = $this->yellow->text->getTextHtml("languageDescription", $language); + foreach ($this->yellow->system->getAvailable("language") as $language) { + $data["coreLanguages"][$language] = $this->yellow->language->getTextHtml("languageDescription", $language); } $data["editSettingsActions"] = $this->getSettingsActions(); $data["editUploadExtensions"] = $this->yellow->system->get("editUploadExtensions"); $data["editKeyboardShortcuts"] = $this->yellow->system->get("editKeyboardShortcuts"); $data["editToolbarButtons"] = $this->getToolbarButtons(); $data["editStatusValues"] = $this->getStatusValues(); - $data["emojiawesomeToolbarButtons"] = $this->yellow->system->get("emojiawesomeToolbarButtons"); - $data["fontawesomeToolbarButtons"] = $this->yellow->system->get("fontawesomeToolbarButtons"); - if ($this->isUserAccess("system")) { + $data["emojiToolbarButtons"] = $this->yellow->system->get("emojiToolbarButtons"); + $data["iconToolbarButtons"] = $this->yellow->system->get("iconToolbarButtons"); + if ($this->isUserAccess("configure")) { $data["sitename"] = $this->yellow->system->get("sitename"); $data["author"] = $this->yellow->system->get("author"); $data["email"] = $this->yellow->system->get("email"); @@ -1207,11 +1268,32 @@ class YellowEditResponse { $data["editLoginPassword"] = $this->yellow->page->get("editLoginPassword"); $data["editLoginRestriction"] = intval($this->isLoginRestriction()); } - if (defined("DEBUG") && DEBUG>=1) $data["debug"] = DEBUG; return $data; } - // Return request strings + // Return user data + public function getUserData() { + $data = array(); + if ($this->isUser()) { + $data["email"] = $this->userEmail; + $data["name"] = $this->yellow->user->getUser("name", $this->userEmail); + $data["description"] = $this->yellow->user->getUser("description", $this->userEmail); + $data["language"] = $this->yellow->user->getUser("language", $this->userEmail); + $data["status"] = $this->yellow->user->getUser("status", $this->userEmail); + $data["access"] = $this->yellow->user->getUser("access", $this->userEmail); + $data["home"] = $this->yellow->user->getUser("home", $this->userEmail); + } + return $data; + } + + // Return language data + public function getLanguageData() { + $dataLanguage = $this->yellow->language->getSettings("language", "", $this->language); + $dataEdit = $this->yellow->language->getSettings("edit", "", $this->language); + return array_merge($dataLanguage, $dataEdit); + } + + // Return request data public function getRequestData() { $data = array(); foreach ($_REQUEST as $key=>$value) { @@ -1221,29 +1303,21 @@ class YellowEditResponse { return $data; } - // Return text strings - public function getTextData() { - $textLanguage = $this->yellow->text->getData("language", $this->language); - $textEdit = $this->yellow->text->getData("edit", $this->language); - return array_merge($textLanguage, $textEdit); - } - // Return settings actions public function getSettingsActions() { $settingsActions = "account"; - if ($this->isUserAccess("system")) $settingsActions .= ", system"; + if ($this->isUserAccess("configure")) $settingsActions .= ", configure"; if ($this->isUserAccess("update")) $settingsActions .= ", update"; - return $settingsActions=="account" ? "" : $settingsActions; + return $settingsActions=="account" ? "none" : $settingsActions; } // Return toolbar buttons public function getToolbarButtons() { $toolbarButtons = $this->yellow->system->get("editToolbarButtons"); if ($toolbarButtons=="auto") { - $toolbarButtons = ""; - if ($this->yellow->extensions->isExisting("markdown")) $toolbarButtons = "format, bold, italic, strikethrough, code, separator, list, link, file"; - if ($this->yellow->extensions->isExisting("emojiawesome")) $toolbarButtons .= ", emojiawesome"; - if ($this->yellow->extensions->isExisting("fontawesome")) $toolbarButtons .= ", fontawesome"; + $toolbarButtons = "format, bold, italic, strikethrough, code, separator, list, link, file"; + if ($this->yellow->extension->isExisting("emoji")) $toolbarButtons .= ", emoji"; + if ($this->yellow->extension->isExisting("icon")) $toolbarButtons .= ", icon"; $toolbarButtons .= ", status, preview"; } return $toolbarButtons; @@ -1252,8 +1326,8 @@ class YellowEditResponse { // Return status values public function getStatusValues() { $statusValues = ""; - if ($this->yellow->extensions->isExisting("private")) $statusValues .= ", private"; - if ($this->yellow->extensions->isExisting("draft")) $statusValues .= ", draft"; + if ($this->yellow->extension->isExisting("private")) $statusValues .= ", private"; + if ($this->yellow->extension->isExisting("draft")) $statusValues .= ", draft"; $statusValues .= ", unlisted"; return ltrim($statusValues, ", "); } @@ -1262,7 +1336,7 @@ class YellowEditResponse { public function getEndOfLine($rawData = "") { $endOfLine = $this->yellow->system->get("editEndOfLine"); if ($endOfLine=="auto") { - $rawData = empty($rawData) ? PHP_EOL : substru($rawData, 0, 4096); + $rawData = is_string_empty($rawData) ? PHP_EOL : substru($rawData, 0, 4096); $endOfLine = strposu($rawData, "\r")===false ? "lf" : "crlf"; } return $endOfLine; @@ -1271,35 +1345,29 @@ class YellowEditResponse { // Return update information public function getUpdateInformation() { $statusCode = 200; - $updates = 0; $rawData = ""; - if ($this->yellow->extensions->isExisting("update")) { - list($statusCodeCurrent, $dataCurrent) = $this->yellow->extensions->get("update")->getExtensionsVersion(); - list($statusCodeLatest, $dataLatest) = $this->yellow->extensions->get("update")->getExtensionsVersion(true); - list($statusCodeModified, $dataModified) = $this->yellow->extensions->get("update")->getExtensionsModified(); - $statusCode = max($statusCodeCurrent, $statusCodeLatest, $statusCodeModified); - foreach ($dataCurrent as $key=>$value) { - if (strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0) { - $rawData .= htmlspecialchars(ucfirst($key)." $dataLatest[$key]")."<br />\n"; - ++$updates; - } - } - if ($updates==0) { - foreach ($dataCurrent as $key=>$value) { - if (isset($dataModified[$key]) && isset($dataLatest[$key])) { - $output = $this->yellow->text->getTextHtml("editUpdateModified", $this->language)." - <a href=\"#\" data-action=\"submit\" data-arguments=\"".$this->yellow->toolbox->normaliseArguments("action:update/extension:$key/option:force")."\">".$this->yellow->text->getTextHtml("editUpdateForce", $this->language)."</a><br />\n"; - $rawData .= preg_replace("/@extension/i", htmlspecialchars(ucfirst($key)." $dataLatest[$key]"), $output); + if ($this->yellow->extension->isExisting("update")) { + list($statusCodeCurrent, $settingsCurrent) = $this->yellow->extension->get("update")->getExtensionSettings(false); + list($statusCodeLatest, $settingsLatest) = $this->yellow->extension->get("update")->getExtensionSettings(true); + $statusCode = max($statusCodeCurrent, $statusCodeLatest); + foreach ($settingsCurrent as $key=>$value) { + if ($settingsLatest->isExisting($key)) { + $versionCurrent = $settingsCurrent[$key]->get("version"); + $versionLatest = $settingsLatest[$key]->get("version"); + if (strnatcasecmp($versionCurrent, $versionLatest)<0) { + $rawData .= htmlspecialchars(ucfirst($key)." $versionLatest")."<br />"; } } } + if (!is_string_empty($rawData)) $rawData = "<p>$rawData</p>\n"; } - return array($statusCode, $updates, $rawData); + return array($statusCode, $rawData); } // Return raw data for generated page public function getRawDataGenerated($page) { $title = $page->get("title"); - $text = $this->yellow->text->getText("editDataGenerated", $page->get("language")); + $text = $this->yellow->language->getText("editDataGenerated", $page->get("language")); return "---\nTitle: $title\n---\n$text"; } @@ -1309,26 +1377,26 @@ class YellowEditResponse { foreach ($this->yellow->content->path($page->location)->reverse() as $ancestor) { if ($ancestor->isExisting("layoutNew")) { $name = $this->yellow->lookup->normaliseName($ancestor->get("layoutNew")); - $location = $this->yellow->content->getHomeLocation($page->location).$this->yellow->system->get("coreContentSharedDirectory"); - $fileName = $this->yellow->lookup->findFileFromLocation($location, true).$this->yellow->system->get("editNewFile"); - $fileName = strreplaceu("(.*)", $name, $fileName); + $location = $this->yellow->content->getHomeLocation($page->location)."shared/"; + $fileName = $this->yellow->lookup->findFileFromContentLocation($location, true).$this->yellow->system->get("editNewFile"); + $fileName = str_replace("(.*)", $name, $fileName); if (is_file($fileName)) break; } } if (!is_file($fileName)) { $name = $this->yellow->lookup->normaliseName($this->yellow->system->get("layout")); - $location = $this->yellow->content->getHomeLocation($page->location).$this->yellow->system->get("coreContentSharedDirectory"); - $fileName = $this->yellow->lookup->findFileFromLocation($location, true).$this->yellow->system->get("editNewFile"); - $fileName = strreplaceu("(.*)", $name, $fileName); + $location = $this->yellow->content->getHomeLocation($page->location)."shared/"; + $fileName = $this->yellow->lookup->findFileFromContentLocation($location, true).$this->yellow->system->get("editNewFile"); + $fileName = str_replace("(.*)", $name, $fileName); } if (is_file($fileName)) { $rawData = $this->yellow->toolbox->readFile($fileName); $rawData = preg_replace("/@timestamp/i", time(), $rawData); $rawData = preg_replace("/@datetime/i", date("Y-m-d H:i:s"), $rawData); $rawData = preg_replace("/@date/i", date("Y-m-d"), $rawData); - $rawData = preg_replace("/@usershort/i", strtok($this->extension->users->getUser($this->userEmail, "name"), " "), $rawData); - $rawData = preg_replace("/@username/i", $this->extension->users->getUser($this->userEmail, "name"), $rawData); - $rawData = preg_replace("/@userlanguage/i", $this->extension->users->getUser($this->userEmail, "language"), $rawData); + $rawData = preg_replace("/@usershort/i", strtok($this->yellow->user->getUser("name", $this->userEmail), " "), $rawData); + $rawData = preg_replace("/@username/i", $this->yellow->user->getUser("name", $this->userEmail), $rawData); + $rawData = preg_replace("/@userlanguage/i", $this->yellow->user->getUser("language", $this->userEmail), $rawData); } else { $rawData = "---\nTitle: Page\n---\n"; } @@ -1340,16 +1408,16 @@ class YellowEditResponse { } // Return location for new/modified page - public function getPageNewLocation($rawData, $pageLocation, $pageNewLocation, $pageMatchLocation = false) { - $location = empty($pageNewLocation) ? "@title" : $pageNewLocation; + public function getPageNewLocation($rawData, $pageLocation, $editNewLocation, $pageMatchLocation = false) { + $location = is_string_empty($editNewLocation) ? "@title" : $editNewLocation; $location = preg_replace("/@title/i", $this->getPageNewTitle($rawData), $location); - $location = preg_replace("/@timestamp/i", $this->getPageNewData($rawData, "published", true, "U"), $location); - $location = preg_replace("/@date/i", $this->getPageNewData($rawData, "published", true, "Y-m-d"), $location); - $location = preg_replace("/@year/i", $this->getPageNewData($rawData, "published", true, "Y"), $location); - $location = preg_replace("/@month/i", $this->getPageNewData($rawData, "published", true, "m"), $location); - $location = preg_replace("/@day/i", $this->getPageNewData($rawData, "published", true, "d"), $location); - $location = preg_replace("/@tag/i", $this->getPageNewData($rawData, "tag", true), $location); - $location = preg_replace("/@author/i", $this->getPageNewData($rawData, "author", true), $location); + $location = preg_replace("/@timestamp/i", $this->getPageNewData($rawData, "published", "U"), $location); + $location = preg_replace("/@date/i", $this->getPageNewData($rawData, "published", "Y-m-d"), $location); + $location = preg_replace("/@year/i", $this->getPageNewData($rawData, "published", "Y"), $location); + $location = preg_replace("/@month/i", $this->getPageNewData($rawData, "published", "m"), $location); + $location = preg_replace("/@day/i", $this->getPageNewData($rawData, "published", "d"), $location); + $location = preg_replace("/@tag/i", $this->getPageNewData($rawData, "tag"), $location); + $location = preg_replace("/@author/i", $this->getPageNewData($rawData, "author"), $location); if (!preg_match("/^\//", $location)) { if ($this->yellow->lookup->isFileLocation($pageLocation) || !$pageMatchLocation) { $location = $this->yellow->lookup->getDirectoryLocation($pageLocation).$location; @@ -1357,6 +1425,14 @@ class YellowEditResponse { $location = $this->yellow->lookup->getDirectoryLocation(rtrim($pageLocation, "/")).$location; } } + if (preg_match("/\d/", $location)) { + $locationNew = ""; + $tokens = explode("/", $location); + for ($i=1; $i<count($tokens); ++$i) { + $locationNew .= "/".$this->yellow->lookup->normaliseToken($tokens[$i]); + } + $location = $locationNew; + } if ($pageMatchLocation) { $location = rtrim($location, "/").($this->yellow->lookup->isFileLocation($pageLocation) ? "" : "/"); } @@ -1367,25 +1443,25 @@ class YellowEditResponse { public function getPageNewTitle($rawData) { $title = $this->yellow->toolbox->getMetaData($rawData, "title"); $titleSlug = $this->yellow->toolbox->getMetaData($rawData, "titleSlug"); - $value = empty($titleSlug) ? $title : $titleSlug; - $value = $this->yellow->lookup->normaliseName($value, true, false, true); + $value = is_string_empty($titleSlug) ? $title : $titleSlug; + $value = $this->yellow->lookup->normaliseName($value, false, false, true); return trim(preg_replace("/-+/", "-", $value), "-"); } // Return data for new/modified page - public function getPageNewData($rawData, $key, $filterFirst = false, $dateFormat = "") { + public function getPageNewData($rawData, $key, $dateFormat = "") { $value = $this->yellow->toolbox->getMetaData($rawData, $key); - if ($filterFirst && preg_match("/^(.*?)\,(.*)$/", $value, $matches)) $value = $matches[1]; - if (!empty($dateFormat)) $value = date($dateFormat, strtotime($value)); - if (strempty($value)) $value = "none"; - $value = $this->yellow->lookup->normaliseName($value, true, false, true); + if (preg_match("/^(.*?)\,(.*)$/", $value, $matches)) $value = $matches[1]; + if (!is_string_empty($dateFormat)) $value = date($dateFormat, strtotime($value)); + if (is_string_empty($value)) $value = "none"; + $value = $this->yellow->lookup->normaliseName($value, false, false, true); return trim(preg_replace("/-+/", "-", $value), "-"); } // Return file name for new/modified page public function getPageNewFile($location, $pageFileName = "", $pagePrefix = "") { - $fileName = $this->yellow->lookup->findFileFromLocation($location); - if (!empty($fileName)) { + $fileName = $this->yellow->lookup->findFileFromContentLocation($location); + if (!is_string_empty($fileName)) { if (!is_dir(dirname($fileName))) { $path = ""; $tokens = explode("/", $fileName); @@ -1404,7 +1480,7 @@ class YellowEditResponse { $path .= $tokens[$i]."/"; } $fileName = $path.$tokens[$i]; - $pageFileName = empty($pageFileName) ? $fileName : $pageFileName; + $pageFileName = is_string_empty($pageFileName) ? $fileName : $pageFileName; } $prefix = $this->getPageNewPrefix($location, $pageFileName, $pagePrefix); if ($this->yellow->lookup->isFileLocation($location)) { @@ -1428,7 +1504,7 @@ class YellowEditResponse { // Return prefix for new/modified page public function getPageNewPrefix($location, $pageFileName, $pagePrefix) { - if (empty($pagePrefix)) { + if (is_string_empty($pagePrefix)) { if ($this->yellow->lookup->isFileLocation($location)) { if (preg_match("#^(.*)\/(.+?)$#", $pageFileName, $matches)) $pagePrefix = $matches[2]; } else { @@ -1440,8 +1516,9 @@ class YellowEditResponse { // Return location for new file public function getFileNewLocation($fileNameShort, $pageLocation, $fileNewLocation) { - $location = empty($fileNewLocation) ? $this->yellow->system->get("editUploadNewLocation") : $fileNewLocation; + $location = is_string_empty($fileNewLocation) ? $this->yellow->system->get("editUploadNewLocation") : $fileNewLocation; $location = preg_replace("/@timestamp/i", time(), $location); + $location = preg_replace("/@date/i", date("Y-m-d"), $location); $location = preg_replace("/@type/i", $this->yellow->toolbox->getFileType($fileNameShort), $location); $location = preg_replace("/@group/i", $this->getFileNewGroup($fileNameShort), $location); $location = preg_replace("/@folder/i", $this->getFileNewFolder($pageLocation), $location); @@ -1455,10 +1532,15 @@ class YellowEditResponse { // Return group for new file public function getFileNewGroup($fileNameShort) { $group = "none"; - $path = $this->yellow->system->get("coreMediaDirectory"); $fileType = $this->yellow->toolbox->getFileType($fileNameShort); - $fileName = $this->yellow->system->get(preg_match("/(gif|jpg|png|svg)$/", $fileType) ? "coreImageDirectory" : "coreDownloadDirectory").$fileNameShort; - if (preg_match("#^$path(.+?)\/#", $fileName, $matches)) $group = strtoloweru($matches[1]); + $locationMedia = $this->yellow->system->get("coreMediaLocation"); + $locationGroup = $this->yellow->system->get("coreDownloadLocation"); + if (preg_match("/(gif|jpg|png|svg)$/", $fileType)) { + $locationGroup = $this->yellow->system->get("coreImageLocation"); + } + if (preg_match("#^$locationMedia(.+?)\/#", $locationGroup, $matches)) { + $group = strtoloweru($matches[1]); + } return $group; } @@ -1474,7 +1556,7 @@ class YellowEditResponse { $fileText = $fileNumber = $fileExtension = ""; if (preg_match("/^(.*?)(\d*)(\..*?)?$/", $fileNameShort, $matches)) { $fileText = $matches[1]; - $fileNumber = strempty($matches[2]) ? "-2" : $matches[2]+1; + $fileNumber = is_string_empty($matches[2]) ? "-2" : $matches[2]+1; $fileExtension = $matches[3]; } return $fileText.$fileNumber.$fileExtension; @@ -1483,9 +1565,9 @@ class YellowEditResponse { // Return next title public function getTitleNext($rawData) { $titleText = $titleNumber = ""; - if(preg_match("/^(.*?)(\d*)$/", $this->yellow->toolbox->getMetaData($rawData, "title"), $matches)) { + if (preg_match("/^(.*?)(\d*)$/", $this->yellow->toolbox->getMetaData($rawData, "title"), $matches)) { $titleText = $matches[1]; - $titleNumber = strempty($matches[2]) ? " 2" : $matches[2]+1; + $titleNumber = is_string_empty($matches[2]) ? " 2" : $matches[2]+1; } return $titleText.$titleNumber; } @@ -1497,7 +1579,7 @@ class YellowEditResponse { $userEmail = $this->yellow->system->get("email"); $userLanguage = $this->extension->getUserLanguage($userEmail); } else { - $userName = $this->extension->users->getUser($email, "name"); + $userName = $this->yellow->user->getUser("name", $email); $userEmail = $email; $userLanguage = $this->extension->getUserLanguage($email); } @@ -1505,248 +1587,63 @@ class YellowEditResponse { $url = "$scheme://$address$base/"; } else { $expire = time() + 60*60*24; - $actionToken = $this->extension->users->createActionToken($email, $action, $expire); - $url = "$scheme://$address$base"."/action:$action/email:$email/expire:$expire/language:$userLanguage/actiontoken:$actionToken/"; + $actionToken = $this->createActionToken($email, $action, $expire); + $locationArguments = "/action:$action/email:$email/expire:$expire/language:$userLanguage/actiontoken:$actionToken/"; + $url = "$scheme://$address$base".$this->yellow->lookup->normaliseArguments($locationArguments, false, false); } $prefix = "edit".ucfirst($action); - $message = $this->yellow->text->getText("{$prefix}Message", $userLanguage); - $message = strreplaceu("\\n", "\r\n", $message); + $message = $this->yellow->language->getText("{$prefix}Message", $userLanguage); + $message = str_replace("\\n", "\r\n", $message); $message = preg_replace("/@useraccount/i", $email, $message); $message = preg_replace("/@usershort/i", strtok($userName, " "), $message); $message = preg_replace("/@username/i", $userName, $message); $message = preg_replace("/@userlanguage/i", $userLanguage, $message); $sitename = $this->yellow->system->get("sitename"); - $footer = $this->yellow->text->getText("editMailFooter", $userLanguage); - $footer = strreplaceu("\\n", "\r\n", $footer); + $siteEmail = $this->yellow->system->get("editSiteEmail"); + $subject = $this->yellow->language->getText("{$prefix}Subject", $userLanguage); + $footer = $this->yellow->language->getText("editMailFooter", $userLanguage); + $footer = str_replace("\\n", "\r\n", $footer); $footer = preg_replace("/@sitename/i", $sitename, $footer); - $mailTo = mb_encode_mimeheader("$userName")." <$userEmail>"; - $mailSubject = mb_encode_mimeheader($this->yellow->text->getText("{$prefix}Subject", $userLanguage)); - $mailHeaders = mb_encode_mimeheader("From: $sitename")." <noreply>\r\n"; - $mailHeaders .= mb_encode_mimeheader("X-Request-Url: $scheme://$address$base")."\r\n"; - $mailHeaders .= "Mime-Version: 1.0\r\n"; - $mailHeaders .= "Content-Type: text/plain; charset=utf-8\r\n"; + $mailHeaders = array( + "To" => "$userName <$userEmail>", + "From" => "$sitename <$siteEmail>", + "Subject" => $subject, + "Date" => date(DATE_RFC2822), + "Mime-Version" => "1.0", + "Content-Type" => "text/plain; charset=utf-8", + "X-Request-Url" => "$scheme://$address$base"); $mailMessage = "$message\r\n\r\n$url\r\n-- \r\n$footer"; - return mail($mailTo, $mailSubject, $mailMessage, $mailHeaders); + return $this->yellow->toolbox->mail($action, $mailHeaders, $mailMessage); } // Create browser cookies public function createCookies($scheme, $address, $base, $email) { $expire = time() + $this->yellow->system->get("editLoginSessionTimeout"); - $authToken = $this->extension->users->createAuthToken($email, $expire); - $csrfToken = $this->extension->users->createCsrfToken(); + $authToken = $this->createAuthToken($email, $expire); + $csrfToken = $this->createCsrfToken(); setcookie("authtoken", $authToken, $expire, "$base/", "", $scheme=="https", true); setcookie("csrftoken", $csrfToken, $expire, "$base/", "", $scheme=="https", false); } // Destroy browser cookies public function destroyCookies($scheme, $address, $base) { - setcookie("authtoken", "", 1, "$base/", "", $scheme=="https", true); - setcookie("csrftoken", "", 1, "$base/", "", $scheme=="https", false); - } - - // Change content file - public function editContentFile($page, $action) { - if (!$page->isError()) { - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onEditContentFile")) $value["obj"]->onEditContentFile($page, $action); - } - } - } - - // Change media file - public function editMediaFile($file, $action) { - if (!$file->isError()) { - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onEditMediaFile")) $value["obj"]->onEditMediaFile($file, $action); - } - } - } - - // Change system file - public function editSystemFile($file, $action) { - if (!$file->isError()) { - foreach ($this->yellow->extensions->extensions as $key=>$value) { - if (method_exists($value["obj"], "onEditSystemFile")) $value["obj"]->onEditSystemFile($file, $action); - } - } - } - - // Check if meta data has been modified - public function isMetaModified($pageSource, $pageOther) { - return substrb($pageSource->rawData, 0, $pageSource->metaDataOffsetBytes) != - substrb($pageOther->rawData, 0, $pageOther->metaDataOffsetBytes); - } - - // Check if active - public function isActive() { - return $this->active; - } - - // Check if user is logged in - public function isUser() { - return !empty($this->userEmail); - } - - // Check if user with access - public function isUserAccess($action, $location = "") { - $userHome = $this->extension->users->getUser($this->userEmail, "home"); - $userAccess = preg_split("/\s*,\s*/", $this->extension->users->getUser($this->userEmail, "access")); - return in_array($action, $userAccess) && (empty($location) || substru($location, 0, strlenu($userHome))==$userHome); - } - - // Check if login with restriction - public function isLoginRestriction() { - return $this->yellow->system->get("editLoginRestriction"); - } -} - -class YellowEditUsers { - public $yellow; //access to API - public $users; //registered users - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->users = array(); - } - - // Load users from file - public function load($fileName) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowEditUsers::load file:$fileName<br/>\n"; - $fileData = $this->yellow->toolbox->readFile($fileName); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\#/", $line)) continue; - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])=="email" && !strempty($matches[2])) { - $email = $matches[2]; - if (defined("DEBUG") && DEBUG>=3) echo "YellowEditUsers::load email:$email<br/>\n"; - } - if (!empty($email) && !empty($matches[1]) && !strempty($matches[2])) { - $this->setUser($email, $matches[1], $matches[2]); - } - } - } - } - - // Save user to file - public function save($fileName, $email, $settings) { - $scan = false; - $fileData = $this->yellow->toolbox->readFile($fileName); - $fileDataStart = $fileDataMiddle = $fileDataEnd = ""; - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])=="email" && !strempty($matches[2])) { - $scan = $matches[2]==$email; - } - } - if (!$scan && empty($fileDataMiddle)) { - $fileDataStart .= $line; - } elseif ($scan) { - $fileDataMiddle .= $line; - } else { - $fileDataEnd .= $line; - } - } - $settingsNew = new YellowDataCollection(); - $settingsNew["email"] = $email; - foreach ($settings as $key=>$value) { - if (!empty($key) && !strempty($value)) { - $this->setUser($email, $key, $value); - $settingsNew[$key] = $value; - } - } - $fileDataSettings = ""; - foreach ($this->yellow->toolbox->getTextLines($fileDataMiddle) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!empty($matches[1]) && isset($settingsNew[$matches[1]])) { - $fileDataSettings .= "$matches[1]: ".$settingsNew[$matches[1]]."\n"; - unset($settingsNew[$matches[1]]); - continue; - } - } - $fileDataSettings .= $line; - } - foreach ($settingsNew as $key=>$value) { - $fileDataSettings .= ucfirst($key).": $value\n"; - } - if (!empty($fileDataSettings)) { - $fileDataSettings = preg_replace("/\n+/", "\n", $fileDataSettings); - if (!empty($fileDataStart) && substr($fileDataStart, -2)!="\n\n") $fileDataSettings = "\n".$fileDataSettings; - if (!empty($fileDataEnd)) $fileDataSettings .= "\n"; - } - $fileDataNew = $fileDataStart.$fileDataSettings.$fileDataEnd; - return $this->yellow->toolbox->createFile($fileName, $fileDataNew); - } - - // Remove user from file - public function remove($fileName, $email) { - $scan = false; - $fileData = $this->yellow->toolbox->readFile($fileName); - $fileDataStart = $fileDataMiddle = $fileDataEnd = ""; - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])=="email" && !strempty($matches[2])) { - $scan = $matches[2]==$email; - } - } - if (!$scan && empty($fileDataMiddle)) { - $fileDataStart .= $line; - } elseif ($scan) { - $fileDataMiddle .= $line; - } else { - $fileDataEnd .= $line; - } - } - if (isset($this->users[$email])) unset($this->users[$email]); - $fileDataNew = rtrim($fileDataStart.$fileDataEnd)."\n"; - return $this->yellow->toolbox->createFile($fileName, $fileDataNew); - } - - // Set user setting - public function setUser($email, $key, $value) { - if (!isset($this->users[$email])) $this->users[$email] = new YellowDataCollection(); - $this->users[$email][$key] = $value; - } - - // Return user setting - public function getUser($email, $key) { - return isset($this->users[$email]) && isset($this->users[$email][$key]) ? $this->users[$email][$key] : ""; - } - - // Check user authentication from email and password - public function checkAuthLogin($email, $password) { - $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); - return $this->isExisting($email) && $this->users[$email]["status"]=="active" && - $this->yellow->toolbox->verifyHash($password, $algorithm, $this->users[$email]["hash"]); - } - - // Check user authentication from tokens - public function checkAuthToken($authToken, $csrfTokenExpected, $csrfTokenReceived, $csrfTokenIrrelevant) { - $signature = "$5y$".substrb($authToken, 0, 96); - $email = $this->getAuthEmail($authToken); - $expire = $this->getAuthExpire($authToken); - return $expire>time() && $this->isExisting($email) && $this->users[$email]["status"]=="active" && - $this->yellow->toolbox->verifyHash($this->users[$email]["hash"]."auth".$expire, "sha256", $signature) && - ($this->yellow->toolbox->verifyToken($csrfTokenExpected, $csrfTokenReceived) || $csrfTokenIrrelevant); - } - - // Check action token - public function checkActionToken($actionToken, $email, $action, $expire) { - $signature = "$5y$".$actionToken; - return $expire>time() && $this->isExisting($email) && - $this->yellow->toolbox->verifyHash($this->users[$email]["hash"].$action.$expire, "sha256", $signature); + setcookie("authtoken", "", 1, "$base/"); + setcookie("csrftoken", "", 1, "$base/"); } // Create authentication token public function createAuthToken($email, $expire) { - $signature = $this->yellow->toolbox->createHash($this->users[$email]["hash"]."auth".$expire, "sha256"); - if (empty($signature)) $signature = "padd"."error-hash-algorithm-sha256"; - return substrb($signature, 4).$this->getUser($email, "stamp").dechex($expire); + $hash = $this->yellow->user->getUser("hash", $email); + $signature = $this->yellow->toolbox->createHash($hash."auth".$expire, "sha256"); + if (is_string_empty($signature)) $signature = "padd"."error-hash-algorithm-sha256"; + return substrb($signature, 4).$this->yellow->user->getUser("stamp", $email).dechex($expire); } // Create action token public function createActionToken($email, $action, $expire) { - $signature = $this->yellow->toolbox->createHash($this->users[$email]["hash"].$action.$expire, "sha256"); - if (empty($signature)) $signature = "padd"."error-hash-algorithm-sha256"; + $hash = $this->yellow->user->getUser("hash", $email); + $signature = $this->yellow->toolbox->createHash($hash.$action.$expire, "sha256"); + if (is_string_empty($signature)) $signature = "padd"."error-hash-algorithm-sha256"; return substrb($signature, 4); } @@ -1760,7 +1657,7 @@ class YellowEditUsers { $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); $cost = $this->yellow->system->get("editUserHashCost"); $hash = $this->yellow->toolbox->createHash($password, $algorithm, $cost); - if (empty($hash)) $hash = "error-hash-algorithm-$algorithm"; + if (is_string_empty($hash)) $hash = "error-hash-algorithm-$algorithm"; return $hash; } @@ -1773,11 +1670,38 @@ class YellowEditUsers { return $stamp; } + // Check user authentication from email and password + public function checkAuthLogin($email, $password) { + $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); + $hash = $this->yellow->user->getUser("hash", $email); + return $this->yellow->user->getUser("status", $email)=="active" && + $this->yellow->toolbox->verifyHash($password, $algorithm, $hash); + } + + // Check user authentication from tokens + public function checkAuthToken($authToken, $csrfTokenExpected, $csrfTokenReceived, $csrfTokenIrrelevant) { + $signature = "$5y$".substrb($authToken, 0, 96); + $email = $this->getAuthEmail($authToken); + $expire = $this->getAuthExpire($authToken); + $hash = $this->yellow->user->getUser("hash", $email); + return $expire>time() && $this->yellow->user->getUser("status", $email)=="active" && + $this->yellow->toolbox->verifyHash($hash."auth".$expire, "sha256", $signature) && + ($this->yellow->toolbox->verifyToken($csrfTokenExpected, $csrfTokenReceived) || $csrfTokenIrrelevant); + } + + // Check action token + public function checkActionToken($actionToken, $email, $action, $expire) { + $signature = "$5y$".$actionToken; + $hash = $this->yellow->user->getUser("hash", $email); + return $expire>time() && $this->yellow->user->isExisting($email) && + $this->yellow->toolbox->verifyHash($hash.$action.$expire, "sha256", $signature); + } + // Return user email from authentication, timing attack safe email lookup public function getAuthEmail($authToken, $stamp = "") { $email = ""; - if (empty($stamp)) $stamp = substrb($authToken, 96, 20); - foreach ($this->users as $key=>$value) { + if (is_string_empty($stamp)) $stamp = substrb($authToken, 96, 20); + foreach ($this->yellow->user->settings as $key=>$value) { if ($this->yellow->toolbox->verifyToken($value["stamp"], $stamp)) $email = $key; } return $email; @@ -1788,43 +1712,150 @@ class YellowEditUsers { return hexdec(substrb($authToken, 96+20)); } - // Return number of users - public function getNumber() { - return count($this->users); + // Change content file + public function editContentFile($page, $action, $email) { + if (!$page->isError()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditContentFile")) $value["object"]->onEditContentFile($page, $action, $email); + } + } } - // Return user data - public function getData() { - $data = array(); - foreach ($this->users as $key=>$value) { - $name = $value["name"]; - if (preg_match("/\s/", $name)) $name = "\"$name\""; - $data[$key] = "$value[email] $name $value[status]"; + // Change media file + public function editMediaFile($file, $action, $email) { + if (!$file->isError()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditMediaFile")) $value["object"]->onEditMediaFile($file, $action, $email); + } } - uksort($data, "strnatcasecmp"); - return $data; } - // Check if user is taken - public function isTaken($email) { - $taken = false; - if ($this->isExisting($email)) { - $status = $this->users[$email]["status"]; - $reserved = strtotime($this->users[$email]["modified"]) + 60*60*24; - if ($status=="active" || $status=="inactive" || $reserved>time()) $taken = true; + // Change system file + public function editSystemFile($file, $action, $email) { + if (!$file->isError()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditSystemFile")) $value["object"]->onEditSystemFile($file, $action, $email); + } } - return $taken; } - // Check if user exists - public function isExisting($email) { - return isset($this->users[$email]); + // Delete file + public function deleteFileLocation($location, $fileName) { + $rawData = $this->yellow->toolbox->readFile($fileName); + $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalLocation", $location); + $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalFileName", $fileName); + return $this->yellow->toolbox->createFile($fileName, $rawData) && + $this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory")); + } + + // Delete directory + public function deleteDirectoryLocation($location, $fileName) { + $rawData = $this->yellow->toolbox->readFile($fileName); + $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalLocation", $location); + $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalFileName", $fileName); + return $this->yellow->toolbox->createFile($fileName, $rawData) && + $this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->system->get("coreTrashDirectory")); + } + + // Restore deleted file from trash + public function restoreFileLocation($location) { + $fileNameDeleted = $fileNameRestored = ""; + $deleted = 0; + $pathTrash = $this->yellow->system->get("coreTrashDirectory"); + $regex = "/^.*\\".$this->yellow->system->get("coreContentExtension")."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, $regex, false, false) as $entry) { + $rawDataOriginal = $this->yellow->toolbox->readFile($entry); + $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation"); + $fileNameOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalFileName"); + $deletedOriginal = $this->yellow->toolbox->getFileDeleted($entry); + if ($location==$locationOriginal && $deleted<=$deletedOriginal) { + $fileNameDeleted = $entry; + $fileNameRestored = $fileNameOriginal; + $rawDataRestored = $rawDataOriginal; + $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalLocation"); + $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalFileName"); + $deleted = $deletedOriginal; + } + } + return !is_string_empty($fileNameDeleted) && $this->yellow->lookup->isContentFile($fileNameRestored) && + $this->yellow->toolbox->renameFile($fileNameDeleted, $fileNameRestored, true) && + $this->yellow->toolbox->createFile($fileNameRestored, $rawDataRestored); + } + + // Restore deleted directory from trash + public function restoreDirectoryLocation($location) { + $pathDeleted = $fileNameRestored = ""; + $deleted = 0; + $pathTrash = $this->yellow->system->get("coreTrashDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, "/.*/", false, true) as $entry) { + $fileName = $entry."/".$this->yellow->system->get("coreContentDefaultFile"); + if (!is_file($fileName)) continue; + $rawDataOriginal = $this->yellow->toolbox->readFile($fileName); + $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation"); + $fileNameOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalFileName"); + $deletedOriginal = $this->yellow->toolbox->getFileDeleted($entry); + if ($location==$locationOriginal && $deleted<=$deletedOriginal) { + $pathDeleted = $entry; + $fileNameRestored = $fileNameOriginal; + $rawDataRestored = $rawDataOriginal; + $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalLocation"); + $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalFileName"); + $deleted = $deletedOriginal; + } + } + return !is_string_empty($pathDeleted) && $this->yellow->lookup->isContentFile($fileNameRestored) && + $this->yellow->toolbox->renameDirectory($pathDeleted, dirname($fileNameRestored), true) && + $this->yellow->toolbox->createFile($fileNameRestored, $rawDataRestored); + } + + // Check if location has been deleted + public function isDeletedLocation($location) { + $found = false; + $pathTrash = $this->yellow->system->get("coreTrashDirectory"); + $regex = "/^.*\\".$this->yellow->system->get("coreContentExtension")."$/"; + $fileNames = $this->yellow->toolbox->getDirectoryEntries($pathTrash, $regex, false, false); + foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, "/.*/", false, true) as $entry) { + $fileName = $entry."/".$this->yellow->system->get("coreContentDefaultFile"); + if (is_file($fileName)) array_push($fileNames, $fileName); + } + foreach ($fileNames as $fileName) { + $rawDataOriginal = $this->yellow->toolbox->readFile($fileName, 4096); + $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation"); + if ($location==$locationOriginal) { + $found = true; + break; + } + } + return $found; + } + + // Check if meta data has been modified + public function isMetaModified($pageSource, $pageOther) { + return substrb($pageSource->rawData, 0, $pageSource->metaDataOffsetBytes) != + substrb($pageOther->rawData, 0, $pageOther->metaDataOffsetBytes); + } + + // Check if login with restriction + public function isLoginRestriction() { + return $this->yellow->system->get("editLoginRestriction"); + } + + // Check if user is logged in + public function isUser() { + return !is_string_empty($this->userEmail); + } + + // Check if user with access + public function isUserAccess($action, $location = "") { + $userHome = $this->yellow->user->getUser("home", $this->userEmail); + $tokens = preg_split("/\s*,\s*/", $this->yellow->user->getUser("access", $this->userEmail)); + return in_array($action, $tokens) && (is_string_empty($location) || substru($location, 0, strlenu($userHome))==$userHome); } } class YellowEditMerge { - public $yellow; //access to API - const ADD = "+"; //merge types + public $yellow; // access to API + const ADD = "+"; // merge types const MODIFY = "*"; const REMOVE = "-"; const SAME = " "; @@ -1936,7 +1967,6 @@ class YellowEditMerge { } else { $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], true); } - if (defined("DEBUG") && DEBUG>=2) echo "YellowEditMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n"; if ($typeMine==YellowEditMerge::ADD || $typeYours==YellowEditMerge::ADD) { if ($typeMine==YellowEditMerge::ADD) ++$posMine; if ($typeYours==YellowEditMerge::ADD) ++$posYours; @@ -1949,13 +1979,11 @@ class YellowEditMerge { array_push($diff, $diffMine[$posMine]); $typeMine = $diffMine[$posMine][0]; $typeYours = " "; - if (defined("DEBUG") && DEBUG>=2) echo "YellowEditMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n"; } for (;$posYours<count($diffYours); ++$posYours) { array_push($diff, $diffYours[$posYours]); $typeYours = $diffYours[$posYours][0]; $typeMine = " "; - if (defined("DEBUG") && DEBUG>=2) echo "YellowEditMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n"; } return $diff; } diff --git a/system/extensions/edit.woff b/system/extensions/edit.woff Binary files differ. diff --git a/system/extensions/generate.php b/system/extensions/generate.php @@ -0,0 +1,456 @@ +<?php +// Generate extension, https://github.com/annaesvensson/yellow-generate + +class YellowGenerate { + const VERSION = "0.8.52"; + public $yellow; // access to API + public $files; // number of files + public $errors; // number of errors + public $locationsArguments; // locations with location arguments detected + public $locationsArgumentsPagination; // locations with pagination arguments detected + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("generateStaticUrl", "auto"); + $this->yellow->system->setDefault("generateStaticDirectory", "public/"); + $this->yellow->system->setDefault("generateStaticDefaultFile", "index.html"); + $this->yellow->system->setDefault("generateStaticErrorFile", "404.html"); + } + + // Handle update + public function onUpdate($action) { + if ($action=="install") { + if ($this->yellow->system->isExisting("commandStaticUrl")) { //TODO: remove later, for backwards compatibility + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + $settings = array( + "generateStaticUrl" => $this->yellow->system->get("commandStaticUrl"), + "generateStaticDirectory" => $this->yellow->system->get("commandStaticDirectory"), + "generateStaticDefaultFile" => $this->yellow->system->get("commandStaticDefaultFile"), + "generateStaticErrorFile" => $this->yellow->system->get("commandStaticErrorFile")); + if (!$this->yellow->system->save($fileName, $settings)) { + $this->yellow->toolbox->log("error", "Can't write file '$fileName'!"); + } + $this->yellow->toolbox->log("info", "Import settings for 'Generate ".YellowGenerate::VERSION."'"); + } + } + } + + // Handle request + public function onRequest($scheme, $address, $base, $location, $fileName) { + return $this->processRequestCache($scheme, $address, $base, $location, $fileName); + } + + // Handle command + public function onCommand($command, $text) { + switch ($command) { + case "generate": $statusCode = $this->processCommandGenerate($command, $text); break; + case "clean": $statusCode = $this->processCommandClean($command, $text); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + return array("generate [directory location]", "clean [directory location]"); + } + + // Process command to generate static website + public function processCommandGenerate($command, $text) { + $statusCode = 0; + list($path, $location) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($location) || substru($location, 0, 1)=="/") { + if ($this->checkStaticSettings()) { + $statusCode = $this->generateStatic($path, $location); + } else { + $statusCode = 500; + $this->files = 0; + $this->errors = 1; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + echo "ERROR generating files: Please configure GenerateStaticUrl in file '$fileName'!\n"; + } + echo "Yellow $command: $this->files file".($this->files!=1 ? "s" : ""); + echo ", $this->errors error".($this->errors!=1 ? "s" : "")."\n"; + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Generate static website + public function generateStatic($path, $location) { + $statusCode = 200; + $this->files = $this->errors = 0; + $path = rtrim(is_string_empty($path) ? $this->yellow->system->get("generateStaticDirectory") : $path, "/"); + if (is_string_empty($location)) { + $statusCode = $this->cleanStatic($path, $location); + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("clean"); + } + } + $statusCode = max($statusCode, $this->generateStaticContent($path, $location, "\rGenerating static website", 5, 95)); + $statusCode = max($statusCode, $this->generateStaticMedia($path, $location)); + echo "\rGenerating static website 100%... done\n"; + return $statusCode; + } + + // Generate static content + public function generateStaticContent($path, $locationFilter, $progressText, $increments, $max) { + $statusCode = 200; + $this->locationsArguments = $this->locationsArgumentsPagination = array(); + $staticUrl = $this->yellow->system->get("generateStaticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $locations = $this->getContentLocations(); + $filesEstimated = count($locations); + foreach ($locations as $location) { + echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max/1.5)."%... "; + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + $statusCode = max($statusCode, $this->generateStaticFile($path, $location, true)); + } + foreach ($this->locationsArguments as $location) { + echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max/1.5)."%... "; + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + $statusCode = max($statusCode, $this->generateStaticFile($path, $location, true)); + } + $filesEstimated = $this->files + count($this->locationsArguments) + count($this->locationsArgumentsPagination); + foreach ($this->locationsArgumentsPagination as $location) { + echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max)."%... "; + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + if (substru($location, -1)!=$this->yellow->toolbox->getLocationArgumentsSeparator()) { + $statusCode = max($statusCode, $this->generateStaticFile($path, $location, false, true)); + } + for ($pageNumber=2; $pageNumber<=999; ++$pageNumber) { + $statusCodeLocation = $this->generateStaticFile($path, $location.$pageNumber, false, true); + $statusCode = max($statusCode, $statusCodeLocation); + if ($statusCodeLocation==100) break; + } + } + echo "$progressText ".$this->getProgressPercent(100, 100, $increments, $max)."%... "; + return $statusCode; + } + + // Generate static media + public function generateStaticMedia($path, $locationFilter) { + $statusCode = 200; + if (is_string_empty($locationFilter)) { + foreach ($this->getMediaLocations() as $location) { + $statusCode = max($statusCode, $this->generateStaticFile($path, $location)); + } + foreach ($this->getExtraLocations($path) as $location) { + $statusCode = max($statusCode, $this->generateStaticFile($path, $location)); + } + $statusCode = max($statusCode, $this->generateStaticFile($path, "/error/", false, false, true)); + } + return $statusCode; + } + + // Generate static file + public function generateStaticFile($path, $location, $analyse = false, $probe = false, $error = false) { + $this->yellow->content = new YellowContent($this->yellow); + $this->yellow->page = new YellowPage($this->yellow); + $this->yellow->page->fileName = substru($location, 1); + if (!is_readable($this->yellow->page->fileName)) { + ob_start(); + $staticUrl = $this->yellow->system->get("generateStaticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $statusCode = $this->requestStaticFile($scheme, $address, $base, $location); + if ($statusCode<400 || $error) { + $fileData = ob_get_contents(); + $statusCode = $this->saveStaticFile($path, $location, $fileData, $statusCode); + } + ob_end_clean(); + } else { + $statusCode = $this->copyStaticFile($path, $location); + } + if ($statusCode==200 && $analyse) $this->analyseLocations($scheme, $address, $base, $fileData); + if ($statusCode==404 && $probe) $statusCode = 100; + if ($statusCode==404 && $error) $statusCode = 200; + if ($statusCode>=200) ++$this->files; + if ($statusCode>=400) { + ++$this->errors; + echo "\rERROR generating location '$location', ".$this->yellow->page->getStatusCode(true)."\n"; + } + if ($this->yellow->system->get("coreDebugMode")>=1) { + echo "YellowGenerate::generateStaticFile status:$statusCode location:$location<br/>\n"; + } + return $statusCode; + } + + // Request static file + public function requestStaticFile($scheme, $address, $base, $location) { + list($serverName, $serverPort) = $this->yellow->toolbox->getTextList($address, ":", 2); + if (is_string_empty($serverPort)) $serverPort = $scheme=="https" ? 443 : 80; + $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1"; + $_SERVER["SERVER_NAME"] = $serverName; + $_SERVER["SERVER_PORT"] = $serverPort; + $_SERVER["REQUEST_METHOD"] = "GET"; + $_SERVER["REQUEST_SCHEME"] = $scheme; + $_SERVER["REQUEST_URI"] = $base.$location; + $_SERVER["SCRIPT_NAME"] = $base."/yellow.php"; + $_SERVER["REMOTE_ADDR"] = "127.0.0.1"; + $_REQUEST = array(); + return $this->yellow->request(); + } + + // Save static file + public function saveStaticFile($path, $location, $fileData, $statusCode) { + $modified = strtotime($this->yellow->page->getHeader("Last-Modified")); + if ($modified==0) $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName); + if ($statusCode>=301 && $statusCode<=303) { + $fileData = $this->getStaticRedirect($this->yellow->page->getHeader("Location")); + $modified = time(); + } + $fileName = $this->getStaticFile($path, $location, $statusCode); + if (is_file($fileName)) $this->yellow->toolbox->deleteFile($fileName); + if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || + !$this->yellow->toolbox->modifyFile($fileName, $modified)) { + $statusCode = 500; + $this->yellow->page->statusCode = $statusCode; + $this->yellow->page->errorMessage = "Can't write file '$fileName'!"; + } + return $statusCode; + } + + // Copy static file + public function copyStaticFile($path, $location) { + $statusCode = 200; + $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName); + $fileName = $this->getStaticFile($path, $location, $statusCode); + if (is_file($fileName)) $this->yellow->toolbox->deleteFile($fileName); + if (!$this->yellow->toolbox->copyFile($this->yellow->page->fileName, $fileName, true) || + !$this->yellow->toolbox->modifyFile($fileName, $modified)) { + $statusCode = 500; + $this->yellow->page->statusCode = $statusCode; + $this->yellow->page->errorMessage = "Can't write file '$fileName'!"; + } + return $statusCode; + } + + // Analyse locations with arguments + public function analyseLocations($scheme, $address, $base, $rawData) { + preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $rawData, $matches); + foreach ($matches[2] as $match) { + $location = rawurldecode($match); + if (preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1]; + if (preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $tokens)) { + if ($tokens[1]!=$scheme) continue; + if ($tokens[2]!=$address) continue; + $location = $tokens[3]; + } + if (substru($location, 0, strlenu($base))!=$base) continue; + if (substru($location, strlenu($base), 1)!="/") continue; + $location = substru($location, strlenu($base)); + if (!$this->yellow->toolbox->isLocationArguments($location)) continue; + if (!$this->yellow->toolbox->isLocationArgumentsPagination($location)) { + $location = rtrim($location, "/")."/"; + if (!isset($this->locationsArguments[$location])) { + $this->locationsArguments[$location] = $location; + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowGenerate::analyseLocations detected location:$location<br/>\n"; + } + } + } else { + $location = rtrim($location, "0..9"); + if (!isset($this->locationsArgumentsPagination[$location])) { + $this->locationsArgumentsPagination[$location] = $location; + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowGenerate::analyseLocations detected location:$location<br/>\n"; + } + } + } + } + } + + // Process command to clean static website + public function processCommandClean($command, $text) { + $statusCode = 0; + list($path, $location) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($location) || substru($location, 0, 1)=="/") { + $statusCode = $this->cleanStatic($path, $location); + echo "Yellow $command: Static website"; + echo " ".($statusCode!=200 ? "not " : "")."cleaned\n"; + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Clean static website + public function cleanStatic($path, $location) { + $statusCode = 200; + $path = rtrim(is_string_empty($path) ? $this->yellow->system->get("generateStaticDirectory") : $path, "/"); + if (is_string_empty($location)) { + $statusCode = max($statusCode, $this->cleanStaticDirectory($path)); + } else { + if ($this->yellow->lookup->isFileLocation($location)) { + $fileName = $this->getStaticFile($path, $location, $statusCode); + $statusCode = $this->cleanStaticFile($fileName); + } else { + $statusCode = $this->cleanStaticDirectory($path.$location); + } + } + return $statusCode; + } + + // Clean static directory + public function cleanStaticDirectory($path) { + $statusCode = 200; + if (is_dir($path) && $this->checkStaticDirectory($path)) { + if (!$this->yellow->toolbox->deleteDirectory($path)) { + $statusCode = 500; + echo "ERROR cleaning files: Can't delete directory '$path'!\n"; + } + } + return $statusCode; + } + + // Clean static file + public function cleanStaticFile($fileName) { + $statusCode = 200; + if (is_file($fileName)) { + if (!$this->yellow->toolbox->deleteFile($fileName)) { + $statusCode = 500; + echo "ERROR cleaning files: Can't delete file '$fileName'!\n"; + } + } + return $statusCode; + } + + // Process request for cached files + public function processRequestCache($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if (is_dir($this->yellow->system->get("coreCacheDirectory"))) { + $location .= $this->yellow->toolbox->getLocationArguments(); + $fileName = rtrim($this->yellow->system->get("coreCacheDirectory"), "/").$location; + if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->system->get("generateStaticDefaultFile"); + if (is_file($fileName) && is_readable($fileName) && !$this->yellow->lookup->isCommandLine()) { + $statusCode = $this->yellow->sendFile(200, $fileName, true); + } + } + return $statusCode; + } + + // Check static settings + public function checkStaticSettings() { + return preg_match("/^(http|https):/", $this->yellow->system->get("generateStaticUrl")); + } + + // Check static directory + public function checkStaticDirectory($path) { + $ok = false; + if (!is_string_empty($path)) { + if ($path==rtrim($this->yellow->system->get("generateStaticDirectory"), "/")) $ok = true; + if ($path==rtrim($this->yellow->system->get("coreCacheDirectory"), "/")) $ok = true; + if ($path==rtrim($this->yellow->system->get("coreTrashDirectory"), "/")) $ok = true; + if (is_file("$path/".$this->yellow->system->get("generateStaticDefaultFile"))) $ok = true; + if (is_file("$path/yellow.php")) $ok = false; + } + return $ok; + } + + // Return progress in percent + public function getProgressPercent($now, $total, $increments, $max) { + $max = intval($max/$increments) * $increments; + $percent = intval(($max/$total) * $now); + if ($increments>1) $percent = intval($percent/$increments) * $increments; + return min($max, $percent); + } + + // Return static file + public function getStaticFile($path, $location, $statusCode) { + if ($statusCode<400) { + $fileName = $path.$location; + if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->system->get("generateStaticDefaultFile"); + } elseif ($statusCode==404) { + $fileName = $path."/".$this->yellow->system->get("generateStaticErrorFile"); + } else { + $fileName = $path."/error.html"; + } + return $fileName; + } + + // Return static redirect + public function getStaticRedirect($location) { + $output = "<!DOCTYPE html><html>\n<head>\n"; + $output .= "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n"; + $output .= "<meta http-equiv=\"refresh\" content=\"0;url=".htmlspecialchars($location)."\" />\n"; + $output .= "</head>\n</html>"; + return $output; + } + + // Return content locations + public function getContentLocations($includeAll = false) { + $locations = array(); + $staticUrl = $this->yellow->system->get("generateStaticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $this->yellow->page->setRequestInformation($scheme, $address, $base, "", "", false); + foreach ($this->yellow->content->index(true, true) as $page) { + if (preg_match("/exclude/i", $page->get("generate")) && !$includeAll) continue; + if ($page->get("status")=="private" || $page->get("status")=="draft") continue; + array_push($locations, $page->location); + } + if (!$this->yellow->content->find("/") && $this->yellow->system->get("coreMultiLanguageMode")) array_unshift($locations, "/"); + return $locations; + } + + // Return media locations + public function getMediaLocations() { + $locations = array(); + $mediaPath = $this->yellow->system->get("coreMediaDirectory"); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($mediaPath, "/.*/", false, false); + foreach ($fileNames as $fileName) { + array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName)); + } + $extensionPath = $this->yellow->system->get("coreExtensionDirectory"); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($extensionPath, "/.*/", false, false); + foreach ($fileNames as $fileName) { + array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName)); + } + $themePath = $this->yellow->system->get("coreThemeDirectory"); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($themePath, "/.*/", false, false); + foreach ($fileNames as $fileName) { + array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName)); + } + return array_diff($locations, $this->getMediaLocationsIgnore()); + } + + // Return media locations to ignore + public function getMediaLocationsIgnore() { + $locations = array(""); + $extensionPath = $this->yellow->system->get("coreExtensionDirectory"); + $extensionDirectoryLength = strlenu($this->yellow->system->get("coreExtensionDirectory")); + if ($this->yellow->extension->isExisting("bundle")) { + foreach ($this->yellow->toolbox->getDirectoryEntries($extensionPath, "/^bundle-(.*)/", false, false) as $entry) { + list($locationsBundle) = $this->yellow->extension->get("bundle")->getBundleInformation($entry); + $locations = array_merge($locations, $locationsBundle); + } + } + if ($this->yellow->extension->isExisting("edit")) { + foreach ($this->yellow->toolbox->getDirectoryEntries($extensionPath, "/^edit\.(.*)/", false, false) as $entry) { + $location = $this->yellow->system->get("coreExtensionLocation").substru($entry, $extensionDirectoryLength); + array_push($locations, $location); + } + } + return array_unique($locations); + } + + // Return extra locations + public function getExtraLocations($path) { + $locations = array(); + $pathIgnore = "($path/|". + $this->yellow->system->get("generateStaticDirectory")."|". + $this->yellow->system->get("coreContentDirectory")."|". + $this->yellow->system->get("coreMediaDirectory")."|". + $this->yellow->system->get("coreSystemDirectory").")"; + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive(".", "/.*/", false, false); + foreach ($fileNames as $fileName) { + $fileName = substru($fileName, 2); + if (preg_match("#^$pathIgnore#", $fileName) || $fileName=="yellow.php") continue; + array_push($locations, "/".$fileName); + } + return $locations; + } +} diff --git a/system/extensions/image.php b/system/extensions/image.php @@ -1,24 +1,30 @@ <?php -// Image extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/image -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. +// Image extension, https://github.com/annaesvensson/yellow-image class YellowImage { - const VERSION = "0.8.8"; - const TYPE = "feature"; - public $yellow; //access to API + const VERSION = "0.8.19"; + public $yellow; // access to API // Handle initialisation public function onLoad($yellow) { $this->yellow = $yellow; - $this->yellow->system->setDefault("imageAlt", "Image"); $this->yellow->system->setDefault("imageUploadWidthMax", "1280"); $this->yellow->system->setDefault("imageUploadHeightMax", "1280"); $this->yellow->system->setDefault("imageUploadJpgQuality", "80"); - $this->yellow->system->setDefault("imageThumbnailLocation", "/media/thumbnails/"); - $this->yellow->system->setDefault("imageThumbnailDirectory", "media/thumbnails/"); $this->yellow->system->setDefault("imageThumbnailJpgQuality", "80"); } + + // Handle update + public function onUpdate($action) { + if ($action=="clean") { + $statusCode = 200; + $path = $this->yellow->lookup->findMediaDirectory("coreThumbnailLocation"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) { + if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + } + if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!"); + } + } // Handle page content of shortcut public function onParseContentShortcut($page, $name, $text, $type) { @@ -26,91 +32,80 @@ class YellowImage { if ($name=="image" && $type=="inline") { list($name, $alt, $style, $width, $height) = $this->yellow->toolbox->getTextArguments($text); if (!preg_match("/^\w+:/", $name)) { - if (empty($alt)) $alt = $this->yellow->system->get("imageAlt"); - if (empty($width)) $width = "100%"; - if (empty($height)) $height = $width; - list($src, $width, $height) = $this->getImageInformation($this->yellow->system->get("coreImageDirectory").$name, $width, $height); + if (is_string_empty($alt)) $alt = $this->yellow->language->getText("imageDefaultAlt"); + if (is_string_empty($width)) $width = "100%"; + if (is_string_empty($height)) $height = $width; + $path = $this->yellow->lookup->findMediaDirectory("coreImageLocation"); + list($src, $width, $height) = $this->getImageInformation($path.$name, $width, $height); } else { - if (empty($alt)) $alt = $this->yellow->system->get("imageAlt"); + if (is_string_empty($alt)) $alt = $this->yellow->language->getText("imageDefaultAlt"); $src = $this->yellow->lookup->normaliseUrl("", "", "", $name); $width = $height = 0; } $output = "<img src=\"".htmlspecialchars($src)."\""; if ($width && $height) $output .= " width=\"".htmlspecialchars($width)."\" height=\"".htmlspecialchars($height)."\""; - if (!empty($alt)) $output .= " alt=\"".htmlspecialchars($alt)."\" title=\"".htmlspecialchars($alt)."\""; - if (!empty($style)) $output .= " class=\"".htmlspecialchars($style)."\""; + if (!is_string_empty($alt)) $output .= " alt=\"".htmlspecialchars($alt)."\" title=\"".htmlspecialchars($alt)."\""; + if (!is_string_empty($style)) $output .= " class=\"".htmlspecialchars($style)."\""; $output .= " />"; } return $output; } // Handle media file changes - public function onEditMediaFile($file, $action) { + public function onEditMediaFile($file, $action, $email) { if ($action=="upload") { $fileName = $file->fileName; - list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInformation($fileName, $file->get("type")); + list($widthInput, $heightInput, $orientation, $type) = + $this->yellow->toolbox->detectImageInformation($fileName, $file->get("type")); $widthMax = $this->yellow->system->get("imageUploadWidthMax"); $heightMax = $this->yellow->system->get("imageUploadHeightMax"); - if (($widthInput>$widthMax || $heightInput>$heightMax) && ($type=="gif" || $type=="jpg" || $type=="png")) { - list($widthOutput, $heightOutput) = $this->getImageDimensionsFit($widthInput, $heightInput, $widthMax, $heightMax); - $image = $this->loadImage($fileName, $type); - $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); - $image = $this->orientImage($image, $fileName, $type); - if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) { - $file->error(500, "Can't write file '$fileName'!"); + if ($type=="gif" || $type=="jpg" || $type=="png") { + if ($widthInput>$widthMax || $heightInput>$heightMax) { + list($widthOutput, $heightOutput) = $this->getImageDimensionsFit($widthInput, $heightInput, $widthMax, $heightMax); + $image = $this->loadImage($fileName, $type); + $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); + $image = $this->orientImage($image, $orientation); + if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) { + $file->error(500, "Can't write file '$fileName'!"); + } + } elseif ($orientation>1) { + $image = $this->loadImage($fileName, $type); + $image = $this->orientImage($image, $orientation); + if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) { + $file->error(500, "Can't write file '$fileName'!"); + } } } } } - - // Handle command - public function onCommand($command, $text) { - switch ($command) { - case "clean": $statusCode = $this->processCommandClean($command, $text); break; - default: $statusCode = 0; - } - return $statusCode; - } - // Process command to clean thumbnails - public function processCommandClean($command, $text) { - $statusCode = 0; - if ($command=="clean" && $text=="all") { - $path = $this->yellow->system->get("imageThumbnailDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) { - if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; - } - if ($statusCode==500) echo "ERROR cleaning thumbnails: Can't delete files in directory '$path'!\n"; - } - return $statusCode; - } - - // Return image info, create thumbnail on demand + // Return image information, create thumbnail on demand public function getImageInformation($fileName, $widthOutput, $heightOutput) { - $fileNameShort = substru($fileName, strlenu($this->yellow->system->get("coreImageDirectory"))); - list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInformation($fileName); + $fileNameShort = substru($fileName, strlenu($this->yellow->lookup->findMediaDirectory("coreImageLocation"))); + list($widthInput, $heightInput, $orientation, $type) = $this->yellow->toolbox->detectImageInformation($fileName); $widthOutput = $this->convertValueAndUnit($widthOutput, $widthInput); $heightOutput = $this->convertValueAndUnit($heightOutput, $heightInput); - if (($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg") { + if (($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg" || $type=="") { $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreImageLocation").$fileNameShort; $width = $widthOutput; $height = $heightOutput; } else { + $pathThumb = $this->yellow->lookup->findMediaDirectory("coreThumbnailLocation"); $fileNameThumb = ltrim(str_replace(array("/", "\\", "."), "-", dirname($fileNameShort)."/".pathinfo($fileName, PATHINFO_FILENAME)), "-"); $fileNameThumb .= "-".$widthOutput."x".$heightOutput; $fileNameThumb .= ".".pathinfo($fileName, PATHINFO_EXTENSION); - $fileNameOutput = $this->yellow->system->get("imageThumbnailDirectory").$fileNameThumb; + $fileNameOutput = $pathThumb.$fileNameThumb; if ($this->isFileNotUpdated($fileName, $fileNameOutput)) { $image = $this->loadImage($fileName, $type); $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); - $image = $this->orientImage($image, $fileName, $type); + $image = $this->orientImage($image, $orientation); if (is_file($fileNameOutput)) $this->yellow->toolbox->deleteFile($fileNameOutput); if (!$this->saveImage($image, $fileNameOutput, $type, $this->yellow->system->get("imageThumbnailJpgQuality")) || !$this->yellow->toolbox->modifyFile($fileNameOutput, $this->yellow->toolbox->getFileModified($fileName))) { $this->yellow->page->error(500, "Can't write file '$fileNameOutput'!"); } } - $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("imageThumbnailLocation").$fileNameThumb; + $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreThumbnailLocation").$fileNameThumb; list($width, $height) = $this->yellow->toolbox->detectImageInformation($fileNameOutput); } return array($src, $width, $height); @@ -173,20 +168,15 @@ class YellowImage { } // Orient image automatically - public function orientImage($image, $fileName, $type) { - if ($type=="jpg") { - $exif = @exif_read_data($fileName); - if ($exif && isset($exif["Orientation"])) { - switch ($exif["Orientation"]) { - case 2: imageflip($image, IMG_FLIP_HORIZONTAL); break; - case 3: $image = imagerotate($image, 180, 0); break; - case 4: imageflip($image, IMG_FLIP_VERTICAL); break; - case 5: $image = imagerotate($image, 90, 0); imageflip($image, IMG_FLIP_VERTICAL); break; - case 6: $image = imagerotate($image, -90, 0); break; - case 7: $image = imagerotate($image, 90, 0); imageflip($image, IMG_FLIP_HORIZONTAL); break; - case 8: $image = imagerotate($image, 90, 0); break; - } - } + public function orientImage($image, $orientation) { + switch ($orientation) { + case 2: imageflip($image, IMG_FLIP_HORIZONTAL); break; + case 3: $image = imagerotate($image, 180, 0); break; + case 4: imageflip($image, IMG_FLIP_VERTICAL); break; + case 5: $image = imagerotate($image, 90, 0); imageflip($image, IMG_FLIP_VERTICAL); break; + case 6: $image = imagerotate($image, -90, 0); break; + case 7: $image = imagerotate($image, 90, 0); imageflip($image, IMG_FLIP_HORIZONTAL); break; + case 8: $image = imagerotate($image, 90, 0); break; } return $image; } diff --git a/system/extensions/install-blog.bin b/system/extensions/install-blog.bin Binary files differ. diff --git a/system/extensions/install-language.bin b/system/extensions/install-language.bin Binary files differ. diff --git a/system/extensions/install-wiki.bin b/system/extensions/install-wiki.bin Binary files differ. diff --git a/system/extensions/markdown.php b/system/extensions/markdown.php @@ -1,12 +1,9 @@ <?php -// Markdown extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/markdown -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. +// Markdown extension, https://github.com/annaesvensson/yellow-markdown class YellowMarkdown { - const VERSION = "0.8.14"; - const TYPE = "feature"; - public $yellow; //access to API + const VERSION = "0.8.26"; + public $yellow; // access to API // Handle initialisation public function onLoad($yellow) { @@ -16,7 +13,9 @@ class YellowMarkdown { // Handle page content in raw format public function onParseContentRaw($page, $text) { $markdown = new YellowMarkdownParser($this->yellow, $page); - return $markdown->transform($text); + $text = $markdown->transform($text); + $text = $this->yellow->lookup->normaliseData($text, "html"); + return $text; } } @@ -3831,14 +3830,13 @@ class MarkdownExtraParser extends MarkdownParser { } } -// Datenstrom Yellow Markdown parser -// Copyright (c) 2013-2020 Datenstrom +// Markdown parser, Copyright Datenstrom, License GPLv2 class YellowMarkdownParser extends MarkdownExtraParser { - public $yellow; //access to API - public $page; //access to page - public $idAttributes; //id attributes - public $noticeLevel; //recursive level + public $yellow; // access to API + public $page; // access to page + public $idAttributes; // id attributes + public $noticeLevel; // recursive level public function __construct($yellow, $page) { $this->yellow = $yellow; @@ -3846,17 +3844,18 @@ class YellowMarkdownParser extends MarkdownExtraParser { $this->idAttributes = array(); $this->noticeLevel = 0; $this->url_filter_func = function($url) use ($yellow, $page) { - return $yellow->lookup->normaliseLocation($url, $page->location); + return $yellow->lookup->normaliseLocation($url, $page->getPage("main")->location); }; $this->span_gamut += array("doStrikethrough" => 55); $this->block_gamut += array("doNoticeBlocks" => 65); + $this->document_gamut += array("doFootnotesLinks" => 55); $this->escape_chars .= "~"; parent::__construct(); } // Handle striketrough public function doStrikethrough($text) { - $parts = preg_split("/(?<![~])(~~)(?![~])/", $text, null, PREG_SPLIT_DELIM_CAPTURE); + $parts = preg_split("/(?<![~])(~~)(?![~])/", $text, -1, PREG_SPLIT_DELIM_CAPTURE); if (count($parts)>3) { $text = ""; $open = false; @@ -3877,7 +3876,7 @@ class YellowMarkdownParser extends MarkdownExtraParser { public function doAutoLinks($text) { $text = preg_replace_callback("/<(\w+:[^\'\">\s]+)>/", array($this, "_doAutoLinks_url_callback"), $text); $text = preg_replace_callback("/<([\w\+\-\.]+@[\w\-\.]+)>/", array($this, "_doAutoLinks_email_callback"), $text); - $text = preg_replace_callback("/^\s*\[(\w+)(.*?)\]\s*$/", array($this, "_doAutoLinks_shortcutBlock_callback"), $text); + $text = preg_replace_callback("/^\s*\[(\w+)([^\]]*)\]\s*$/", array($this, "_doAutoLinks_shortcutBlock_callback"), $text); $text = preg_replace_callback("/\[(\w+)(.*?)\]/", array($this, "_doAutoLinks_shortcutInline_callback"), $text); $text = preg_replace_callback("/\[\-\-(.*?)\-\-\]/", array($this, "_doAutoLinks_shortcutComment_callback"), $text); $text = preg_replace_callback("/\:([\w\+\-\_]+)\:/", array($this, "_doAutoLinks_shortcutSymbol_callback"), $text); @@ -3913,7 +3912,7 @@ class YellowMarkdownParser extends MarkdownExtraParser { // Handle fenced code blocks public function _doFencedCodeBlocks_callback($matches) { $text = $matches[4]; - $name = empty($matches[2]) ? "" : trim("$matches[2] $matches[3]"); + $name = is_string_empty($matches[2]) ? "" : trim("$matches[2] $matches[3]"); $output = $this->page->parseContentShortcut($name, $text, "code"); if (is_null($output)) { $attr = $this->doExtraAttributes("pre", ".$matches[2] $matches[3]"); @@ -3928,7 +3927,7 @@ class YellowMarkdownParser extends MarkdownExtraParser { $text = $matches[1]; $level = $matches[3][0]=="=" ? 1 : 2; $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]); - if (empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text); + if (is_string_empty($attr) && $level>=2) $attr = $this->getIdAttribute($text); $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; return "\n".$this->hashBlock($output)."\n\n"; } @@ -3938,7 +3937,7 @@ class YellowMarkdownParser extends MarkdownExtraParser { $text = $matches[2]; $level = strlen($matches[1]); $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]); - if (empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text); + if (is_string_empty($attr) && $level>=2) $attr = $this->getIdAttribute($text); $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; return "\n".$this->hashBlock($output)."\n\n"; } @@ -3950,7 +3949,7 @@ class YellowMarkdownParser extends MarkdownExtraParser { $title = isset($matches[7]) ? $matches[7] : ""; $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); $output = "<a href=\"".$this->encodeURLAttribute($url)."\""; - if (!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; + if (!is_string_empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; $output .= $attr; $output .= ">".$this->runSpanGamut($text)."</a>"; return $this->hashPart($output); @@ -3966,8 +3965,8 @@ class YellowMarkdownParser extends MarkdownExtraParser { $title = isset($matches[7]) ? $matches[7] : $matches[2]; $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); $output = "<img src=\"".$this->encodeURLAttribute($src)."\""; - if (!empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\""; - if (!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; + if (!is_string_empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\""; + if (!is_string_empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; $output .= $attr; $output .= $this->empty_element_suffix; return $this->hashPart($output); @@ -3996,6 +3995,11 @@ class YellowMarkdownParser extends MarkdownExtraParser { return "<li$attr>".$item."</li>\n"; } + // Handle blockquotes, CommonMark compatible + public function doBlockQuotes($text) { + return preg_replace_callback("/((?>^[ ]*>[ ]?.+\n(.+\n)*)+)/m", array($this, "_doBlockQuotes_callback"), $text); + } + // Handle notice blocks public function doNoticeBlocks($text) { return preg_replace_callback("/((?>^[ ]*!(?!\[)[ ]?.+\n(.+\n)*)+)/m", array($this, "_doNoticeBlocks_callback"), $text); @@ -4013,7 +4017,7 @@ class YellowMarkdownParser extends MarkdownExtraParser { $level = strspn(str_replace(array("![", " "), "", $lines), "!"); $attr = " class=\"notice$level\""; } - if (!empty($text)) { + if (!is_string_empty($text)) { ++$this->noticeLevel; $output = "<div$attr>\n".$this->runBlockGamut($text)."\n</div>"; --$this->noticeLevel; @@ -4023,6 +4027,25 @@ class YellowMarkdownParser extends MarkdownExtraParser { return "\n".$this->hashBlock($output)."\n\n"; } + // Handle footnotes links, normalise ids and links + public function doFootnotesLinks($text) { + if (!is_null($this->footnotes_assembled)) { + $callbackId = function ($matches) { + $id = str_replace(":", "-", $matches[2]); + return "<$matches[1] id=\"$id\" $matches[3]>"; + }; + $text = preg_replace_callback("/<(li|sup) id=\"(fn:\d+)\"(.*?)>/", $callbackId, $text); + $text = preg_replace_callback("/<(li|sup) id=\"(fnref\d*:\d+)\"(.*?)>/", $callbackId, $text); + $callbackHref = function ($matches) { + $href = $this->page->base.$this->page->location.str_replace(":", "-", $matches[2]); + return "<$matches[1] href=\"$href\" $matches[3]>"; + }; + $text = preg_replace_callback("/<(a) href=\"(#fn:\d+)\"(.*?)>/", $callbackHref, $text); + $text = preg_replace_callback("/<(a) href=\"(#fnref\d*:\d+)\"(.*?)>/", $callbackHref, $text); + } + return $text; + } + // Return unique id attribute public function getIdAttribute($text) { $attr = ""; diff --git a/system/extensions/meta.php b/system/extensions/meta.php @@ -1,68 +0,0 @@ -<?php -// Meta extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/meta -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowMeta { - const VERSION = "0.8.12"; - const TYPE = "feature"; - public $yellow; //access to API - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - $this->yellow->system->setDefault("metaDefaultImage", "icon"); - } - - // Handle page extra data - public function onParsePageExtra($page, $name) { - $output = null; - if ($name=="header" && !$page->isError()) { - list($imageUrl, $imageAlt) = $this->getImageInformation($page); - $locale = $this->yellow->text->getText("languageLocale", $page->get("language")); - $output .= "<meta property=\"og:url\" content=\"".htmlspecialchars($page->getUrl().$this->yellow->toolbox->getLocationArguments())."\" />\n"; - $output .= "<meta property=\"og:locale\" content=\"".htmlspecialchars($locale)."\" />\n"; - $output .= "<meta property=\"og:type\" content=\"website\" />\n"; - $output .= "<meta property=\"og:title\" content=\"".$page->getHtml("title")."\" />\n"; - $output .= "<meta property=\"og:site_name\" content=\"".$page->getHtml("sitename")."\" />\n"; - $output .= "<meta property=\"og:description\" content=\"".$page->getHtml("description")."\" />\n"; - $output .= "<meta property=\"og:image\" content=\"".htmlspecialchars($imageUrl)."\" />\n"; - $output .= "<meta property=\"og:image:alt\" content=\"".htmlspecialchars($imageAlt)."\" />\n"; - } - return $output; - } - - // Handle page output data - public function onParsePageOutput($page, $text) { - $output = null; - if ($text && preg_match("/^(.*?)<html(.*?)>(.*)$/s", $text, $matches)) { - $output = $matches[1]."<html".$matches[2]." prefix=\"og: http://ogp.me/ns#\">".$matches[3]; - } - return $output; - } - - // Return image information for page - public function getImageInformation($page) { - if ($page->isExisting("image")) { - $name = $page->get("image"); - $alt = $page->isExisting("imageAlt") ? $page->get("imageAlt") : $page->get("title"); - } elseif (preg_match("/\[image(\s.*?)\]/", $page->getContent(true), $matches)) { - list($name, $alt) = $this->yellow->toolbox->getTextArguments(trim($matches[1])); - if (empty($alt)) $alt = $page->get("title"); - } else { - $name = $this->yellow->system->get("metaDefaultImage"); - $alt = $page->isExisting("imageAlt") ? $page->get("imageAlt") : $page->get("title"); - } - if (!preg_match("/^\w+:/", $name)) { - $location = $name!="icon" ? $this->yellow->system->get("coreImageLocation").$name : - $this->yellow->system->get("coreResourceLocation").$page->get("theme")."-icon.png"; - $url = $this->yellow->lookup->normaliseUrl( - $this->yellow->system->get("coreServerScheme"), - $this->yellow->system->get("coreServerAddress"), - $this->yellow->system->get("coreServerBase"), $location); - } else { - $url = $this->yellow->lookup->normaliseUrl("", "", "", $name); - } - return array($url, $alt); - } -} diff --git a/system/extensions/serve.php b/system/extensions/serve.php @@ -0,0 +1,61 @@ +<?php +// Serve extension, https://github.com/annaesvensson/yellow-serve + +class YellowServe { + const VERSION = "0.8.24"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle command + public function onCommand($command, $text) { + switch ($command) { + case "serve": $statusCode = $this->processCommandServe($command, $text); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + return "serve [url]"; + } + + // Process command to start web server + public function processCommandServe($command, $text) { + list($url) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($url)) $url = "http://localhost:8000/"; + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($url); + if ($scheme=="http" && !is_string_empty($address) && is_string_empty($base)) { + if (!preg_match("/\:\d+$/", $address)) $address .= ":8000"; + if ($this->checkServerSettings("$scheme://$address/")) { + echo "Starting web server. Open a web browser and go to $scheme://$address/\n"; + echo "Press Ctrl+C to quit...\n"; + exec(PHP_BINARY." -S $address yellow.php 2>&1", $outputLines, $returnStatus); + $statusCode = $returnStatus!=0 ? 500 : 200; + if ($statusCode!=200) { + $output = !is_array_empty($outputLines) ? end($outputLines) : "Please check arguments!"; + if (preg_match("/^\[(.*?)\]\s*(.*)$/", $output, $matches)) $output = $matches[2]; + echo "ERROR starting web server: $output\n"; + } + } else { + $statusCode = 400; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + echo "ERROR starting web server: Please configure `CoreServerUrl: auto` in file '$fileName'!\n"; + } + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Check server settings + public function checkServerSettings($url) { + return $this->yellow->system->get("coreServerUrl")=="auto" || + $this->yellow->system->get("coreServerUrl")==$url; + } +} diff --git a/system/extensions/stockholm.php b/system/extensions/stockholm.php @@ -1,12 +1,9 @@ <?php -// Stockholm extension, https://github.com/datenstrom/yellow-extensions/tree/master/themes/stockholm -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. +// Stockholm extension, https://github.com/annaesvensson/yellow-stockholm class YellowStockholm { - const VERSION = "0.8.8"; - const TYPE = "theme"; - public $yellow; //access to API + const VERSION = "0.8.14"; + public $yellow; // access to API // Handle initialisation public function onLoad($yellow) { @@ -15,12 +12,11 @@ class YellowStockholm { // Handle update public function onUpdate($action) { - $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); if ($action=="install") { $this->yellow->system->save($fileName, array("theme" => "stockholm")); } elseif ($action=="uninstall" && $this->yellow->system->get("theme")=="stockholm") { - $theme = reset(array_diff($this->yellow->extensions->getExtensions("theme"), array("stockholm"))); - $this->yellow->system->save($fileName, array("theme" => $theme)); + $this->yellow->system->save($fileName, array("theme" => $this->yellow->system->getDifferent("theme"))); } } } diff --git a/system/extensions/update-current.ini b/system/extensions/update-current.ini @@ -0,0 +1,131 @@ +# Datenstrom Yellow update settings + +Extension: Core +Version: 0.8.125 +Description: Core functionality of your website. +DownloadUrl: https://github.com/annaesvensson/yellow-core/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-core +DocumentationLanguage: en, de, sv +Published: 2023-10-27 17:02:46 +Developer: Anna Svensson +Tag: feature +system/extensions/core.php: core.php, create, update +system/layouts/default.html: default.html, create, update, careful +system/layouts/error.html: error.html, create, update, careful +system/layouts/header.html: header.html, create, update, careful +system/layouts/footer.html: footer.html, create, update, careful +system/layouts/navigation.html: navigation.html, create, update, careful +system/layouts/pagination.html: pagination.html, create, update, careful + +Extension: Edit +Version: 0.8.77 +Description: Edit your website in a web browser. +DownloadUrl: https://github.com/annaesvensson/yellow-edit/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-edit +DocumentationLanguage: en, de, sv +Published: 2023-12-20 23:19:46 +Developer: Anna Svensson +Tag: feature +system/extensions/edit.php: edit.php, create, update +system/extensions/edit.css: edit.css, create, update +system/extensions/edit.js: edit.js, create, update +system/extensions/edit-stack.svg: edit-stack.svg, create, update +system/extensions/edit.woff: edit.woff, delete +content/shared/page-new-default.md: page-new-default.md, create, optional + +Extension: Generate +Version: 0.8.52 +Description: Generate a static website. +DownloadUrl: https://github.com/annaesvensson/yellow-generate/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-generate +DocumentationLanguage: en, de, sv +Published: 2023-06-09 15:56:36 +Developer: Anna Svensson +Tag: feature +system/extensions/generate.php: generate.php, create, update + +Extension: Image +Version: 0.8.19 +Description: Add images and thumbnails. +DownloadUrl: https://github.com/annaesvensson/yellow-image/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-image +DocumentationLanguage: en, de, sv +Published: 2023-04-16 23:50:53 +Developer: Anna Svensson +Tag: feature +system/extensions/image.php: image.php, create, update +media/images/photo.jpg: photo.jpg, create, optional +media/thumbnails/photo-100x40.jpg: photo-100x40.jpg, create, optional + +Extension: Install +Version: 0.8.93 +Description: Install a brand new website. +DownloadUrl: https://github.com/annaesvensson/yellow-install/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-install +DocumentationLanguage: en, de, sv +Published: 2023-12-04 20:53:04 +Developer: Anna Svensson +Status: unlisted +system/extensions/install.php: install.php, create +system/extensions/install-language.bin: install-language.bin, compress @source/yellow-language/, create +system/extensions/install-wiki.bin: install-wiki.bin, compress @source/yellow-wiki/, create +system/extensions/install-blog.bin: install-blog.bin, compress @source/yellow-blog/, create +system/extensions/yellow-system.ini: yellow-system.ini, create +system/extensions/yellow-user.ini: yellow-user.ini, create +system/extensions/yellow-language.ini: yellow-language.ini, create +content/1-home/page.md: 1-home-page.md, create +content/9-about/page.md: 9-about-page.md, create +content/shared/page-error-404.md: page-error-404.md, create +media/downloads/yellow.pdf: yellow.pdf, create +./yellow.php: yellow.php, create +./robots.txt: robots.txt, create + +Extension: Markdown +Version: 0.8.26 +Description: Text formatting for humans. +DownloadUrl: https://github.com/annaesvensson/yellow-markdown/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-markdown +DocumentationLanguage: en, de, sv +Published: 2023-09-18 20:49:33 +Developer: Anna Svensson +Tag: feature +system/extensions/markdown.php: markdown.php, create, update + +Extension: Serve +Version: 0.8.24 +Description: Built-in web server. +DownloadUrl: https://github.com/annaesvensson/yellow-serve/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-serve +DocumentationLanguage: en, de, sv +Published: 2023-05-25 22:35:15 +Developer: Anna Svensson +Tag: feature +system/extensions/serve.php: serve.php, create, update + +Extension: Stockholm +Version: 0.8.14 +Description: Stockholm is a clean theme. +DownloadUrl: https://github.com/annaesvensson/yellow-stockholm/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-stockholm +DocumentationLanguage: en, de, sv +Published: 2022-10-20 12:44:02 +Designer: Anna Svensson +Tag: theme +system/extensions/stockholm.php: stockholm.php, create, update +system/themes/stockholm.css: stockholm.css, create, update, careful +system/themes/stockholm.png: stockholm.png, create +system/themes/stockholm-opensans-bold.woff: stockholm-opensans-bold.woff, create, update, careful +system/themes/stockholm-opensans-light.woff: stockholm-opensans-light.woff, create, update, careful +system/themes/stockholm-opensans-regular.woff: stockholm-opensans-regular.woff, create, update, careful + +Extension: Update +Version: 0.8.97 +Description: Keep your website up to date. +DownloadUrl: https://github.com/annaesvensson/yellow-update/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-update +DocumentationLanguage: en, de, sv +Published: 2023-11-24 21:13:15 +Developer: Anna Svensson +Tag: feature +system/extensions/update.php: update.php, create, update +system/extensions/updatepatch.bin: updatepatch.php, create, additional diff --git a/system/extensions/update-latest.ini b/system/extensions/update-latest.ini @@ -0,0 +1,775 @@ +# Datenstrom Yellow update settings + +Extension: Berlin +Version: 0.8.14 +Description: Berlin is a theme inspired by Dieter Rams. +DownloadUrl: https://github.com/annaesvensson/yellow-berlin/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-berlin +DocumentationLanguage: en, de, sv +Published: 2022-10-20 12:44:35 +Designer: Anna Svensson +Tag: theme +system/extensions/berlin.php: berlin.php, create, update +system/layouts/berlin-default.html: berlin-default.html, create, update, careful +system/themes/berlin.css: berlin.css, create, update, careful +system/themes/berlin.png: berlin.png, create +system/themes/berlin-opensans-bold.woff: berlin-opensans-bold.woff, create, update, careful +system/themes/berlin-opensans-light.woff: berlin-opensans-light.woff, create, update, careful +system/themes/berlin-opensans-regular.woff: berlin-opensans-regular.woff, create, update, careful + +Extension: Blog +Version: 0.8.30 +Description: Blog for your website. +DownloadUrl: https://github.com/annaesvensson/yellow-blog/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-blog +DocumentationLanguage: en, de, sv +Published: 2023-11-01 17:49:02 +Developer: Anna Svensson +Tag: feature +system/extensions/blog.php: blog.php, create, update +system/layouts/blog.html: blog.html, create, update, careful +system/layouts/blog-start.html: blog-start.html, create, update, careful +content/shared/page-new-blog.md: page-new-blog.md, create, optional +content/2-blog/page.md: page.md, create, optional +content/2-blog/2020-04-07-blog-example-page.md: 2020-04-07-blog-example-page.md, create, optional +content/2-blog/2020-12-06-made-for-people.md: 2020-12-06-made-for-people.md, create, optional + +Extension: Breadcrumb +Version: 0.8.10 +Description: Breadcrumb navigation. +DownloadUrl: https://github.com/annaesvensson/yellow-breadcrumb/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-breadcrumb +DocumentationLanguage: en, de, sv +Published: 2022-10-24 17:50:24 +Developer: Anna Svensson +Tag: feature +system/extensions/breadcrumb.php: breadcrumb.php, create, update + +Extension: Bundle +Version: 0.8.31 +Description: Bundle website files. +DownloadUrl: https://github.com/annaesvensson/yellow-bundle/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-bundle +DocumentationLanguage: en, de, sv +Published: 2023-11-28 16:26:34 +Developer: Anna Svensson +Tag: feature +system/extensions/bundle.php: bundle.php, create, update + +Extension: Catalan +Version: 0.8.43 +Description: Catalan language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/catalan.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/catalan +Published: 2023-04-13 22:54:43 +Translator: Andreu Ferrer +Tag: language +system/extensions/catalan.php: catalan.php, create, update + +Extension: Check +Version: 0.8.2 +Description: Find broken links. +DownloadUrl: https://github.com/annaesvensson/yellow-check/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-check +DocumentationLanguage: en, de, sv +Published: 2023-06-09 15:55:12 +Developer: Anna Svensson +Tag: feature +system/extensions/check.php: check.php, create, update + +Extension: Chinese +Version: 0.8.43 +Description: Chinese language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/chinese.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/chinese +Published: 2023-04-13 22:53:25 +Translator: Hyson Lee +Tag: language +system/extensions/chinese.php: chinese.php, create, update + +Extension: Contact +Version: 0.8.23 +Description: Email contact page. +DownloadUrl: https://github.com/annaesvensson/yellow-contact/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-contact +DocumentationLanguage: en, de, sv +Published: 2023-05-18 17:59:29 +Developer: Anna Svensson +Tag: feature +system/extensions/contact.php: contact.php, create, update +system/layouts/contact.html: contact.html, create, update, careful +content/contact/page.md: page.md, create, optional + +Extension: Copenhagen +Version: 0.8.15 +Description: Copenhagen is a beautiful theme. +DownloadUrl: https://github.com/annaesvensson/yellow-copenhagen/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-copenhagen +DocumentationLanguage: en, de, sv +Published: 2023-08-21 09:04:08 +Designer: Anna Svensson +Tag: theme +system/extensions/copenhagen.php: copenhagen.php, create, update +system/themes/copenhagen.css: copenhagen.css, create, update, careful +system/themes/copenhagen.png: copenhagen.png, create + +Extension: Core +Version: 0.8.125 +Description: Core functionality of your website. +DownloadUrl: https://github.com/annaesvensson/yellow-core/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-core +DocumentationLanguage: en, de, sv +Published: 2023-10-27 17:02:46 +Developer: Anna Svensson +Tag: feature +system/extensions/core.php: core.php, create, update +system/layouts/default.html: default.html, create, update, careful +system/layouts/error.html: error.html, create, update, careful +system/layouts/header.html: header.html, create, update, careful +system/layouts/footer.html: footer.html, create, update, careful +system/layouts/navigation.html: navigation.html, create, update, careful +system/layouts/pagination.html: pagination.html, create, update, careful + +Extension: Czech +Version: 0.8.43 +Description: Czech language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/czech.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/czech +Published: 2023-04-13 22:59:43 +Translator: Ufo Vyhuleny +Tag: language +system/extensions/czech.php: czech.php, create, update + +Extension: Danish +Version: 0.8.43 +Description: Danish language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/danish.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/danish +Published: 2023-04-13 23:07:01 +Translator: David Garcia +Tag: language +system/extensions/danish.php: danish.php, create, update + +Extension: Draft +Version: 0.8.18 +Description: Support for draft pages. +DownloadUrl: https://github.com/annaesvensson/yellow-draft/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-draft +DocumentationLanguage: en, de, sv +Published: 2023-05-19 13:21:26 +Developer: Anna Svensson +Tag: feature +system/extensions/draft.php: draft.php, create, update + +Extension: Dutch +Version: 0.8.43 +Description: Dutch language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/dutch.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/dutch +Published: 2023-04-13 23:07:33 +Translator: Robin Vannieuwenhuijse +Tag: language +system/extensions/dutch.php: dutch.php, create, update + +Extension: Edit +Version: 0.8.77 +Description: Edit your website in a web browser. +DownloadUrl: https://github.com/annaesvensson/yellow-edit/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-edit +DocumentationLanguage: en, de, sv +Published: 2023-12-20 23:19:46 +Developer: Anna Svensson +Tag: feature +system/extensions/edit.php: edit.php, create, update +system/extensions/edit.css: edit.css, create, update +system/extensions/edit.js: edit.js, create, update +system/extensions/edit-stack.svg: edit-stack.svg, create, update +system/extensions/edit.woff: edit.woff, delete +content/shared/page-new-default.md: page-new-default.md, create, optional + +Extension: Emoji +Version: 0.8.14 +Description: Lots and lots of emoji. +DownloadUrl: https://github.com/annaesvensson/yellow-emoji/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-emoji +DocumentationLanguage: en, de, sv +Published: 2022-11-16 10:24:57 +Developer: Anna Svensson +Tag: feature +system/extensions/emoji.php: emoji.php, create, update +system/extensions/emoji.css: emoji.css, create, update +system/extensions/emoji-default-stack.svg: emoji-default-stack.svg, create, update +system/extensions/emoji-extra1-stack.svg: emoji-extra1-stack.svg, create, update +system/extensions/emoji-extra2-stack.svg: emoji-extra2-stack.svg, create, update +system/extensions/emoji-extra3-stack.svg: emoji-extra3-stack.svg, create, update +system/extensions/emoji-extra4-stack.svg: emoji-extra4-stack.svg, create, update +system/extensions/emoji-extra5-stack.svg: emoji-extra5-stack.svg, create, update +system/extensions/emoji-extra6-stack.svg: emoji-extra6-stack.svg, create, update +system/extensions/emoji-extra7-stack.svg: emoji-extra7-stack.svg, create, update +system/extensions/emoji-flags-stack.svg: emoji-flags-stack.svg, create, update + +Extension: English +Version: 0.8.43 +Description: English language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/english.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/english +Published: 2023-04-13 22:54:43 +Translator: Mark Seuffert +Tag: language +system/extensions/english.php: english.php, create, update + +Extension: Feed +Version: 0.8.25 +Description: Feed with recent changes. +DownloadUrl: https://github.com/annaesvensson/yellow-feed/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-feed +DocumentationLanguage: en, de, sv +Published: 2023-11-04 01:58:26 +Developer: Anna Svensson +Tag: feature +system/extensions/feed.php: feed.php, create, update +system/layouts/feed.html: feed.html, create, update, careful +content/feed/page.md: page.md, create, optional + +Extension: French +Version: 0.8.43 +Description: French language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/french.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/french +Published: 2023-04-13 23:07:59 +Translator: Juh Nibreh +Tag: language +system/extensions/french.php: french.php, create, update + +Extension: Gallery +Version: 0.8.18 +Description: Image gallery with popup. +DownloadUrl: https://github.com/annaesvensson/yellow-gallery/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-gallery +DocumentationLanguage: en, de, sv +Published: 2022-11-04 09:39:17 +Developer: Anna Svensson +Tag: feature +system/extensions/gallery.php: gallery.php, create, update +system/extensions/gallery.js: gallery.js, create, update +system/extensions/gallery.css: gallery.css, create, update +system/extensions/gallery-photoswipe.min.js: gallery-photoswipe.min.js, create, update +system/extensions/gallery-default-skin.png: gallery-default-skin.png, create, update +system/extensions/gallery-default-skin.svg: gallery-default-skin.svg, create, update +system/extensions/gallery-preloader.gif: gallery-preloader.gif, create, update + +Extension: Generate +Version: 0.8.52 +Description: Generate a static website. +DownloadUrl: https://github.com/annaesvensson/yellow-generate/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-generate +DocumentationLanguage: en, de, sv +Published: 2023-06-09 15:56:36 +Developer: Anna Svensson +Tag: feature +system/extensions/generate.php: generate.php, create, update + +Extension: German +Version: 0.8.43 +Description: German language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/german.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/german +Published: 2023-04-13 22:54:43 +Translator: David Fehrmann +Tag: language +system/extensions/german.php: german.php, create, update + +Extension: Googlecalendar +Version: 0.8.17 +Description: Embed Google calendar. +DownloadUrl: https://github.com/annaesvensson/yellow-googlecalendar/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-googlecalendar +DocumentationLanguage: en, de, sv +Published: 2023-04-18 01:21:56 +Developer: Anna Svensson +Tag: feature +system/extensions/googlecalendar.php: googlecalendar.php, create, update +system/extensions/googlecalendar.js: googlecalendar.js, create, update +system/extensions/googlecalendar.css: googlecalendar.css, create, update + +Extension: Googlemap +Version: 0.8.9 +Description: Embed Google map. +DownloadUrl: https://github.com/annaesvensson/yellow-googlemap/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-googlemap +DocumentationLanguage: en, de, sv +Published: 2023-04-18 01:19:46 +Developer: Anna Svensson +Tag: feature +system/extensions/googlemap.php: googlemap.php, create, update + +Extension: Helloworld +Version: 0.8.15 +Description: Make animated text. +DownloadUrl: https://github.com/schulle4u/yellow-helloworld/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/schulle4u/yellow-helloworld +Published: 2020-08-13 16:12:30 +Developer: Steffen Schultz +Tag: example, feature +system/extensions/helloworld.php: helloworld.php, create, update +system/extensions/helloworld.js: helloworld.js, create, update +system/extensions/helloworld.css: helloworld.css, create, update + +Extension: Help +Version: 0.8.23 +Description: Help for your website. +DownloadUrl: https://github.com/annaesvensson/yellow-help/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-help +DocumentationLanguage: en, de, sv +Published: 2023-05-18 20:09:11 +Developer: Anna Svensson +Tag: feature +system/extensions/help.php: help.php, create, update +content/9-help/api-for-developers.md: api-for-developers.md, create, optional, multi-language +content/9-help/contributing-guidelines.md: contributing-guidelines.md, create, optional, multi-language +content/9-help/how-to-change-the-content.md: how-to-change-the-content.md, create, optional, multi-language +content/9-help/how-to-change-the-media.md: how-to-change-the-media.md, create, optional, multi-language +content/9-help/how-to-change-the-system.md: how-to-change-the-system.md, create, optional, multi-language +content/9-help/how-to-customise-a-language.md: how-to-customise-a-language.md, create, optional, multi-language +content/9-help/how-to-customise-a-layout.md: how-to-customise-a-layout.md, create, optional, multi-language +content/9-help/how-to-customise-a-theme.md: how-to-customise-a-theme.md, create, optional, multi-language +content/9-help/how-to-get-started.md: how-to-get-started.md, create, optional, multi-language +content/9-help/how-to-make-a-small-blog.md: how-to-make-a-small-blog.md, create, optional, multi-language +content/9-help/how-to-make-a-small-website.md: how-to-make-a-small-website.md, create, optional, multi-language +content/9-help/how-to-make-a-small-wiki.md: how-to-make-a-small-wiki.md, create, optional, multi-language +content/9-help/page.md: page.md, create, optional, multi-language +content/9-help/troubleshooting.md: troubleshooting.md, create, optional, multi-language +content/9-help/what-s-new.md: what-s-new.md, create, optional, multi-language +media/images/help-photo.jpg: help-photo.jpg, create, optional +media/images/language-de.png: language-de.png, create, optional +media/images/language-en.png: language-en.png, create, optional +media/images/language-sv.png: language-sv.png, create, optional + +Extension: Highlight +Version: 0.8.16 +Description: Highlight source code. +DownloadUrl: https://github.com/annaesvensson/yellow-highlight/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-highlight +DocumentationLanguage: en, de, sv +Published: 2022-12-22 16:37:25 +Developer: Anna Svensson +Tag: feature +system/extensions/highlight.php: highlight.php, create, update +system/extensions/highlight.css: highlight.css, create, update +system/extensions/highlight-cpp.json: highlight-cpp.json, create, update +system/extensions/highlight-css.json: highlight-css.json, create, update +system/extensions/highlight-javascript.json: highlight-javascript.json, create, update +system/extensions/highlight-json.json: highlight-json.json, create, update +system/extensions/highlight-lua.json: highlight-lua.json, create, update +system/extensions/highlight-php.json: highlight-php.json, create, update +system/extensions/highlight-python.json: highlight-python.json, create, update +system/extensions/highlight-xml.json: highlight-xml.json, create, update +system/extensions/highlight-yaml.json: highlight-yaml.json, create, update + +Extension: Hungarian +Version: 0.8.43 +Description: Hungarian language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/hungarian.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/hungarian +Published: 2023-04-13 23:08:22 +Translator: Ádám Tuba +Tag: language +system/extensions/hungarian.php: hungarian.php, create, update + +Extension: Icon +Version: 0.8.14 +Description: Icons and symbols. +DownloadUrl: https://github.com/annaesvensson/yellow-icon/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-icon +DocumentationLanguage: en, de, sv +Published: 2022-11-16 11:09:07 +Developer: Anna Svensson +Tag: feature +system/extensions/icon.php: icon.php, create, update +system/extensions/icon.css: icon.css, create, update +system/extensions/icon.woff: icon.woff, create, update + +Extension: Image +Version: 0.8.19 +Description: Add images and thumbnails. +DownloadUrl: https://github.com/annaesvensson/yellow-image/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-image +DocumentationLanguage: en, de, sv +Published: 2023-04-16 23:50:53 +Developer: Anna Svensson +Tag: feature +system/extensions/image.php: image.php, create, update +media/images/photo.jpg: photo.jpg, create, optional +media/thumbnails/photo-100x40.jpg: photo-100x40.jpg, create, optional + +Extension: Instagram +Version: 0.8.7 +Description: Embed Instagram photos. +DocumentationUrl: https://github.com/GiovanniSalmeri/yellow-instagram +DownloadUrl: https://github.com/GiovanniSalmeri/yellow-instagram/archive/refs/heads/main.zip +Published: 2022-10-15 17:54:00 +Developer: Giovanni Salmeri +Tag: feature +system/extensions/instagram.php: instagram.php, create, update +system/extensions/instagram.js: instagram.js, create, update + +Extension: Italian +Version: 0.8.43 +Description: Italian language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/italian.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/italian +Published: 2023-04-13 23:08:39 +Translator: Giovanni Salmeri +Tag: language +system/extensions/italian.php: italian.php, create, update + +Extension: Japanese +Version: 0.8.43 +Description: Japanese language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/japanese.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/japanese +Published: 2023-04-13 23:03:24 +Translator: Yuhko Senuma, Tomonori Ikeda +Tag: language +system/extensions/japanese.php: japanese.php, create, update + +Extension: Karlskrona +Version: 0.8.18 +Description: Karlskrona is a semantic theme. +DownloadUrl: https://github.com/pftnhr/yellow-karlskrona/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/pftnhr/yellow-karlskrona +Published: 2023-09-11 20:00:13 +Designer: Robert Pfotenhauer +Tag: theme +system/extensions/karlskrona.php: karlskrona.php, create, update +system/layouts/karlskrona-blog-start.html: karlskrona-blog-start.html, create, update, careful +system/layouts/karlskrona-blog.html: karlskrona-blog.html, create, update, careful +system/layouts/karlskrona-default.html: karlskrona-default.html, create, update, careful +system/layouts/karlskrona-error.html: karlskrona-error.html, create, update, careful +system/layouts/karlskrona-footer.html: karlskrona-footer.html, create, update, careful +system/layouts/karlskrona-header.html: karlskrona-header.html, create, update, careful +system/layouts/karlskrona-navigation.html: karlskrona-navigation.html, create, update, careful +system/layouts/karlskrona-pagination.html: karlskrona-pagination.html, create, update, careful +system/layouts/karlskrona-wiki-start.html: karlskrona-wiki-start.html, create, update, careful +system/layouts/karlskrona-wiki.html: karlskrona-wiki.html, create, update, careful +system/themes/karlskrona.css: karlskrona.css, create, update, careful +system/themes/karlskrona.png: karlskrona.png, create + +Extension: Markdown +Version: 0.8.26 +Description: Text formatting for humans. +DownloadUrl: https://github.com/annaesvensson/yellow-markdown/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-markdown +DocumentationLanguage: en, de, sv +Published: 2023-09-18 20:49:33 +Developer: Anna Svensson +Tag: feature +system/extensions/markdown.php: markdown.php, create, update + +Extension: Meta +Version: 0.8.17 +Description: Meta data for humans and machines. +DownloadUrl: https://github.com/annaesvensson/yellow-meta/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-meta +DocumentationLanguage: en, de, sv +Published: 2023-05-19 00:58:40 +Developer: Anna Svensson +Tag: feature +system/extensions/meta.php: meta.php, create, update + +Extension: Norwegian +Version: 0.8.43 +Description: Norwegian language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/norwegian.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/norwegian +Published: 2023-04-13 23:08:53 +Translator: Per Arne Solvik +Tag: language +system/extensions/norwegian.php: norwegian.php, create, update + +Extension: Paris +Version: 0.8.14 +Description: Paris is an elegant theme. +DownloadUrl: https://github.com/annaesvensson/yellow-paris/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-paris +DocumentationLanguage: en, de, sv +Published: 2022-10-20 12:45:32 +Designer: Anna Svensson +Tag: theme +system/extensions/paris.php: paris.php, create, update +system/layouts/paris-navigation.html: paris-navigation.html, create, update, careful +system/themes/paris.css: paris.css, create, update, careful +system/themes/paris.png: paris.png, create +system/themes/paris-logo.png: paris-logo.png, create +system/themes/paris-quote.png: paris-quote.png, create +system/themes/paris-opensans-bold.woff: paris-opensans-bold.woff, create, update, careful +system/themes/paris-opensans-light.woff: paris-opensans-light.woff, create, update, careful +system/themes/paris-opensans-regular.woff: paris-opensans-regular.woff, create, update, careful + +Extension: Parsedown +Version: 0.8.26 +Description: Text formatting for humans. +DownloadUrl: https://github.com/annaesvensson/yellow-parsedown/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-parsedown +DocumentationLanguage: en, de, sv +Published: 2023-09-18 20:49:49 +Developer: Anna Svensson +Tag: feature +system/extensions/parsedown.php: parsedown.php, create, update + +Extension: Polish +Version: 0.8.43 +Description: Polish language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/polish.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/polish +Published: 2023-04-13 23:09:08 +Translator: Paweł Klockiewicz, Kanbeq +Tag: language +system/extensions/polish.php: polish.php, create, update + +Extension: Portuguese +Version: 0.8.43 +Description: Portuguese language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/portuguese.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/portuguese +Published: 2023-04-13 23:09:38 +Translator: Al Garcia +Tag: language +system/extensions/portuguese.php: portuguese.php, create, update + +Extension: Previousnext +Version: 0.8.18 +Description: Show links to previous/next page. +DownloadUrl: https://github.com/annaesvensson/yellow-previousnext/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-previousnext +DocumentationLanguage: en, de, sv +Published: 2023-04-03 09:37:00 +Developer: Anna Svensson +Tag: feature +system/extensions/previousnext.php: previousnext.php, create, update + +Extension: Private +Version: 0.8.13 +Tag: feature, page, private, security +Description: Support for password-protected pages. +DownloadUrl: https://github.com/schulle4u/yellow-private/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/schulle4u/yellow-private +DocumentationLanguage: en, de +Published: 2023-05-22 14:35:25 +Developer: Steffen Schultz +system/extensions/private.php: private.php, create, update + +Extension: Publish +Version: 0.8.70 +Description: Make and publish extensions. +DownloadUrl: https://github.com/annaesvensson/yellow-publish/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-publish +DocumentationLanguage: en, de, sv +Published: 2023-12-11 18:37:01 +Developer: Anna Svensson +Tag: feature +system/extensions/publish.php: publish.php, create, update + +Extension: Russian +Version: 0.8.43 +Description: Russian language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/russian.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/russian +Published: 2023-04-13 23:09:57 +Translator: Сергей Ворон +Tag: language +system/extensions/russian.php: russian.php, create, update + +Extension: Search +Version: 0.8.29 +Description: Full-text search. +DownloadUrl: https://github.com/annaesvensson/yellow-search/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-search +DocumentationLanguage: en, de, sv +Published: 2023-10-13 12:38:07 +Developer: Anna Svensson +Tag: feature +system/extensions/search.php: search.php, create, update +system/layouts/search.html: search.html, create, update, careful +content/search/page.md: page.md, create, optional + +Extension: Serve +Version: 0.8.24 +Description: Built-in web server. +DownloadUrl: https://github.com/annaesvensson/yellow-serve/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-serve +DocumentationLanguage: en, de, sv +Published: 2023-05-25 22:35:15 +Developer: Anna Svensson +Tag: feature +system/extensions/serve.php: serve.php, create, update + +Extension: Sitemap +Version: 0.8.15 +Description: Sitemap with all pages. +DownloadUrl: https://github.com/annaesvensson/yellow-sitemap/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-sitemap +DocumentationLanguage: en, de, sv +Published: 2023-10-26 16:26:39 +Developer: Anna Svensson +Tag: feature +system/extensions/sitemap.php: sitemap.php, create, update +system/layouts/sitemap.html: sitemap.html, create, update, careful +content/sitemap/page.md: page.md, create, optional + +Extension: Slider +Version: 0.8.18 +Description: Image gallery with slider. +DownloadUrl: https://github.com/annaesvensson/yellow-slider/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-slider +DocumentationLanguage: en, de, sv +Published: 2022-11-04 09:39:38 +Developer: Anna Svensson +Tag: feature +system/extensions/slider.php: slider.php, create, update +system/extensions/slider.js: slider.js, create, update +system/extensions/slider.css: slider.css, create, update +system/extensions/slider-splide.min.js: slider-splide.min.js, create, update + +Extension: Slovak +Version: 0.8.43 +Description: Slovak language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/slovak.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/slovak +Published: 2023-04-13 23:10:38 +Translator: Ádám Tuba +Tag: language +system/extensions/slovak.php: slovak.php, create, update + +Extension: Spanish +Version: 0.8.43 +Description: Spanish language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/spanish.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/spanish +Published: 2023-04-13 23:11:01 +Translator: Al Garcia, David Garcia +Tag: language +system/extensions/spanish.php: spanish.php, create, update + +Extension: Stockholm +Version: 0.8.14 +Description: Stockholm is a clean theme. +DownloadUrl: https://github.com/annaesvensson/yellow-stockholm/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-stockholm +DocumentationLanguage: en, de, sv +Published: 2022-10-20 12:44:02 +Designer: Anna Svensson +Tag: theme +system/extensions/stockholm.php: stockholm.php, create, update +system/themes/stockholm.css: stockholm.css, create, update, careful +system/themes/stockholm.png: stockholm.png, create +system/themes/stockholm-opensans-bold.woff: stockholm-opensans-bold.woff, create, update, careful +system/themes/stockholm-opensans-light.woff: stockholm-opensans-light.woff, create, update, careful +system/themes/stockholm-opensans-regular.woff: stockholm-opensans-regular.woff, create, update, careful + +Extension: Swedish +Version: 0.8.43 +Description: Swedish language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/swedish.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/swedish +Published: 2023-04-13 22:54:43 +Translator: Anna Svensson +Tag: language +system/extensions/swedish.php: swedish.php, create, update + +Extension: Toc +Version: 0.8.11 +Description: Table of contents. +DownloadUrl: https://github.com/annaesvensson/yellow-toc/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-toc +DocumentationLanguage: en, de, sv +Published: 2023-11-01 14:32:36 +Developer: Anna Svensson +Tag: feature +system/extensions/toc.php: toc.php, create, update + +Extension: Traffic +Version: 0.8.32 +Description: Create traffic analytics from log files. +DownloadUrl: https://github.com/annaesvensson/yellow-traffic/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-traffic +DocumentationLanguage: en, de, sv +Published: 2023-06-09 15:38:36 +Developer: Anna Svensson +Tag: feature +system/extensions/traffic.php: traffic.php, create, update + +Extension: Turkish +Version: 0.8.43 +Description: Turkish language. +DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/turkish.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/turkish +Published: 2023-04-13 23:11:16 +Translator: Osman Kars +Tag: language +system/extensions/turkish.php: turkish.php, create, update + +Extension: Update +Version: 0.8.97 +Description: Keep your website up to date. +DownloadUrl: https://github.com/annaesvensson/yellow-update/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-update +DocumentationLanguage: en, de, sv +Published: 2023-11-24 21:13:15 +Developer: Anna Svensson +Tag: feature +system/extensions/update.php: update.php, create, update +system/extensions/updatepatch.bin: updatepatch.php, create, additional + +Extension: Wiki +Version: 0.8.30 +Description: Wiki for your website. +DownloadUrl: https://github.com/annaesvensson/yellow-wiki/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-wiki +DocumentationLanguage: en, de, sv +Published: 2023-11-01 17:49:10 +Developer: Anna Svensson +Tag: feature +system/extensions/wiki.php: wiki.php, create, update +system/layouts/wiki.html: wiki.html, create, update, careful +system/layouts/wiki-start.html: wiki-start.html, create, update, careful +content/shared/page-new-wiki.md: page-new-wiki.md, create, optional +content/2-wiki/page.md: page.md, create, optional +content/2-wiki/wiki-example-page.md: wiki-example-page.md, create, optional + +Extension: Wittstock +Version: 0.8.28 +Description: Wittstock is a classless theme. +DownloadUrl: https://github.com/schulle4u/yellow-wittstock/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/schulle4u/yellow-wittstock +DocumentationLanguage: en, de +Published: 2023-10-27 13:55:23 +Designer: Steffen Schultz +Tag: classless, theme +system/extensions/wittstock.php: wittstock.php, create, update +system/themes/wittstock.css: wittstock.css, create, update, careful +system/layouts/wittstock-blog.html: wittstock-blog.html, create, update, careful +system/layouts/wittstock-blog-start.html: wittstock-blog-start.html, create, update, careful +system/layouts/wittstock-contact.html: wittstock-contact.html, create, update, careful +system/layouts/wittstock-default.html: wittstock-default.html, create, update, careful +system/layouts/wittstock-error.html: wittstock-error.html, create, update, careful +system/layouts/wittstock-feed.html: wittstock-feed.html, create, update, careful +system/layouts/wittstock-footer.html: wittstock-footer.html, create, update, careful +system/layouts/wittstock-header.html: wittstock-header.html, create, update, careful +system/layouts/wittstock-navigation.html: wittstock-navigation.html, create, update, careful +system/layouts/wittstock-pagination.html: wittstock-pagination.html, create, update, careful +system/layouts/wittstock-search.html: wittstock-search.html, create, update, careful +system/layouts/wittstock-sitemap.html: wittstock-sitemap.html, create, update, careful +system/layouts/wittstock-wiki.html: wittstock-wiki.html, create, update, careful +system/layouts/wittstock-wiki-start.html: wittstock-wiki-start.html, create, update, careful + +Extension: Youtube +Version: 0.8.7 +Description: Embed Youtube videos. +DownloadUrl: https://github.com/annaesvensson/yellow-youtube/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-youtube +DocumentationLanguage: en, de, sv +Published: 2023-04-18 01:25:49 +Developer: Anna Svensson +Tag: feature +system/extensions/youtube.php: youtube.php, create, update diff --git a/system/extensions/update.php b/system/extensions/update.php @@ -1,43 +1,59 @@ <?php -// Update extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/update -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. +// Update extension, https://github.com/annaesvensson/yellow-update class YellowUpdate { - const VERSION = "0.8.23"; - const TYPE = "feature"; + const VERSION = "0.8.97"; const PRIORITY = "2"; - public $yellow; //access to API - public $updates; //number of updates + public $yellow; // access to API + public $extensions; // number of extensions // Handle initialisation public function onLoad($yellow) { $this->yellow = $yellow; - $this->yellow->system->setDefault("updateExtensionUrl", "https://github.com/datenstrom/yellow-extensions"); - $this->yellow->system->setDefault("updateExtensionDirectory", "/Users/yourname/Documents/GitHub/"); + $this->yellow->system->setDefault("updateCurrentRelease", "none"); + $this->yellow->system->setDefault("updateLatestUrl", "auto"); + $this->yellow->system->setDefault("updateLatestFile", "update-latest.ini"); + $this->yellow->system->setDefault("updateCurrentFile", "update-current.ini"); $this->yellow->system->setDefault("updateExtensionFile", "extension.ini"); - $this->yellow->system->setDefault("updateVersionFile", "version.ini"); - $this->yellow->system->setDefault("updateWaffleFile", "waffle.ini"); - $this->yellow->system->setDefault("updateNotification", "none"); + $this->yellow->system->setDefault("updateEventPending", "none"); + $this->yellow->system->setDefault("updateEventDaily", "0"); + $this->yellow->system->setDefault("updateTrashTimeout", "7776660"); + } + + // Handle update + public function onUpdate($action) { + if ($action=="clean" || $action=="daily") { + $statusCode = 200; + $path = $this->yellow->system->get("coreExtensionDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.download$/", false, false) as $entry) { + if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + } + if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!"); + $statusCode = 200; + $path = $this->yellow->system->get("coreTrashDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) { + $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout"); + if ($expire<=time() && !$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + } + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, true) as $entry) { + $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout"); + if ($expire<=time() && !$this->yellow->toolbox->deleteDirectory($entry)) $statusCode = 500; + } + if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!"); + } } // Handle request public function onRequest($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->yellow->lookup->isContentFile($fileName) && $this->isExtensionPending()) { - $statusCode = $this->processRequestPending($scheme, $address, $base, $location, $fileName); - } - return $statusCode; + return $this->processRequestPending($scheme, $address, $base, $location, $fileName); } // Handle command public function onCommand($command, $text) { - $statusCode = 0; - if ($this->isExtensionPending()) $statusCode = $this->processCommandPending(); + $statusCode = $this->processCommandPending(); if ($statusCode==0) { switch ($command) { case "about": $statusCode = $this->processCommandAbout($command, $text); break; - case "clean": $statusCode = $this->processCommandClean($command, $text); break; case "install": $statusCode = $this->processCommandInstall($command, $text); break; case "uninstall": $statusCode = $this->processCommandUninstall($command, $text); break; case "update": $statusCode = $this->processCommandUpdate($command, $text); break; @@ -49,379 +65,184 @@ class YellowUpdate { // Handle command help public function onCommandHelp() { - $help = "about\n"; - $help .= "install [extension]\n"; - $help .= "uninstall [extension]\n"; - $help .= "update [extension]\n"; - return $help; + return array("about [extension]", "install [extension]", "uninstall [extension]", "update [extension]"); } - - // Handle update - public function onUpdate($action) { - if ($action=="update") { //TODO: remove later, converts old content settings - if ($this->yellow->system->isExisting("multiLanguageMode")) { - $coreMultiLanguageMode = $this->yellow->system->get("multiLanguageMode"); - $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); - $this->yellow->system->save($fileName, array("coreMultiLanguageMode" => $coreMultiLanguageMode)); - $path = $this->yellow->system->get("coreContentDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/^.*\.md$/", true, false) as $entry) { - $fileData = $fileDataNew = $this->yellow->toolbox->readFile($entry); - $fileStatusUnlisted = false; - $tokens = explode("/", substru($entry, strlenu($path))); - for ($i=0; $i<count($tokens)-1; ++$i) { - if (!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i]) && $tokens[$i]!="shared") { - $fileStatusUnlisted = true; - break; - } - } - $fileDataNew = preg_replace("/Status: hidden/i", "Status: shared", $fileDataNew); - $fileDataNew = preg_replace("/Status: ignore/i", "Build: exclude", $fileDataNew); - if ($fileStatusUnlisted && empty($this->yellow->toolbox->getMetaData($fileDataNew, "status"))) { - $fileDataNew = $this->yellow->toolbox->setMetaData($fileDataNew, "status", "unlisted"); - } - if ($fileData!=$fileDataNew) { - $modified = $this->yellow->toolbox->getFileModified($entry); - if (!$this->yellow->toolbox->deleteFile($entry) || - !$this->yellow->toolbox->createFile($entry, $fileDataNew) || - !$this->yellow->toolbox->modifyFile($entry, $modified)) { - $this->yellow->log("error", "Can't write file '$entry'!"); - } - } - } - } - } - if ($action=="update") { //TODO: remove later, converts old layout files - if ($this->yellow->system->isExisting("coreLayoutDir")) { - $path = $this->yellow->system->get("coreLayoutDir"); - foreach ($this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/^.*\.html$/", true, false) as $entry) { - $fileData = $fileDataNew = $this->yellow->toolbox->readFile($entry); - $fileDataNew = str_replace("yellow->getLayoutArgs", "yellow->getLayoutArguments", $fileDataNew); - $fileDataNew = str_replace("toolbox->getLocationArgs", "toolbox->getLocationArguments", $fileDataNew); - $fileDataNew = str_replace("toolbox->getTextArgs", "toolbox->getTextArguments", $fileDataNew); - $fileDataNew = str_replace("toolbox->normaliseArgs", "toolbox->normaliseArguments", $fileDataNew); - $fileDataNew = str_replace("toolbox->isLocationArgs", "toolbox->isLocationArguments", $fileDataNew); - $fileDataNew = str_replace("\$this->yellow->page->get(\"navigation\")", "\"navigation\"", $fileDataNew); - $fileDataNew = str_replace("\$this->yellow->page->get(\"header\")", "\"header\"", $fileDataNew); - $fileDataNew = str_replace("\$this->yellow->page->get(\"sidebar\")", "\"sidebar\"", $fileDataNew); - $fileDataNew = str_replace("\$this->yellow->page->get(\"footer\")", "\"footer\"", $fileDataNew); - if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($entry, $fileDataNew)) { - $this->yellow->log("error", "Can't write file '$entry'!"); - } - } - } - } - if ($action=="update") { //TODO: remove later, converts old commandline - if ($this->yellow->system->isExisting("coreStaticDir")) { - $fileName = "yellow.php"; - $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName); - $fileDataNew = str_replace("make websites", "make small websites", $fileDataNew); - $fileDataNew = str_replace("command(\$argv[1], \$argv[2], \$argv[3], \$argv[4], \$argv[5], \$argv[6], \$argv[7])", "command()", $fileDataNew); - if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { - $this->yellow->log("error", "Can't write file '$fileName'!"); + + // Parse page content shortcut + public function onParseContentShortcut($page, $name, $text, $type) { + $output = null; + if ($name=="yellow" && $type=="inline") { + if ($text=="about") { + list($dummy, $settingsCurrent) = $this->getExtensionSettings(false); + $output = "Datenstrom Yellow ".YellowCore::RELEASE."<br />\n"; + foreach ($settingsCurrent as $key=>$value) { + $output .= ucfirst($key)." ".$value->get("version")."<br />\n"; } } - } - if ($action=="startup") { - if ($this->yellow->system->get("updateNotification")!="none") { - foreach (explode(",", $this->yellow->system->get("updateNotification")) as $token) { - list($extension, $action) = $this->yellow->toolbox->getTextList($token, "/", 2); - if ($this->yellow->extensions->isExisting($extension) && ($action!="startup" && $action!="uninstall")) { - $value = $this->yellow->extensions->extensions[$extension]; - if (method_exists($value["obj"], "onUpdate")) $value["obj"]->onUpdate($action); - } - } - $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); - $this->yellow->system->save($fileName, array("updateNotification" => "none")); - $fileData = $this->yellow->toolbox->readFile($fileName); - $fileDataHeader = $fileDataSettings = $fileDataFooter = ""; - $settings = new YellowDataCollection(); - $settings->exchangeArray($this->yellow->system->settingsDefaults->getArrayCopy()); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (empty($fileDataHeader) && preg_match("/^\#/", $line)) { - $fileDataHeader = $line; - } elseif (!empty($matches[1]) && isset($settings[$matches[1]])) { - $settings[$matches[1]] = $matches[2]; - } elseif (!empty($matches[1]) && substru($matches[1], 0, 1)!="#") { - $fileDataFooter .= "# $line"; - } elseif (!empty($matches[1])) { - $fileDataFooter .= $line; + if ($text=="release") $output = "Datenstrom Yellow ".YellowCore::RELEASE; + if ($text=="log") { + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreWebsiteFile"); + $fileHandle = @fopen($fileName, "rb"); + if ($fileHandle) { + clearstatcache(true, $fileName); + if (flock($fileHandle, LOCK_SH)) { + $dataBufferSize = 1024; + fseek($fileHandle, max(0, filesize($fileName) - $dataBufferSize)); + $dataBuffer = fread($fileHandle, $dataBufferSize); + if (strlenb($dataBuffer)==$dataBufferSize) { + $dataBuffer = ($pos = strposu($dataBuffer, "\n")) ? substru($dataBuffer, $pos+1) : $dataBuffer; + } + flock($fileHandle, LOCK_UN); } + fclose($fileHandle); } - unset($settings["coreSystemFile"]); - foreach ($settings as $key=>$value) { - if ($key=="coreStaticUrl") $fileDataSettings .= "\n"; - $fileDataSettings .= ucfirst($key).(strempty($value) ? ":\n" : ": $value\n"); - } - if (!empty($fileDataHeader)) $fileDataHeader .= "\n"; - if (!empty($fileDataFooter)) $fileDataSettings .= "\n"; - $fileDataNew = $fileDataHeader.$fileDataSettings.$fileDataFooter; - if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { - $this->yellow->log("error", "Can't write file '$fileName'!"); - } + $output = str_replace("\n", "<br />\n", htmlspecialchars($dataBuffer)); } } + return $output; } - // Process command to show website version and updates + // Process command to show current version public function processCommandAbout($command, $text) { - echo "Datenstrom Yellow ".YellowCore::VERSION."\n"; - list($statusCode, $dataCurrent) = $this->getExtensionsVersion(); - list($statusCode, $dataLatest) = $this->getExtensionsVersion(true); - foreach ($dataCurrent as $key=>$value) { - if (!isset($dataLatest[$key]) || strnatcasecmp($dataCurrent[$key], $dataLatest[$key])>=0) { - echo ucfirst($key)." $value\n"; - } else { - echo ucfirst($key)." $value - Update available\n"; + $statusCode = 200; + $extensions = $this->getExtensionsFromText($text); + if (!is_array_empty($extensions)) { + list($statusCode, $settings) = $this->getExtensionAboutInformation($extensions); + if ($statusCode==200) { + foreach ($settings as $key=>$value) { + echo ucfirst($key)." ".$value->get("version")." - ".$this->getExtensionDescription($key, $value)."\n"; + if ($value->isExisting("documentationUrl")) echo "Read more at ".$value->get("documentationUrl")."\n"; + } } - } - if ($statusCode!=200) echo "ERROR checking updates: ".$this->yellow->page->get("pageError")."\n"; - return $statusCode; - } - - // Process command to clean downloads - public function processCommandClean($command, $text) { - $statusCode = 0; - if ($command=="clean" && $text=="all") { - $path = $this->yellow->system->get("coreExtensionDirectory"); - $regex = "/^.*\\".$this->yellow->system->get("coreDownloadExtension")."$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, false) as $entry) { - if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + if ($statusCode>=400) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n"; + } else { + echo "Datenstrom Yellow ".YellowCore::RELEASE."\n"; + list($statusCode, $settingsCurrent) = $this->getExtensionSettings(false); + foreach ($settingsCurrent as $key=>$value) { + echo ucfirst($key)." ".$value->get("version")."\n"; } - if ($statusCode==500) echo "ERROR cleaning downloads: Can't delete files in directory '$path'!\n"; } return $statusCode; } // Process command to install extensions public function processCommandInstall($command, $text) { - list($extensions) = $this->getExtensionInformation($text); - if (!empty($extensions)) { - $this->updates = 0; - list($statusCode, $data) = $this->getInstallInformation($extensions); - if ($statusCode==200) $statusCode = $this->downloadExtensions($data); + $extensions = $this->getExtensionsFromText($text); + if (!is_array_empty($extensions)) { + $this->extensions = 0; + list($statusCode, $settings) = $this->getExtensionInstallInformation($extensions); + if ($statusCode==200) $statusCode = $this->downloadExtensions($settings); if ($statusCode==200) $statusCode = $this->updateExtensions("install"); - if ($statusCode>=400) echo "ERROR installing files: ".$this->yellow->page->get("pageError")."\n"; + if ($statusCode>=400) echo "ERROR installing files: ".$this->yellow->page->errorMessage."\n"; echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; - echo ", $this->updates extension".($this->updates!=1 ? "s" : "")." installed\n"; + echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed\n"; } else { - $statusCode = $this->showExtensions(); + list($statusCode, $settingsLatest) = $this->getExtensionSettings(true); + foreach ($settingsLatest as $key=>$value) { + echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n"; + } + if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n"; } return $statusCode; } // Process command to uninstall extensions public function processCommandUninstall($command, $text) { - list($extensions) = $this->getExtensionInformation($text); - if (!empty($extensions)) { - $this->updates = 0; - list($statusCode, $data) = $this->getUninstallInformation($extensions, "core, command, update"); - if ($statusCode==200) $statusCode = $this->removeExtensions($data); - if ($statusCode>=400) echo "ERROR uninstalling files: ".$this->yellow->page->get("pageError")."\n"; + $extensions = $this->getExtensionsFromText($text); + if (!is_array_empty($extensions)) { + $this->extensions = 0; + list($statusCode, $settings) = $this->getExtensionUninstallInformation($extensions, "core, update"); + if ($statusCode==200) $statusCode = $this->removeExtensions($settings); + if ($statusCode>=400) echo "ERROR uninstalling files: ".$this->yellow->page->errorMessage."\n"; echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; - echo ", $this->updates extension".($this->updates!=1 ? "s" : "")." uninstalled\n"; + echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." uninstalled\n"; } else { - $statusCode = $this->showExtensions(); + list($statusCode, $settingsCurrent) = $this->getExtensionSettings(false); + foreach ($settingsCurrent as $key=>$value) { + echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n"; + } + if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n"; } return $statusCode; } - + // Process command to update website public function processCommandUpdate($command, $text) { - list($extensions, $force) = $this->getExtensionInformation($text); - list($statusCode, $data) = $this->getUpdateInformation($extensions, $force); - if ($statusCode!=200 || !empty($data)) { - $this->updates = 0; - if ($statusCode==200) $statusCode = $this->downloadExtensions($data); - if ($statusCode==200) $statusCode = $this->updateExtensions("update", $force); - if ($statusCode>=400) echo "ERROR updating files: ".$this->yellow->page->get("pageError")."\n"; - echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; - echo ", $this->updates update".($this->updates!=1 ? "s" : "")." installed\n"; + $extensions = $this->getExtensionsFromText($text); + if (!is_array_empty($extensions)) { + list($statusCode, $settings) = $this->getExtensionUpdateInformation($extensions); + if ($statusCode!=200 || !is_array_empty($settings)) { + $this->extensions = 0; + if ($statusCode==200) $statusCode = $this->downloadExtensions($settings); + if ($statusCode==200) $statusCode = $this->updateExtensions("update"); + if ($statusCode>=400) echo "ERROR updating files: ".$this->yellow->page->errorMessage."\n"; + echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; + echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." updated\n"; + } else { + echo "Your website is up to date\n"; + } } else { - echo "Your website is up to date\n"; + list($statusCode, $settings) = $this->getExtensionUpdateInformation(array("all")); + if (!is_array_empty($settings)) { + foreach ($settings as $key=>$value) { + echo ucfirst($key)." ".$value->get("version")."\n"; + } + echo "Yellow $command: Updates are available. Please type 'php yellow.php update all'.\n"; + } elseif ($statusCode!=200) { + echo "ERROR updating files: ".$this->yellow->page->errorMessage."\n"; + } else { + echo "Your website is up to date\n"; + } } return $statusCode; } - // Process command to install pending extension + // Process command for pending events public function processCommandPending() { - $statusCode = $this->updateExtensions("install"); - if ($statusCode!=200) echo "ERROR updating files: ".$this->yellow->page->get("pageError")."\n"; - echo "Your website has ".($statusCode!=200 ? "not " : "")."been updated: Please run command again\n"; - return $statusCode; - } - - // Process request to install pending extension - public function processRequestPending($scheme, $address, $base, $location, $fileName) { - $statusCode = $this->updateExtensions("install"); - if ($statusCode==200) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } - return $statusCode; - } - - // Process update notification - public function processUpdateNotification($extension, $action) { - $statusCode = 200; - if ($this->yellow->extensions->isExisting($extension) && $action=="uninstall") { - $value = $this->yellow->extensions->extensions[$extension]; - if (method_exists($value["obj"], "onUpdate")) $value["obj"]->onUpdate($action); - } - $updateNotification = $this->yellow->system->get("updateNotification"); - if ($updateNotification=="none") $updateNotification = ""; - if (!empty($updateNotification)) $updateNotification .= ","; - $updateNotification .= "$extension/$action"; - $fileName = $this->yellow->system->get("coreSettingDirectory").$this->yellow->system->get("coreSystemFile"); - if (!$this->yellow->system->save($fileName, array("updateNotification" => $updateNotification))) { - $statusCode = 500; - $this->yellow->page->error(500, "Can't write file '$fileName'!"); + $statusCode = 0; + $this->extensions = 0; + $this->updatePatchPending(); + $this->updateEventPending(); + $statusCode = $this->updateExtensionPending(); + if ($statusCode==303) { + echo "Detected ZIP file".($this->extensions!=1 ? "s" : ""); + echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed. Please run command again.\n"; } return $statusCode; } - // Return extension information - public function getExtensionInformation($text) { - $extensions = array_unique(array_filter($this->yellow->toolbox->getTextArguments($text), "strlen")); - $force = false; - foreach ($extensions as $key=>$value) { - if ($value=="force") { - $force = true; - unset($extensions[$key]); - } - } - return array($extensions, $force); - } - - // Return install information - public function getInstallInformation($extensions) { - $data = array(); - list($statusCodeCurrent, $dataCurrent) = $this->getExtensionsVersion(); - list($statusCodeLatest, $dataLatest) = $this->getExtensionsVersion(true, true); - $statusCode = max($statusCodeCurrent, $statusCodeLatest); - foreach ($extensions as $extension) { - $found = false; - foreach ($dataLatest as $key=>$value) { - if (strtoloweru($key)==strtoloweru($extension)) { - $data[$key] = $dataLatest[$key]; - $found = true; - break; - } - } - if (!$found) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); - } - } - return array($statusCode, $data); - } - - // Return uninstall information - public function getUninstallInformation($extensions, $extensionsProtected) { - $data = array(); - list($statusCodeCurrent, $dataCurrent) = $this->getExtensionsVersion(); - list($statusCodeLatest, $dataLatest) = $this->getExtensionsVersion(true, true); - list($statusCodeRelevant, $dataRelevant) = $this->getExtensionsRelevant(); - $statusCode = max($statusCodeCurrent, $statusCodeLatest, $statusCodeRelevant); - foreach ($extensions as $extension) { - $found = false; - foreach ($dataCurrent as $key=>$value) { - if (strtoloweru($key)==strtoloweru($extension) && isset($dataLatest[$key]) && isset($dataRelevant[$key])) { - $data[$key] = $dataRelevant[$key]; - $found = true; - break; - } - } - if (!$found) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); - } - } - $protected = preg_split("/\s*,\s*/", $extensionsProtected); - foreach ($data as $key=>$value) { - if (in_array($key, $protected)) unset($data[$key]); - } - return array($statusCode, $data); - } - - // Return update information - public function getUpdateInformation($extensions, $force) { - $data = array(); - list($statusCodeCurrent, $dataCurrent) = $this->getExtensionsVersion(); - list($statusCodeLatest, $dataLatest) = $this->getExtensionsVersion(true, true); - list($statusCodeModified, $dataModified) = $this->getExtensionsModified(); - $statusCode = max($statusCodeCurrent, $statusCodeLatest, $statusCodeModified); - if (empty($extensions)) { - foreach ($dataCurrent as $key=>$value) { - if (isset($dataLatest[$key])) { - list($version, $dummy1, $dummy2) = $this->yellow->toolbox->getTextList($dataLatest[$key], ",", 3); - if (strnatcasecmp($dataCurrent[$key], $version)<0) $data[$key] = $dataLatest[$key]; - if (isset($dataModified[$key]) && !empty($version) && $force) $data[$key] = $dataLatest[$key]; - } - } - } else { - foreach ($extensions as $extension) { - $found = false; - foreach ($dataCurrent as $key=>$value) { - if (isset($dataLatest[$key])) { - list($version, $dummy1, $dummy2) = $this->yellow->toolbox->getTextList($dataLatest[$key], ",", 3); - if (strtoloweru($key)==strtoloweru($extension) && !empty($version)) { - $data[$key] = $dataLatest[$key]; - $dataModified = array_intersect_key($dataModified, $data); - $found = true; - break; - } - } - } - if (!$found) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); - } - } - } - if ($statusCode==200) { - foreach (array_merge($dataModified, $data) as $key=>$value) { - list($version, $dummy1, $dummy2) = $this->yellow->toolbox->getTextList($value, ",", 3); - if (!isset($dataModified[$key]) || $force) { - echo ucfirst($key)." $version\n"; - } else { - echo ucfirst($key)." $version has been modified - Force update\n"; - } + // Process request for pending events + public function processRequestPending($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->yellow->lookup->isContentFile($fileName)) { + $this->updatePatchPending(); + $this->updateEventPending(); + $statusCode = $this->updateExtensionPending(); + if ($statusCode==303) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); } } - return array($statusCode, $data); - } - - // Show extensions - public function showExtensions() { - list($statusCode, $dataLatest) = $this->getExtensionsVersion(true, true); - foreach ($dataLatest as $key=>$value) { - list($dummy1, $dummy2, $text) = $this->yellow->toolbox->getTextList($value, ",", 3); - echo ucfirst($key).": $text\n"; - } - if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->get("pageError")."\n"; return $statusCode; } // Download extensions - public function downloadExtensions($data) { + public function downloadExtensions($settings) { $statusCode = 200; $path = $this->yellow->system->get("coreExtensionDirectory"); - $fileExtension = $this->yellow->system->get("coreDownloadExtension"); - foreach ($data as $key=>$value) { + foreach ($settings as $key=>$value) { $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; - list($dummy1, $url, $dummy2) = $this->yellow->toolbox->getTextList($value, ",", 3); - list($statusCode, $fileData) = $this->getExtensionFile($url); - if (empty($fileData) || !$this->yellow->toolbox->createFile($fileName.$fileExtension, $fileData)) { + list($statusCode, $fileData) = $this->getExtensionFile($value->get("downloadUrl")); + if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileName.".download", $fileData)) { $statusCode = 500; $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - break; } + if ($statusCode!=200) break; } if ($statusCode==200) { - foreach ($data as $key=>$value) { + foreach ($settings as $key=>$value) { $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; - if (!$this->yellow->toolbox->renameFile($fileName.$fileExtension, $fileName)) { + if (!$this->yellow->toolbox->renameFile($fileName.".download", $fileName)) { $statusCode = 500; $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); } @@ -431,12 +252,12 @@ class YellowUpdate { } // Update extensions - public function updateExtensions($action, $force = false) { + public function updateExtensions($action) { $statusCode = 200; if (function_exists("opcache_reset")) opcache_reset(); $path = $this->yellow->system->get("coreExtensionDirectory"); foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { - $statusCode = max($statusCode, $this->updateExtensionArchive($entry, $action, $force)); + $statusCode = max($statusCode, $this->updateExtensionArchive($entry, $action)); if (!$this->yellow->toolbox->deleteFile($entry)) { $statusCode = 500; $this->yellow->page->error($statusCode, "Can't delete file '$entry'!"); @@ -446,74 +267,67 @@ class YellowUpdate { } // Update extension from archive - public function updateExtensionArchive($path, $action, $force = false) { + public function updateExtensionArchive($path, $action) { $statusCode = 200; $zip = new ZipArchive(); if ($zip->open($path)===true) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateExtensionArchive file:$path<br/>\n"; - $extension = $version = $language = ""; - $modified = $lastModified = $lastPublished = 0; + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updateExtensionArchive file:$path<br/>\n"; + $pathBase = ""; if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1]; $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateExtensionFile")); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2]) && strposu($matches[1], "/")) { - $fileName = $matches[1]; - if (is_file($fileName)) { - $lastPublished = filemtime($fileName); - break; - } - } - } - $rootPages = array(); - foreach ($this->yellow->content->scanLocation("") as $page) { - if ($page->isAvailable() && $page->isVisible()) array_push($rootPages, $page); - } - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])=="extension") $extension = lcfirst($matches[2]); - if (lcfirst($matches[1])=="version") $version = lcfirst($matches[2]); - if (lcfirst($matches[1])=="published") $modified = strtotime($matches[2]); - if (lcfirst($matches[1])=="language") $language = $matches[2]; - if (!empty($matches[1]) && !empty($matches[2]) && strposu($matches[1], "/")) { - $fileName = $matches[1]; - list($dummy, $entry, $flags) = $this->yellow->toolbox->getTextList($matches[2], ",", 3); - foreach ($rootPages as $page) { - list($fileNameSource, $fileNameDestination) = $this->getExtensionsFileNames($fileName, $entry, $flags, $language, $pathBase, $page); + $settings = $this->yellow->toolbox->getTextSettings($fileData, ""); + list($extension, $version, $newModified, $oldModified) = $this->getExtensionInformation($settings); + if (!is_string_empty($extension) && !is_string_empty($version)) { + $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action, $settings)); + $paths = $this->getExtensionDirectories($zip, $pathBase); + foreach ($this->getExtensionFileNames($settings) as $fileName) { + list($entry, $flags) = $this->yellow->toolbox->getTextList($settings[$fileName], ",", 2); + if (!$this->yellow->lookup->isContentFile($fileName)) { + $fileNameSource = $pathBase.$entry; + $fileData = $zip->getFromName($fileNameSource); + $lastModified = $this->yellow->toolbox->getFileModified($fileName); + $statusCode = max($statusCode, $this->updateExtensionFile($fileName, $fileData, + $newModified, $oldModified, $lastModified, $flags, $extension)); + } else { + foreach ($this->getExtensionContentRootPages() as $page) { + list($fileNameSource, $fileNameDestination) = $this->getExtensionContentFileNames( + $fileName, $pathBase, $entry, $flags, $paths, $page); $fileData = $zip->getFromName($fileNameSource); $lastModified = $this->yellow->toolbox->getFileModified($fileNameDestination); - $statusCode = $this->updateExtensionFile($fileNameDestination, $fileData, - $modified, $lastModified, $lastPublished, $flags, $force, $extension); + $statusCode = max($statusCode, $this->updateExtensionFile($fileNameDestination, $fileData, + $newModified, $oldModified, $lastModified, $flags, $extension)); } - if ($statusCode!=200) break; } } + $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action)); + $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); + ++$this->extensions; + } else { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't detect file '$path'!"); } $zip->close(); - $statusCode = max($statusCode, $this->processUpdateNotification($extension, $action)); - $this->yellow->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); - ++$this->updates; } else { $statusCode = 500; - $this->yellow->page->error(500, "Can't open file '$path'!"); + $this->yellow->page->error($statusCode, "Can't open file '$path'!"); } return $statusCode; } // Update extension from file - public function updateExtensionFile($fileName, $fileData, $modified, $lastModified, $lastPublished, $flags, $force, $extension) { + public function updateExtensionFile($fileName, $fileData, $newModified, $oldModified, $lastModified, $flags, $extension) { $statusCode = 200; - $fileName = $this->yellow->toolbox->normaliseTokens($fileName); - if ($this->yellow->lookup->isValidFile($fileName) && !empty($extension)) { + $fileName = $this->yellow->lookup->normalisePath($fileName); + if ($this->yellow->lookup->isValidFile($fileName)) { $create = $update = $delete = false; - if (preg_match("/create/i", $flags) && !is_file($fileName) && !empty($fileData)) $create = true; - if (preg_match("/update/i", $flags) && is_file($fileName) && !empty($fileData)) $update = true; + if (preg_match("/create/i", $flags) && !is_file($fileName) && !is_string_empty($fileData)) $create = true; + if (preg_match("/update/i", $flags) && is_file($fileName) && !is_string_empty($fileData)) $update = true; if (preg_match("/delete/i", $flags) && is_file($fileName)) $delete = true; - if (preg_match("/optional/i", $flags) && $this->yellow->extensions->isExisting($extension)) $create = $update = $delete = false; - if (preg_match("/careful/i", $flags) && is_file($fileName) && $lastModified!=$lastPublished && !$force) $update = false; + if (preg_match("/optional/i", $flags) && $this->yellow->extension->isExisting($extension)) $create = $update = $delete = false; + if (preg_match("/careful/i", $flags) && is_file($fileName) && $lastModified!=$oldModified) $update = false; if ($create) { if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || - !$this->yellow->toolbox->modifyFile($fileName, $modified)) { + !$this->yellow->toolbox->modifyFile($fileName, $newModified)) { $statusCode = 500; $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); } @@ -521,7 +335,7 @@ class YellowUpdate { if ($update) { if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory")) || !$this->yellow->toolbox->createFile($fileName, $fileData) || - !$this->yellow->toolbox->modifyFile($fileName, $modified)) { + !$this->yellow->toolbox->modifyFile($fileName, $newModified)) { $statusCode = 500; $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); } @@ -532,7 +346,7 @@ class YellowUpdate { $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); } } - if (defined("DEBUG") && DEBUG>=2) { + if ($this->yellow->system->get("coreDebugMode")>=2) { $debug = "action:".($create ? "create" : "").($update ? "update" : "").($delete ? "delete" : ""); if (!$create && !$update && !$delete) $debug = "action:none"; echo "YellowUpdate::updateExtensionFile file:$fileName $debug<br/>\n"; @@ -540,172 +354,586 @@ class YellowUpdate { } return $statusCode; } + + // Update pending patches + public function updatePatchPending() { + $fileName = $this->yellow->system->get("coreExtensionDirectory")."updatepatch.bin"; + if (is_file($fileName)) { + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updatePatchPending file:$fileName<br/>\n"; + if (!$this->yellow->extension->isExisting("updatepatch")) { + require_once($fileName); + $this->yellow->extension->register("updatepatch", "YellowUpdatePatch"); + } + if ($this->yellow->extension->isExisting("updatepatch")) { + $value = $this->yellow->extension->data["updatepatch"]; + if (method_exists($value["object"], "onLoad")) $value["object"]->onLoad($this->yellow); + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("patch"); + } + unset($this->yellow->extension->data["updatepatch"]); + if (function_exists("opcache_reset")) opcache_reset(); + if (!$this->yellow->toolbox->deleteFile($fileName)) { + $this->yellow->toolbox->log("error", "Can't delete file '$fileName'!"); + } + } + } + + // Update pending events + public function updateEventPending() { + if ($this->yellow->system->get("updateCurrentRelease")!="none") { + if ($this->yellow->system->get("updateCurrentRelease")!=YellowCore::RELEASE) { + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, array("updateCurrentRelease" => YellowCore::RELEASE))) { + $this->yellow->toolbox->log("error", "Can't write file '$fileName'!"); + } else { + list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation(); + $product = "Datenstrom Yellow ".YellowCore::RELEASE; + $this->yellow->toolbox->log("info", "Update $product, PHP ".PHP_VERSION.", $name $version, $os"); + } + } + if ($this->yellow->system->get("updateEventPending")!="none") { + foreach (explode(",", $this->yellow->system->get("updateEventPending")) as $token) { + list($extension, $action) = $this->yellow->toolbox->getTextList($token, "/", 2); + if ($this->yellow->extension->isExisting($extension) && $action!="uninstall") { + $value = $this->yellow->extension->data[$extension]; + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action); + } + } + $this->updateSystemSettings("all", $action); + $this->updateLanguageSettings("all", $action); + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, array("updateEventPending" => "none"))) { + $this->yellow->toolbox->log("error", "Can't write file '$fileName'!"); + } + } + if ($this->yellow->system->get("updateEventDaily")<=time()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("daily"); + } + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, array("updateEventDaily" => $this->getTimestampDaily()))) { + $this->yellow->toolbox->log("error", "Can't write file '$fileName'!"); + } + } + } + } + + // Update pending extensions + public function updateExtensionPending() { + $statusCode = 0; + $path = $this->yellow->system->get("coreExtensionDirectory"); + if (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))) { + $statusCode = $this->updateExtensions("install"); + if ($statusCode==200) $statusCode = 303; + if ($statusCode>=400) { + $this->yellow->toolbox->log("error", $this->yellow->page->errorMessage); + $this->yellow->page->statusCode = 0; + $this->yellow->page->errorMessage = ""; + $statusCode = 303; + } + } + return $statusCode; + } - // Return extension file names - public function getExtensionsFileNames($fileName, $entry, $flags, $language, $pathBase, $page) { - if (preg_match("/multi-language/i", $flags)) { - $languagesAvailable = preg_split("/\s*,\s*/", $language); - $languagesWanted = array($page->get("language"), "en"); - foreach ($languagesWanted as $language) { - if (in_array($language, $languagesAvailable)) { - $languageFound = $language; - break; + // Update extension settings + public function updateExtensionSettings($extension, $action, $settings) { + $statusCode = 200; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile"); + $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName); + if ($action=="install" || $action=="update") { + $settingsCurrent = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + $settingsCurrent[$extension] = new YellowArray(); + foreach ($settings as $key=>$value) $settingsCurrent[$extension][$key] = $value; + $settingsCurrent->uksort("strnatcasecmp"); + $fileDataNew = ""; + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\#/", $line)) $fileDataNew = $line; + break; + } + foreach ($settingsCurrent as $extension=>$block) { + if (!is_string_empty($fileDataNew)) $fileDataNew .= "\n"; + foreach ($block as $key=>$value) { + $fileDataNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; } } - $pathLanguage = $languageFound ? "$languageFound/" : ""; - $fileNameSource = $pathBase.$pathLanguage.basename($entry); - } else { - $fileNameSource = $pathBase.basename($entry); + } elseif ($action=="uninstall") { + $fileDataNew = $this->yellow->toolbox->unsetTextSettings($fileData, "extension", $extension); } - if ($this->yellow->system->get("coreMultiLanguageMode") && $this->yellow->lookup->isContentFile($fileName)) { - $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); - $fileNameDestination = $page->fileName.substru($fileName, $contentDirectoryLength); - } else { - $fileNameDestination = $fileName; + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); } - return array($fileNameSource, $fileNameDestination); + return $statusCode; } - + + // Update system settings + public function updateSystemSettings($extension, $action) { + $statusCode = 200; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName); + if ($action=="install" || $action=="update") { + $fileDataStart = $fileDataSettings = ""; + $settings = new YellowArray(); + $settings->exchangeArray($this->yellow->system->settingsDefaults->getArrayCopy()); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\#/", $line)) { + if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n"; + continue; + } + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $settings[$matches[1]] = $matches[2]; + } + } + } + foreach ($settings as $key=>$value) { + $fileDataSettings .= ucfirst($key).(is_string_empty($value) ? ":\n" : ": $value\n"); + } + $fileDataNew = $fileDataStart.$fileDataSettings; + } elseif ($action=="uninstall") { + if (!is_string_empty($extension)) { + $fileDataNew = ""; + $regex = "/^".ucfirst($extension)."[A-Z]+/"; + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue; + } + $fileDataNew .= $line; + } + } + } + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Update language settings + public function updateLanguageSettings($extension, $action) { + $statusCode = 200; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile"); + $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName); + if ($action=="install" || $action=="update") { + $fileDataStart = $fileDataSettings = $language = ""; + $settings = new YellowArray(); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\#/", $line)) { + if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n"; + continue; + } + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + if (lcfirst($matches[1])=="language") { + if (!is_array_empty($settings)) { + if (!is_string_empty($fileDataSettings)) $fileDataSettings .= "\n"; + foreach ($settings as $key=>$value) { + $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + } + $language = $matches[2]; + $settings = new YellowArray(); + $settings["language"] = $language; + $settings["languageLocale"] = "n/a"; + $settings["languageDescription"] = "n/a"; + $settings["languageTranslator"] = "Unknown"; + foreach ($this->yellow->language->settingsDefaults as $key=>$value) { + $require = preg_match("/^([a-z]*)[A-Z]+/", $key, $tokens) ? $tokens[1] : "core"; + if ($require=="language") $require = "core"; + if ($this->yellow->extension->isExisting($require)) { + if ($this->yellow->language->isText($key, $language)) { + $settings[$key] = $this->yellow->language->getText($key, $language); + } else { + $settings[$key] = $this->yellow->language->getText($key, "en"); + } + } + } + } + if (!is_string_empty($language)) { + $settings[$matches[1]] = $matches[2]; + } + } + } + } + if (!is_array_empty($settings)) { + if (!is_string_empty($fileDataSettings)) $fileDataSettings .= "\n"; + foreach ($settings as $key=>$value) { + $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + } + $fileDataNew = $fileDataStart.$fileDataSettings; + } elseif ($action=="uninstall") { + if (!is_string_empty($extension) && ucfirst($extension)!="Language") { + $fileDataNew = ""; + $regex = "/^".ucfirst($extension)."[A-Z]+/"; + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue; + } + $fileDataNew .= $line; + } + } + } + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Update extension notification + public function updateExtensionNotification($extension, $action) { + $statusCode = 200; + if ($this->yellow->extension->isExisting($extension) && $action=="uninstall") { + $value = $this->yellow->extension->data[$extension]; + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action); + } + $updateEventPending = $this->yellow->system->get("updateEventPending"); + if ($updateEventPending=="none") $updateEventPending = ""; + if (!is_string_empty($updateEventPending)) $updateEventPending .= ","; + $updateEventPending .= "$extension/$action"; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, array("updateEventPending" => $updateEventPending))) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + return $statusCode; + } + // Remove extensions - public function removeExtensions($data) { + public function removeExtensions($settings) { $statusCode = 200; if (function_exists("opcache_reset")) opcache_reset(); - foreach ($data as $key=>$value) { - foreach (preg_split("/\s*,\s*/", $value) as $fileName) { - $statusCode = max($statusCode, $this->removeExtensionsFile($fileName, $key)); + foreach ($settings as $extension=>$block) { + $statusCode = max($statusCode, $this->removeExtensionArchive($extension, "uninstall", $block)); + } + return $statusCode; + } + + // Remove extension archive + public function removeExtensionArchive($extension, $action, $settings) { + $statusCode = 200; + $fileNames = $this->getExtensionFileNames($settings, true); + if (!is_array_empty($fileNames)) { + $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action)); + foreach ($fileNames as $fileName) { + $statusCode = max($statusCode, $this->removeExtensionFile($fileName)); + } + if ($statusCode==200) { + $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action, $settings)); + $statusCode = max($statusCode, $this->updateSystemSettings($extension, $action)); + $statusCode = max($statusCode, $this->updateLanguageSettings($extension, $action)); } - $statusCode = max($statusCode, $this->processUpdateNotification($key, "uninstall")); - $version = $this->yellow->extensions->isExisting($key) ? $this->yellow->extensions->extensions[$key]["version"] : ""; - $this->yellow->log($statusCode==200 ? "info" : "error", "Uninstall extension '".ucfirst($key)." $version'"); - ++$this->updates; + $version = $settings->get("version"); + $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); + ++$this->extensions; + } else { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Please delete extension '$extension' manually!"); } return $statusCode; } - - // Remove extensions file - public function removeExtensionsFile($fileName, $extension) { + + // Remove extension file + public function removeExtensionFile($fileName) { $statusCode = 200; - $fileName = $this->yellow->toolbox->normaliseTokens($fileName); - if ($this->yellow->lookup->isValidFile($fileName) && !empty($extension)) { + $fileName = $this->yellow->lookup->normalisePath($fileName); + if ($this->yellow->lookup->isValidFile($fileName) && is_file($fileName)) { if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) { $statusCode = 500; $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); } - if (defined("DEBUG") && DEBUG>=2) { - echo "YellowUpdate::removeExtensionsFile file:$fileName action:delete<br/>\n"; + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowUpdate::removeExtensionFile file:$fileName action:delete<br/>\n"; } } return $statusCode; } + + // Return extensions from text, space separated + public function getExtensionsFromText($text) { + return array_unique(array_filter($this->yellow->toolbox->getTextArguments($text), "strlen")); + } - // Return extensions version - public function getExtensionsVersion($latest = false, $rawFormat = false) { - $data = array(); - if ($latest) { - $url = $this->yellow->system->get("updateExtensionUrl")."/raw/master/".$this->yellow->system->get("updateVersionFile"); - list($statusCode, $fileData) = $this->getExtensionFile($url); - if ($statusCode==200) { - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2])) { - $extension = lcfirst($matches[1]); - list($version, $dummy1, $dummy2) = $this->yellow->toolbox->getTextList($matches[2], ",", 3); - $data[$extension] = $rawFormat ? $matches[2] : $version; - } + // Return extension about information + public function getExtensionAboutInformation($extensions) { + $settings = array(); + list($statusCode, $settingsCurrent) = $this->getExtensionSettings(false); + $settingsCurrent["Datenstrom Yellow"] = new YellowArray(); + $settingsCurrent["Datenstrom Yellow"]["version"] = YellowCore::RELEASE; + $settingsCurrent["Datenstrom Yellow"]["description"] = "Datenstrom Yellow is for people who make small websites."; + $settingsCurrent["Datenstrom Yellow"]["documentationUrl"] = "https://datenstrom.se/yellow/"; + foreach ($extensions as $extension) { + $found = false; + if (strtoloweru($extension)=="yellow") $extension = "Datenstrom Yellow"; + foreach ($settingsCurrent as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension)) { + $settings[$key] = $settingsCurrent[$key]; + $found = true; + break; } } - } else { - $statusCode = 200; - $data = $this->yellow->extensions->getData(); + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } } - return array($statusCode, $data); + return array($statusCode, $settings); } - - // Return extensions relevant files - public function getExtensionsRelevant() { - $data = array(); - $url = $this->yellow->system->get("updateExtensionUrl")."/raw/master/".$this->yellow->system->get("updateWaffleFile"); - list($statusCode, $fileData) = $this->getExtensionFile($url); - if ($statusCode==200) { - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2])) { - $fileName = $matches[1]; - list($extension, $dummy1, $dummy2) = $this->yellow->toolbox->getTextList(lcfirst($matches[2]), ",", 3); - if (!isset($data[$extension])) { - $data[$extension] = $fileName; - } else { - $data[$extension] .= ",".$fileName; - } + + // Return extension install information + public function getExtensionInstallInformation($extensions) { + $settings = array(); + list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(false); + list($statusCodeLatest, $settingsLatest) = $this->getExtensionSettings(true); + $statusCode = max($statusCodeCurrent, $statusCodeLatest); + foreach ($extensions as $extension) { + $found = false; + foreach ($settingsLatest as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension)) { + if (!$settingsCurrent->isExisting($key)) $settings[$key] = $settingsLatest[$key]; + $found = true; + break; } } + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } } - return array($statusCode, $data); + return array($statusCode, $settings); + } + + // Return extension about information + public function getExtensionUninstallInformation($extensions, $extensionsProtected = "") { + $settings = array(); + list($statusCode, $settingsCurrent) = $this->getExtensionSettings(false); + foreach ($extensions as $extension) { + $found = false; + foreach ($settingsCurrent as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension)) { + $settings[$key] = $settingsCurrent[$key]; + $found = true; + break; + } + } + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } + } + $protected = preg_split("/\s*,\s*/", $extensionsProtected); + foreach ($settings as $key=>$value) { + if (in_array($key, $protected)) unset($settings[$key]); + } + return array($statusCode, $settings); } - // Return extensions modified files - public function getExtensionsModified() { - $data = array(); - $dataCurrent = $this->yellow->extensions->getData(); - $url = $this->yellow->system->get("updateExtensionUrl")."/raw/master/".$this->yellow->system->get("updateWaffleFile"); - list($statusCode, $fileData) = $this->getExtensionFile($url); - if ($statusCode==200) { - $extension = ""; - $lastModified = $lastPublished = 0; - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2])) { - $fileName = $matches[1]; - list($extensionNew, $dummy, $flags) = $this->yellow->toolbox->getTextList(lcfirst($matches[2]), ",", 3); - if ($extension!=$extensionNew) { - $extension = $extensionNew; - $lastPublished = $this->yellow->toolbox->getFileModified($fileName); + // Return extension update information + public function getExtensionUpdateInformation($extensions) { + $settings = array(); + list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(false); + list($statusCodeLatest, $settingsLatest) = $this->getExtensionSettings(true); + $statusCode = max($statusCodeCurrent, $statusCodeLatest); + if (in_array("all", $extensions)) { + foreach ($settingsCurrent as $key=>$value) { + if ($settingsLatest->isExisting($key)) { + $versionCurrent = $settingsCurrent[$key]->get("version"); + $versionLatest = $settingsLatest[$key]->get("version"); + if (strnatcasecmp($versionCurrent, $versionLatest)<0) { + $settings[$key] = $settingsLatest[$key]; } - if (isset($dataCurrent[$extension])) { - $lastModified = $this->yellow->toolbox->getFileModified($fileName); - if (preg_match("/update/i", $flags) && preg_match("/careful/i", $flags) && $lastModified!=$lastPublished) { - $data[$extension] = $dataCurrent[$extension]; - if (defined("DEBUG") && DEBUG>=2) { - echo "YellowUpdate::getExtensionsModified detected file:$fileName extension:$extension<br/>\n"; - } + } + } + } else { + foreach ($extensions as $extension) { + $found = false; + foreach ($settingsCurrent as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension) && $settingsLatest->isExisting($key)) { + $versionCurrent = $settingsCurrent[$key]->get("version"); + $versionLatest = $settingsLatest[$key]->get("version"); + if (strnatcasecmp($versionCurrent, $versionLatest)<0) { + $settings[$key] = $settingsLatest[$key]; } + $found = true; + break; + } + } + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } + } + } + return array($statusCode, $settings); + } + + // Return extension settings + public function getExtensionSettings($latest) { + $statusCode = 200; + $settings = array(); + if (!$latest) { + $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile"); + $fileData = $this->yellow->toolbox->readFile($fileNameCurrent); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + foreach ($settings->getArrayCopy() as $key=>$value) { + if (!$this->yellow->extension->isExisting($key)) unset($settings[$key]); + } + foreach ($this->yellow->extension->data as $key=>$value) { + if (!$settings->isExisting($key)) $settings[$key] = new YellowArray(); + $settings[$key]["extension"] = ucfirst($key); + $settings[$key]["version"] = $value["version"]; + } + } else { + $fileNameLatest = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateLatestFile"); + $expire = $this->yellow->toolbox->getFileModified($fileNameLatest) + 60*10; + if ($expire<=time()) { + $url = $this->yellow->system->get("updateLatestUrl"); + if ($url=="auto") $url = "https://raw.githubusercontent.com/datenstrom/yellow/main/system/extensions/update-latest.ini"; + list($statusCode, $fileData) = $this->getExtensionFile($url); + if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileNameLatest, $fileData)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileNameLatest'!"); + } + } + $fileData = $this->yellow->toolbox->readFile($fileNameLatest); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + } + $settings->uksort("strnatcasecmp"); + return array($statusCode, $settings); + } + + // Return extension information + public function getExtensionInformation($settings) { + $extension = lcfirst($settings->get("extension")); + $version = $settings->get("version"); + $newModified = strtotime($settings->get("published")); + $oldModified = 0; + $invalid = false; + foreach ($settings as $key=>$value) { + if (strposu($key, "/")) { + $fileName = $this->yellow->lookup->normalisePath($key); + if (!$this->yellow->lookup->isValidFile($fileName)) $invalid = true; + if ($oldModified==0) $oldModified = $this->yellow->toolbox->getFileModified($fileName); + } + } + if ($invalid) $extension = $version = ""; + return array($extension, $version, $newModified, $oldModified); + } + + // Return extension directories + public function getExtensionDirectories($zip, $pathBase) { + $paths = array(); + for ($index=0; $index<$zip->numFiles; ++$index) { + $entry = substru($zip->getNameIndex($index), strlenu($pathBase)); + if (preg_match("#^(.*\/).*?$#", $entry, $matches)) { + array_push($paths, $matches[1]); + } + } + return array_unique($paths); + } + + // Return extension file names + public function getExtensionFileNames($settings, $reverse = false) { + $fileNames = array(); + foreach ($settings as $key=>$value) { + if (strposu($key, "/")) array_push($fileNames, $key); + } + if ($reverse) $fileNames = array_reverse($fileNames); + return $fileNames; + } + + // Return extension root pages for content files + public function getExtensionContentRootPages() { + $rootPages = array(); + foreach ($this->yellow->content->scanLocation("") as $page) { + if ($page->isAvailable() && $page->isVisible()) array_push($rootPages, $page); + } + return $rootPages; + } + + // Return extension files names for content files + public function getExtensionContentFileNames($fileName, $pathBase, $entry, $flags, $paths, $page) { + if (preg_match("/multi-language/i", $flags)) { + $pathMultiLanguage = ""; + $languagesWanted = array($page->get("language"), "en"); + foreach ($languagesWanted as $language) { + foreach ($paths as $path) { + if ($this->yellow->lookup->normaliseToken(rtrim($path, "/"))==$language) { + $pathMultiLanguage = $path; + break; } } + if (!is_string_empty($pathMultiLanguage)) break; } + $fileNameSource = $pathBase.$pathMultiLanguage.$entry; + } else { + $fileNameSource = $pathBase.$entry; } - return array($statusCode, $data); + if ($this->yellow->system->get("coreMultiLanguageMode")) { + $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); + $fileNameDestination = $page->fileName.substru($fileName, $contentDirectoryLength); + } else { + $fileNameDestination = $fileName; + } + return array($fileNameSource, $fileNameDestination); + } + + // Return extension description including responsible developer/designer/translator + public function getExtensionDescription($key, $value) { + $description = $responsible = ""; + if ($value->isExisting("description")) $description = $value->get("description"); + if ($value->isExisting("developer")) $responsible = "Developed by ".$value["developer"]."."; + if ($value->isExisting("designer")) $responsible = "Designed by ".$value["designer"]."."; + if ($value->isExisting("translator")) $responsible = "Translated by ".$value["translator"]."."; + if (is_string_empty($description)) $description = "No description available."; + return "$description $responsible"; } // Return extension file public function getExtensionFile($url) { - $urlRequest = $url; - if (preg_match("#^https://github.com/(.+)/raw/(.+)$#", $url, $matches)) $urlRequest = "https://raw.githubusercontent.com/".$matches[1]."/".$matches[2]; $curlHandle = curl_init(); - curl_setopt($curlHandle, CURLOPT_URL, $urlRequest); - curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; DatenstromYellow/".YellowCore::VERSION."; SoftwareUpdater)"); + curl_setopt($curlHandle, CURLOPT_URL, $this->getExtensionDownloadUrl($url)); + curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowUpdate/".YellowUpdate::VERSION."; SoftwareUpdater)"); curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); - $rawData = curl_exec($curlHandle); + $fileData = curl_exec($curlHandle); $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); - $fileData = ""; + $redirectUrl = ($statusCode>=300 && $statusCode<=399) ? curl_getinfo($curlHandle, CURLINFO_REDIRECT_URL) : ""; curl_close($curlHandle); - if ($statusCode==200) { - $fileData = $rawData; - } elseif ($statusCode==0) { - $statusCode = 500; - list($scheme, $address) = $this->yellow->lookup->getUrlInformation($url); - $this->yellow->page->error($statusCode, "Can't connect to server '$scheme://$address'!"); - } else { + if ($statusCode==0) { + $statusCode = 450; + $this->yellow->page->error($statusCode, "Can't connect to the update server!"); + } + if ($statusCode!=450 && $statusCode!=200) { $statusCode = 500; $this->yellow->page->error($statusCode, "Can't download file '$url'!"); } - if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::getExtensionFile status:$statusCode url:$url<br/>\n"; + if ($this->yellow->system->get("coreDebugMode")>=2 && !is_string_empty($redirectUrl)) { + echo "YellowUpdate::getExtensionFile redirected to url:$redirectUrl<br/>\n"; + } + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowUpdate::getExtensionFile status:$statusCode url:$url<br/>\n"; + } return array($statusCode, $fileData); } - - // Check if extension pending - public function isExtensionPending() { - $path = $this->yellow->system->get("coreExtensionDirectory"); - return count($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))>0; + + // Return extension download URL, redirect to known URL if necessary + public function getExtensionDownloadUrl($url) { + if (preg_match("#^https://github.com/(.+)/archive/refs/heads/main.zip$#", $url, $matches)) { + $url = "https://codeload.github.com/".$matches[1]."/zip/refs/heads/main"; + } + if (preg_match("#^https://github.com/(.+)/raw/main/(.+)$#", $url, $matches)) { + $url = "https://raw.githubusercontent.com/".$matches[1]."/main/".$matches[2]; + } + return $url; + } + + // Return time of next daily update + public function getTimestampDaily() { + $timeOffset = 0; + foreach (str_split($this->yellow->system->get("sitename")) as $char) { + $timeOffset = ($timeOffset+ord($char)) % 60; + } + return mktime(0, 0, 0) + 60*60*24 + $timeOffset; } } diff --git a/system/extensions/yellow-language.ini b/system/extensions/yellow-language.ini @@ -0,0 +1,5 @@ +# Datenstrom Yellow language settings + +Language: en +CoreDateFormatMedium: Y-m-d +picture.jpg: This is an example image diff --git a/system/settings/system.ini b/system/extensions/yellow-system.ini diff --git a/system/settings/user.ini b/system/extensions/yellow-user.ini diff --git a/system/layouts/default.html b/system/layouts/default.html @@ -2,7 +2,7 @@ <div class="content"> <div class="main" role="main"> <h1><?php echo $this->yellow->page->getHtml("titleContent") ?></h1> -<?php echo $this->yellow->page->getContent() ?> +<?php echo $this->yellow->page->getContentHtml() ?> </div> </div> <?php $this->yellow->layout("footer") ?> diff --git a/system/layouts/error.html b/system/layouts/error.html @@ -2,7 +2,7 @@ <div class="content"> <div class="main" role="main"> <h1><?php echo $this->yellow->page->getHtml("titleContent") ?></h1> -<?php echo $this->yellow->page->getContent() ?> +<?php echo $this->yellow->page->getContentHtml() ?> </div> </div> <?php $this->yellow->layout("footer") ?> diff --git a/system/layouts/footer.html b/system/layouts/footer.html @@ -1,11 +1,10 @@ <div class="footer" role="contentinfo"> <div class="siteinfo"> -<?php if ($page = $this->yellow->content->shared("footer")) $this->yellow->page->setPage("footer", $page) ?> -<?php if ($this->yellow->page->isPage("footer")) echo $this->yellow->page->getPage("footer")->getContent() ?> +<?php echo $this->yellow->page->getPage("footer")->getContentHtml() ?> </div> <div class="siteinfo-banner"></div> </div> </div> -<?php echo $this->yellow->page->getExtra("footer") ?> +<?php echo $this->yellow->page->getExtraHtml("footer") ?> </body> </html> diff --git a/system/layouts/header.html b/system/layouts/header.html @@ -7,17 +7,14 @@ <meta name="author" content="<?php echo $this->yellow->page->getHtml("author") ?>" /> <meta name="generator" content="Datenstrom Yellow" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> -<?php $resourceLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreResourceLocation") ?> -<link rel="icon" type="image/png" href="<?php echo $resourceLocation.$this->yellow->page->getHtml("theme")."-icon.png" ?>" /> -<?php echo $this->yellow->page->getExtra("header") ?> +<?php echo $this->yellow->page->getExtraHtml("header") ?> </head> <body> <div class="<?php echo "page layout-".$this->yellow->page->getHtml("layout") ?>"> <div class="header" role="banner"> <div class="sitename"> <h1><a href="<?php echo $this->yellow->page->getBase(true)."/" ?>"><i class="sitename-logo"></i><span class="latex">M<sub>I</sub>K<sup>U</sup>L<sup>I</sup>.C<sub>Z</sub></span></a></h1> -<?php if ($page = $this->yellow->content->shared("header")) $this->yellow->page->setPage("header", $page) ?> -<?php if ($this->yellow->page->isPage("header")) echo $this->yellow->page->getPage("header")->getContent() ?> +<?php echo $this->yellow->page->getPage("header")->getContentHtml() ?> </div> <div class="sitename-banner"></div> <?php $this->yellow->layout("navigation") ?> diff --git a/system/layouts/navigation.html b/system/layouts/navigation.html @@ -1,6 +1,6 @@ <?php $pages = $this->yellow->content->top() ?> <?php $this->yellow->page->setLastModified($pages->getModified()) ?> -<div class="navigation" role="navigation"> +<div class="navigation" role="navigation" aria-label="<?php echo $this->yellow->language->getTextHtml("coreNavigation") ?>"> <ul> <?php foreach ($pages as $page): ?> <li><a<?php echo $page->isActive() ? " class=\"active\" aria-current=\"page\"" : "" ?> href="<?php echo $page->getLocation(true) ?>"><?php echo $page->getHtml("titleNavigation") ?></a></li> diff --git a/system/layouts/pagination.html b/system/layouts/pagination.html @@ -1,11 +1,11 @@ <?php list($name, $pages) = $this->yellow->getLayoutArguments() ?> <?php if ($pages->isPagination()): ?> -<div class="pagination" role="navigation"> +<div class="pagination" role="navigation" aria-label="<?php echo $this->yellow->language->getTextHtml("corePagination") ?>"> <?php if ($pages->getPaginationPrevious()): ?> -<a class="previous" href="<?php echo $pages->getPaginationPrevious() ?>"><?php echo $this->yellow->text->getHtml("corePaginationPrevious") ?></a> +<a class="previous" href="<?php echo $pages->getPaginationPrevious() ?>"><?php echo $this->yellow->language->getTextHtml("corePaginationPrevious") ?></a> <?php endif ?> <?php if ($pages->getPaginationNext()): ?> -<a class="next" href="<?php echo $pages->getPaginationNext() ?>"><?php echo $this->yellow->text->getHtml("corePaginationNext") ?></a> +<a class="next" href="<?php echo $pages->getPaginationNext() ?>"><?php echo $this->yellow->language->getTextHtml("corePaginationNext") ?></a> <?php endif ?> </div> <?php endif ?> diff --git a/system/resources/stockholm-opensans-license.txt b/system/resources/stockholm-opensans-license.txt @@ -1,201 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -\ No newline at end of file diff --git a/system/resources/stockholm.css b/system/resources/stockholm.css @@ -1,386 +0,0 @@ -/* Stockholm extension, https://github.com/datenstrom/yellow-extensions/tree/master/themes/stockholm */ -/* Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se */ -/* This file may be used and distributed under the terms of the public license. */ - -html, body, div, form, pre, span, tr, th, td, img { - margin: 0; - padding: 0; - border: 0; - vertical-align: baseline; -} -@font-face { - font-family: "Open Sans"; - font-style: normal; - font-weight: 300; - src: url(stockholm-opensans-light.woff) format("woff"); -} -@font-face { - font-family: "Open Sans"; - font-style: normal; - font-weight: 400; - src: url(stockholm-opensans-regular.woff) format("woff"); -} -@font-face { - font-family: "Open Sans"; - font-style: normal; - font-weight: 700; - src: url(stockholm-opensans-bold.woff) format("woff"); -} -body { - margin: 1em; - background-color: #fff; - color: #666; - font-family: "Open Sans", Helvetica, sans-serif; - font-size: 1em; - font-weight: 300; - line-height: 1.5; -} -h1, -h2, -h3, -h4, -h5, -h6 { - color: #111; - font-weight: 400; - font-family: serif; -} -h1 { - font-size: 2em; -} -hr { - height: 1px; - background: #ddd; - border: 0; -} -strong { - font-weight: bold; -} -code { - font-size: 1.1em; -} -a { - color: #07d; - text-decoration: none; -} -a:hover { - color: #07d; - text-decoration: underline; -} - -/* Content */ - -.content h1 { - margin: 1em 0; -} -.content h1 a { - color: #111; -} -.content h1 a:hover { - color: #111; - text-decoration: none; -} -.content img { - max-width: 100%; - height: auto; -} -.content form { - margin: 1em 0; -} -.content table { - border-spacing: 0; - border-collapse: collapse; -} -.content th { - text-align: left; - padding: 0.3em; -} -.content td { - text-align: left; - padding: 0.3em; - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; -} -.content code, -.content pre { - font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-size: 90%; -} -.content code { - padding: 0.15em 0.4em; - margin: 0; - background-color: #f7f7f7; - border-radius: 3px; -} -.content pre > code { - padding: 0; - margin: 0; - white-space: pre; - background: transparent; - border: 0; - font-size: inherit; -} -.content pre { - padding: 1em; - overflow: auto; - line-height: 1.45; - background-color: #f7f7f7; - border-radius: 3px; -} -.content blockquote { - margin-left: 0; - padding-left: 1em; - border-left: 1px solid #ddd; -} -.content .notice1 { - margin: 1em 0; - padding: 10px 1em; - background-color: #fffbf0; - border-left: 10px solid #fb0; -} -.content .notice2 { - margin: 1em 0; - padding: 10px 1em; - background-color: #fdf0f0; - border-left: 10px solid #d00; -} -.content .notice3, -.content .notice4, -.content .notice5, -.content .notice6 { - margin: 1em 0; - padding: 10px 1em; - background-color: #f0f8fe; - border-left: 10px solid #08e; -} -.content .flexible { - position: relative; - padding-top: 0; - padding-bottom: 56.25%; -} -.content .flexible iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} -.content .task-list-item { - list-style-type: none; -} -.content .task-list-item input { - margin: 0 0.2em 0.25em -1.75em; - vertical-align: middle; -} -.content .toc { - margin: 0; - padding: 0; - list-style: none; -} -.content .wikipages ul, -.content .wikitags ul, -.content .wikilinks ul { - padding: 0; - list-style: none; - column-width: 19em; -} -.content .entry-links .previous { - margin-right: 1em; -} -.content .pagination .previous { - margin-right: 1em; -} -.content .pagination { - margin: 1em 0; -} -.content .left { - float: left; - margin: 0 1em 0 0; -} -.content .center { - display: block; - margin: 0 auto; -} -.content .right { - float: right; - margin: 0 0 0 1em; -} -.content .rounded { - border-radius: 4px; -} - -/* Header */ - -.header { - margin: 2em 0; -} -.header .sitename { - display: block; - float: left; -} -.header .sitename h1 { - margin: 0; - font-size: 1em; - font-weight: 300; -} -.header .sitename h1 a { - color: #666; - border-bottom: solid 3px #fff; - text-decoration: none; - padding: 0.5em 0; -} -.header .sitename h1 a:hover { - color: #07d; - border-bottom: solid 3px #29f; -} -.header .sitename p { - margin-top: 0; - color: #666; -} - -/* Navigation */ - -.navigation { - display: block; - float: right; - margin-top: 0.5em; - font-size: 1em; -} -.navigation a { - color: #666; - border-bottom: solid 3px #fff; - text-decoration: none; - padding: 0.5em 0; - margin: 0 0.5em; -} -.navigation a:hover { - color: #07d; - border-bottom: solid 3px #29f; -} -.navigation ul { - margin: 0 -0.5em; - padding: 0; - list-style: none; -} -.navigation li { - display: inline; -} -.navigation li a.active { - border-bottom: solid 3px #29f; -} -.navigation-banner { - clear: both; -} - -/* Footer */ - -.footer { - margin: 2em 0; -} -.footer .siteinfo a { - color: #07d; -} -.footer .siteinfo a:hover { - color: #07d; - text-decoration: underline; -} - -/* Forms and buttons */ - -.form-control { - margin: 0; - padding: 2px 4px; - display: inline-block; - min-width: 7em; - background-color: #fff; - color: #666; - background-image: linear-gradient(to bottom, #fff, #fff); - border: 1px solid #bbb; - border-radius: 4px; - font-size: 0.9em; - font-family: inherit; - font-weight: normal; - line-height: normal; -} -.btn { - margin: 0; - padding: 4px 22px; - display: inline-block; - min-width: 7em; - background-color: #eaeaea; - color: #333333; - background-image: linear-gradient(to bottom, #f8f8f8, #e1e1e1); - border: 1px solid #bbb; - border-color: #c1c1c1 #c1c1c1 #aaaaaa; - border-radius: 4px; - outline-offset: -2px; - font-size: 0.9em; - font-family: inherit; - font-weight: normal; - line-height: 1; - text-align: center; - text-decoration: none; - box-sizing: border-box; -} -.btn:hover, -.btn:focus, -.btn:active { - color: #333333; - background-image: none; - text-decoration: none; -} -.btn:active { - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); -} - -/* Responsive and print */ - -.page { - margin: 0 auto; - max-width: 1000px; -} - -@media screen and (min-width: 62em) { - body { - width: 60em; - margin: 1em auto; - } - .page { - margin: 0; - max-width: none; - } -} -@media screen and (max-width: 32em) { - body { - margin: 0.5em; - font-size: 0.9em; - } - .content h1, - .content h2 { - font-size: 1.5em; - } -} -@media print { - .page { - border: none !important; - } -} -.latex sub { - vertical-align: -0.6ex; - margin-left: -0.125em; - margin-right: -0.125em; - font-size: 1em; -} - -.latex { - font-family: serif; - font-size: 1.5em; -} - -.latex sup { - font-size: 0.7em; - vertical-align: 0.3em; - margin-left: -0.20em; - margin-right: -0.2em; -} - diff --git a/system/settings/text.ini b/system/settings/text.ini @@ -1,5 +0,0 @@ -# Datenstrom Yellow text settings - -Language: en -CoreDateFormatMedium: Y-m-d -picture.jpg: This is an example image diff --git a/system/resources/bundle-4ae45fbba2.min.js b/system/themes/bundle-4ae45fbba2.min.js diff --git a/system/resources/stockholm-opensans-bold.woff b/system/themes/stockholm-opensans-bold.woff Binary files differ. diff --git a/system/resources/stockholm-opensans-light.woff b/system/themes/stockholm-opensans-light.woff Binary files differ. diff --git a/system/resources/stockholm-opensans-regular.woff b/system/themes/stockholm-opensans-regular.woff Binary files differ. diff --git a/system/themes/stockholm.css b/system/themes/stockholm.css @@ -0,0 +1,410 @@ +/* Stockholm extension, https://github.com/annaesvensson/yellow-stockholm */ + +/* Colors and fonts */ + +:root { + --bg: #fff; + --code-bg: #f7f7f7; + --notice1-bg: #fffbf0; + --notice2-bg: #fdf0f0; + --notice3-bg: #f0f8fe; + --heading: #111; + --text: #666; + --code: #666; + --link: #07d; + --link-active: #29f; + --blockquote-accent: #29f; + --notice1-accent: #fb0; + --notice2-accent: #d00; + --notice3-accent: #08e; + --separator: #ddd; + --border: #bbb; + --font: "Open Sans", Helvetica, sans-serif; + --monospace-font: Consolas, Menlo, Courier, monospace; +} +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url(stockholm-opensans-light.woff) format("woff"); +} +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(stockholm-opensans-regular.woff) format("woff"); +} +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(stockholm-opensans-bold.woff) format("woff"); +} + +/* General */ + +html, body, div, form, pre, span, tr, th, td, img { + margin: 0; + padding: 0; + border: 0; + vertical-align: baseline; +} +body { + margin: 1em; + background-color: var(--bg); + color: var(--text); + font-family: var(--font); + font-size: 1em; + font-weight: 300; + line-height: 1.5; +} +h1, h2, h3, h4, h5, h6 { + color: var(--heading); + font-weight: 400; + font-family: serif; +} +h1 { + font-size: 2em; +} +hr { + height: 1px; + background: var(--separator); + border: 0; +} +strong { + font-weight: bold; +} +code { + font-size: 1.1em; +} +a { + color: var(--link); + text-decoration: none; +} +a:hover { + color: var(--link); + text-decoration: underline; +} + +/* Content */ + +.content h1 { + margin: 1em 0; +} +.content h1 a { + color: var(--heading); +} +.content h1 a:hover { + color: var(--heading); + text-decoration: none; +} +.content img { + max-width: 100%; + height: auto; +} +.content form { + margin: 1em 0; +} +.content table { + border-spacing: 0; + border-collapse: collapse; +} +.content th { + text-align: left; + padding: 0.3em; + border-bottom: 1px solid var(--separator); +} +.content td { + text-align: left; + padding: 0.3em; + padding-right: 2em; +} +.content code, +.content pre { + font-family: var(--monospace-font); + font-size: 90%; +} +.content code { + padding: 0.15em 0.4em; + margin: 0; + background-color: var(--code-bg); + color: var(--code); + border-radius: 3px; +} +.content pre > code { + padding: 0; + margin: 0; + white-space: pre; + background: transparent; + border: 0; + font-size: inherit; +} +.content pre { + padding: 1em; + overflow: auto; + line-height: 1.45; + background-color: var(--code-bg); + color: var(--code); + border-radius: 3px; +} +.content blockquote { + margin-left: 0; + padding-left: 1em; + font-weight: bold; + border-left: 3px solid var(--blockquote-accent); +} +.content .notice1 { + margin: 1em 0; + padding: 10px 1em; + background-color: var(--notice1-bg); + border-left: 10px solid var(--notice1-accent); +} +.content .notice2 { + margin: 1em 0; + padding: 10px 1em; + background-color: var(--notice2-bg); + border-left: 10px solid var(--notice2-accent); +} +.content .notice3, +.content .notice4, +.content .notice5, +.content .notice6 { + margin: 1em 0; + padding: 10px 1em; + background-color: var(--notice3-bg); + border-left: 10px solid var(--notice3-accent); +} +.content .flexible { + position: relative; + padding-top: 0; + padding-bottom: 56.25%; +} +.content .flexible iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.content .task-list-item { + list-style-type: none; +} +.content .task-list-item input { + margin: 0 0.2em 0.25em -1.75em; + vertical-align: middle; +} +.content .toc { + margin: 0; + padding: 0; + list-style: none; +} +.content .wikipages ul, +.content .wikitags ul, +.content .wikilinks ul { + padding: 0; + list-style: none; + column-width: 19em; +} +.content .previousnext .previous { + margin-right: 1em; +} +.content .pagination .previous { + margin-right: 1em; +} +.content .pagination { + margin: 1em 0; +} +.content .left { + float: left; + margin: 0 1em 0 0; +} +.content .center { + display: block; + margin: 0 auto; +} +.content .right { + float: right; + margin: 0 0 0 1em; +} +.content .rounded { + border-radius: 4px; +} + +/* Header */ + +.header { + margin: 2em 0; +} +.header .sitename { + display: block; + float: left; +} +.header .sitename h1 { + margin: 0; + font-size: 1em; + font-weight: 300; +} +.header .sitename h1 a { + color: var(--text); + border-bottom: solid 3px var(--bg); + text-decoration: none; + padding: 0.5em 0; +} +.header .sitename h1 a:hover { + color: var(--link); + border-bottom: solid 3px var(--link-active); +} +.header .sitename p { + margin-top: 0; + color: var(--text); +} + +/* Navigation */ + +.navigation { + display: block; + float: right; + margin-top: 0.5em; + font-size: 1em; +} +.navigation a { + color: var(--text); + border-bottom: solid 3px var(--bg); + text-decoration: none; + padding: 0.5em 0; + margin: 0 0.5em; +} +.navigation a:hover { + color: var(--link); + border-bottom: solid 3px var(--link-active); +} +.navigation ul { + margin: 0 -0.5em; + padding: 0; + list-style: none; +} +.navigation li { + display: inline; +} +.navigation li a.active { + border-bottom: solid 3px var(--link-active); +} +.navigation-banner { + clear: both; +} + +/* Footer */ + +.footer { + margin: 2em 0; +} +.footer .siteinfo a { + color: var(--link); +} +.footer .siteinfo a:hover { + color: var(--link); + text-decoration: underline; +} + +/* Forms and buttons */ + +.form-control { + margin: 0; + padding: 2px 4px; + display: inline-block; + min-width: 7em; + background-color: var(--bg); + color: var(--text); + background-image: linear-gradient(to bottom, var(--bg), var(--bg)); + border: 1px solid var(--border); + border-radius: 4px; + font-size: 0.9em; + font-family: inherit; + font-weight: normal; + line-height: normal; +} +.btn { + margin: 0; + padding: 4px 22px; + display: inline-block; + min-width: 7em; + background-color: #eaeaea; + color: #333333; + background-image: linear-gradient(to bottom, #f8f8f8, #e1e1e1); + border: 1px solid var(--border); + border-color: #c1c1c1 #c1c1c1 #aaaaaa; + border-radius: 4px; + outline-offset: -2px; + font-size: 0.9em; + font-family: inherit; + font-weight: normal; + line-height: 1; + text-align: center; + text-decoration: none; + box-sizing: border-box; +} +.btn:hover, +.btn:focus, +.btn:active { + color: #333333; + background-image: none; + text-decoration: none; +} +.btn:active { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Responsive and print */ + +.page { + margin: 0 auto; + max-width: 1000px; +} + +@media screen and (min-width: 62em) { + body { + width: 60em; + margin: 1em auto; + } + .page { + margin: 0; + max-width: none; + } +} +@media screen and (max-width: 32em) { + body { + margin: 0.5em; + font-size: 0.9em; + } + .content h1, + .content h2 { + font-size: 1.5em; + } +} +@media print { + .page { + border: none !important; + } +} +.latex sub { + vertical-align: -0.6ex; + margin-left: -0.125em; + margin-right: -0.125em; + font-size: 1em; +} + +.latex { + font-family: serif; + font-size: 1.5em; +} + +.latex sup { + font-size: 0.7em; + vertical-align: 0.3em; + margin-left: -0.20em; + margin-right: -0.2em; +} + diff --git a/system/resources/stockholm-icon.png b/system/themes/stockholm.png Binary files differ. diff --git a/yellow.php b/yellow.php @@ -1,9 +1,7 @@ <?php -// Datenstrom Yellow is for people who make small websites, https://datenstrom.se/yellow/ -// Copyright (c) 2013-2020 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. +// Datenstrom Yellow, https://github.com/datenstrom/yellow -require_once("system/extensions/core.php"); +require("system/extensions/core.php"); if (PHP_SAPI!="cli") { $yellow = new YellowCore();