mikuli.cz

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

commit c17da68c1a5911c79390f6f002fcd3a278ad067f
parent a19b77b40cdda6b533a78ef543b7d2e435c68fe7
Author: markseu <mark2011@mayberg.se>
Date:   Sat, 23 Feb 2019 15:04:34 +0100

Updated system, new API

Diffstat:
Dsystem/config/config.ini | 82-------------------------------------------------------------------------------
Dsystem/config/text.ini | 2--
Asystem/extensions/bundle.php | 1939+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/extensions/command.php | 622+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/extensions/core.php | 3185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/extensions/edit.css | 553+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/extensions/edit.js | 1317+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/extensions/edit.php | 1862+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsystem/plugins/edit.woff -> system/extensions/edit.woff | 0
Asystem/extensions/flatsite.php | 9+++++++++
Asystem/extensions/image.php | 261+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/extensions/install-blog.zip | 0
Asystem/extensions/install-languages.zip | 0
Asystem/extensions/install-wiki.zip | 0
Asystem/extensions/install.php | 303+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/extensions/markdown.php | 3869+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/extensions/update.php | 750+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/layouts/default.html | 9+++++++++
Asystem/layouts/error.html | 8++++++++
Asystem/layouts/footer.html | 10++++++++++
Asystem/layouts/header.html | 27+++++++++++++++++++++++++++
Asystem/layouts/navigation-sidebar.html | 10++++++++++
Asystem/layouts/navigation-tree.html | 16++++++++++++++++
Asystem/layouts/navigation.html | 10++++++++++
Asystem/layouts/pagination.html | 11+++++++++++
Asystem/layouts/sidebar.html | 21+++++++++++++++++++++
Dsystem/plugins/bundle.php | 1938-------------------------------------------------------------------------------
Dsystem/plugins/command.php | 621-------------------------------------------------------------------------------
Dsystem/plugins/core.php | 3122-------------------------------------------------------------------------------
Dsystem/plugins/edit.css | 553-------------------------------------------------------------------------------
Dsystem/plugins/edit.js | 1317-------------------------------------------------------------------------------
Dsystem/plugins/edit.php | 1862-------------------------------------------------------------------------------
Dsystem/plugins/image.php | 260-------------------------------------------------------------------------------
Dsystem/plugins/install-blog.zip | 0
Dsystem/plugins/install-language.zip | 0
Dsystem/plugins/install-wiki.zip | 0
Dsystem/plugins/install.php | 303-------------------------------------------------------------------------------
Dsystem/plugins/markdown.php | 3868-------------------------------------------------------------------------------
Dsystem/plugins/update.php | 710-------------------------------------------------------------------------------
Asystem/resources/flatsite-icon.png | 0
Asystem/resources/flatsite.css | 541+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsystem/themes/assets/opensans-bold.woff -> system/resources/opensans-bold.woff | 0
Rsystem/themes/assets/opensans-light.woff -> system/resources/opensans-light.woff | 0
Rsystem/themes/assets/opensans-regular.woff -> system/resources/opensans-regular.woff | 0
Asystem/settings/system.ini | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/settings/text.ini | 2++
Rsystem/config/user.ini -> system/settings/user.ini | 0
Dsystem/themes/assets/flatsite.css | 541-------------------------------------------------------------------------------
Dsystem/themes/assets/flatsite.php | 8--------
Dsystem/themes/assets/icon.png | 0
Dsystem/themes/snippets/footer.php | 11-----------
Dsystem/themes/snippets/header.php | 27---------------------------
Dsystem/themes/snippets/navigation-sidebar.php | 10----------
Dsystem/themes/snippets/navigation-tree.php | 16----------------
Dsystem/themes/snippets/navigation.php | 10----------
Dsystem/themes/snippets/pagination.php | 11-----------
Dsystem/themes/snippets/sidebar.php | 21---------------------
Dsystem/themes/templates/default.html | 9---------
Dsystem/themes/templates/error.html | 8--------
59 files changed, 15409 insertions(+), 15310 deletions(-)

diff --git a/system/config/config.ini b/system/config/config.ini @@ -1,82 +0,0 @@ -# Datenstrom Yellow configuration - -Sitename: Datenstrom Yellow -Author: Datenstrom -Email: webmaster -Language: en -Timezone: UTC -Theme: flatsite - -StaticUrl: -StaticDefaultFile: index.html -StaticErrorFile: 404.html -StaticDir: public/ -CacheDir: cache/ -MediaLocation: /media/ -DownloadLocation: /media/downloads/ -ImageLocation: /media/images/ -PluginLocation: /media/plugins/ -ThemeLocation: /media/themes/ -AssetLocation: /media/themes/assets/ -MediaDir: media/ -DownloadDir: media/downloads/ -ImageDir: media/images/ -SystemDir: system/ -ConfigDir: system/config/ -PluginDir: system/plugins/ -ThemeDir: system/themes/ -AssetDir: system/themes/assets/ -SnippetDir: system/themes/snippets/ -TemplateDir: system/themes/templates/ -TrashDir: system/trash/ -ContentDir: content/ -ContentRootDir: default/ -ContentHomeDir: home/ -ContentSharedDir: shared/ -ContentPagination: page -ContentDefaultFile: page.md -ContentExtension: .md -ConfigExtension: .ini -DownloadExtension: .download -TextFile: text.ini -NewFile: page-new-(.*).md -LanguageFile: language-(.*).txt -ServerUrl: -StartupUpdate: none -Template: default -Navigation: navigation -Header: header -Footer: footer -Sidebar: sidebar -Siteicon: icon -Parser: markdown -MultiLanguageMode: 0 -SafeMode: 0 -BundleAndMinify: 1 -EditLocation: /edit/ -EditUploadNewLocation: /media/@group/@filename -EditUploadExtensions: .gif, .jpg, .pdf, .png, .svg, .tgz, .zip -EditKeyboardShortcuts: ctrl+b bold, ctrl+i italic, ctrl+e code, ctrl+k link, ctrl+s save, ctrl+shift+p preview -EditToolbarButtons: auto -EditEndOfLine: auto -EditUserFile: user.ini -EditUserPasswordMinLength: 8 -EditUserHashAlgorithm: bcrypt -EditUserHashCost: 10 -EditUserHome: / -EditLoginRestrictions: 0 -EditLoginSessionTimeout: 2592000 -EditBruteForceProtection: 25 -ImageAlt: Image -ImageUploadWidthMax: 1280 -ImageUploadHeightMax: 1280 -ImageUploadJpgQuality: 80 -ImageThumbnailLocation: /media/thumbnails/ -ImageThumbnailDir: media/thumbnails/ -ImageThumbnailJpgQuality: 80 -UpdatePluginsUrl: https://github.com/datenstrom/yellow-plugins -UpdateThemesUrl: https://github.com/datenstrom/yellow-themes -UpdateInformationFile: update.ini -UpdateVersionFile: version.ini -UpdateResourceFile: resource.ini - diff --git a/system/config/text.ini b/system/config/text.ini @@ -1,2 +0,0 @@ -# Datenstrom Yellow text - diff --git a/system/extensions/bundle.php b/system/extensions/bundle.php @@ -0,0 +1,1939 @@ +<?php +// Bundle extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/bundle +// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +class YellowBundle { + const VERSION = "0.8.2"; + const TYPE = "feature"; + public $yellow; //access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("bundleAndMinify", "1"); + } + + // 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($args) { + list($command) = $args; + switch ($command) { + case "clean": $statusCode = $this->processCommandClean($args); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Process command to clean bundles + public function processCommandClean($args) { + $statusCode = 0; + list($command, $path) = $args; + if ($path=="all") { + $path = $this->yellow->system->get("resourceDir"); + 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 = $dataScript = $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 (is_null($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 (is_null($dataScript[$matches[2]])) $dataScript[$matches[2]] = $line; + } else { + array_push($dataOther, $line); + } + } else { + array_push($dataOther, $line); + } + } + if ($this->yellow->system->get("bundleAndMinify")) { + $dataCss = $this->processBundle($dataCss, "css"); + $dataScript = $this->processBundle($dataScript, "js"); + } + $output = implode($dataMeta).implode($dataLink).implode($dataCss).implode($dataScript).implode($dataOther); + return $output; + } + + // Process bundle, create file on demand + public function processBundle($data, $type) { + $fileNames = array(); + $scheme = $this->yellow->system->get("serverScheme"); + $address = $this->yellow->system->get("serverAddress"); + $base = $this->yellow->system->get("serverBase"); + foreach ($data as $key=>$value) { + if (preg_match("/^\w+:/", $key)) continue; + if (preg_match("/data-bundle=\"none\"/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)) { + $this->yellow->toolbox->timerStart($time); + $id = substru(md5(implode($fileNames).$base), 0, 10); + $fileNameBundle = $this->yellow->system->get("resourceDir")."bundle-$id.min.$type";; + $locationBundle = $base.$this->yellow->system->get("resourceLocation")."bundle-$id.min.$type"; + if ($type=="css") { + $data[$locationBundle] = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"".htmlspecialchars($locationBundle)."\" />\n"; + } else { + $data[$locationBundle] = "<script type=\"text/javascript\" defer=\"defer\" src=\"".htmlspecialchars($locationBundle)."\"></script>\n"; + } + if ($this->yellow->toolbox->getFileModified($fileNameBundle)!=$modified) { + 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 (!empty($fileDataNew)) $fileDataNew .= "\n\n"; + $fileDataNew .= "/* ".basename($fileName)." */\n"; + $fileDataNew .= $fileData; + } + if (defined("DEBUG") && DEBUG>=2) { + if (!empty($fileDataNew)) $fileDataNew .= "\n\n"; + $fileDataNew .= "/* YellowBundle::processBundle file:$fileNameBundle <- ".$this->yellow->page->fileName." */"; + } + if (is_file($fileNameBundle)) $this->yellow->toolbox->deleteFile($fileNameBundle); + if (!$this->yellow->toolbox->createFile($fileNameBundle, $fileDataNew) || + !$this->yellow->toolbox->modifyFile($fileNameBundle, $modified)) { + $this->yellow->page->error(500, "Can't write file '$fileNameBundle'!"); + } + } + $this->yellow->toolbox->timerStop($time); + if (defined("DEBUG") && DEBUG>=2) { + $data["debug"] = "YellowBundle::processBundle file:$fileNameBundle time:$time ms<br/>\n"; + } + } + return $data; + } + + // Process bundle, convert URLs + public function processBundleConvert($scheme, $address, $base, $fileData, $fileName, $type) { + if ($type=="css") { + $extensionDirLength = strlenu($this->yellow->system->get("extensionDir")); + if (substru($fileName, 0, $extensionDirLength) == $this->yellow->system->get("extensionDir")) { + $base .= $this->yellow->system->get("extensionLocation"); + } else { + $base .= $this->yellow->system->get("resourceLocation"); + } + $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 (!isset($positions[$i])) { + 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(); + $css = $this->replace($css); + + $css = $this->stripWhitespace($css); + $css = $this->shortenHex($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 shortenHex($content) + { + $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?=[; }])/i', '#$1$2$3', $content); + + // we can shorten some even more by replacing them with their color name + $colors = array( + '#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', + ); + + 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 + // best to just leave `calc()`s alone, even if they could be optimized + // (which is a whole other undertaking, where units & order of + // operations all need to be considered...) + $calcs = $this->findCalcs($content); + $content = str_replace($calcs, array_keys($calcs), $content); + + // 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); + + // restore `calc()` expressions + $content = str_replace(array_keys($calcs), $calcs, $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() + { + $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); + } + + /** + * Find all `calc()` occurrences. + * + * @param string $content The CSS content to find `calc()`s in. + * + * @return string[] + */ + protected function findCalcs($content) + { + $results = array(); + preg_match_all('/calc(\(.+?)(?=$|;|calc\()/', $content, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + $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; + } + } + + $results['calc('.count($results).')'] = 'calc'.$expr; + } + + return $results; + } + + /** + * 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() + { + // single-line comments + $this->registerPattern('/\/\/.*$/m', ''); + + // multi-line comments + $this->registerPattern('/\/\*.*?\*\//s', ''); + } + + /** + * 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 @@ -0,0 +1,622 @@ +<?php +// Command extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/command +// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +class YellowCommand { + const VERSION = "0.8.2"; + const TYPE = "feature"; + public $yellow; //access to API + public $files; //number of files + public $links; //number of links + public $errors; //number of errors + public $locationsArgs; //locations with location arguments detected + public $locationsArgsPagination; //locations with pagination arguments detected + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle command + public function onCommand($args) { + list($command) = $args; + switch ($command) { + case "": $statusCode = $this->processCommandHelp(); break; + case "about": $statusCode = $this->processCommandAbout($args); break; + case "build": $statusCode = $this->processCommandBuild($args); break; + case "check": $statusCode = $this->processCommandCheck($args); break; + case "clean": $statusCode = $this->processCommandClean($args); break; + case "serve": $statusCode = $this->processCommandServe($args); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + $help .= "about\n"; + $help .= "build [directory location]\n"; + $help .= "check [directory location]\n"; + $help .= "clean [directory location]\n"; + $help .= "serve [url]\n"; + return $help; + } + + // Process command to show available commands + public function processCommandHelp() { + echo "Datenstrom Yellow is for people who make websites.\n"; + $lineCounter = 0; + foreach ($this->getCommandHelp() as $line) { + echo(++$lineCounter>1 ? " " : "Syntax: ")."php yellow.php $line\n"; + } + return 200; + } + + // Process command to show website version and updates + public function processCommandAbout($args) { + $serverVersion = $this->yellow->toolbox->getServerVersion(); + echo "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion\n"; + list($statusCode, $dataCurrent) = $this->getExtensionsVersion(); + list($statusCode, $dataLatest) = $this->getExtensionsVersion(true); + foreach ($dataCurrent as $key=>$value) { + if (strnatcasecmp($dataCurrent[$key], $dataLatest[$key])>=0) { + echo ucfirst($key)." $value\n"; + } else { + echo ucfirst($key)." $value - Update available\n"; + } + } + if ($statusCode!=200) echo "ERROR checking updates: ".$this->yellow->page->get("pageError")."\n"; + return $statusCode; + } + + // Process command to build static website + public function processCommandBuild($args) { + $statusCode = 0; + list($command, $path, $location) = $args; + if (empty($location) || $location[0]=="/") { + if ($this->checkStaticSettings()) { + $statusCode = $this->buildStaticFiles($path, $location); + } else { + $statusCode = 500; + $this->files = 0; + $this->errors = 1; + $fileName = $this->yellow->system->get("settingDir").$this->yellow->system->get("systemFile"); + echo "ERROR building files: Please configure StaticUrl 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("staticDir") : $path, "/"); + $this->files = $this->errors = 0; + $this->locationsArgs = $this->locationsArgsPagination = array(); + $statusCode = empty($locationFilter) ? $this->cleanStaticFiles($path, $locationFilter) : 200; + $staticUrl = $this->yellow->system->get("staticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + foreach ($this->getContentLocations() as $location) { + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + $statusCode = max($statusCode, $this->buildStaticFile($path, $location, true)); + } + foreach ($this->locationsArgs as $location) { + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + $statusCode = max($statusCode, $this->buildStaticFile($path, $location, true)); + } + foreach ($this->locationsArgsPagination as $location) { + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + if (substru($location, -1)!=$this->yellow->toolbox->getLocationArgsSeparator()) { + $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() as $location) { + $statusCode = max($statusCode, $this->buildStaticFile($path, $location)); + } + $statusCode = max($statusCode, $this->buildStaticFile($path, "/error/", false, false, true)); + } + 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("staticUrl"); + 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 "ERROR 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) = explode(":", $address); + if (is_null($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) { + $pagination = $this->yellow->system->get("contentPagination"); + 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->isLocationArgs($location)) continue; + if (!$this->yellow->toolbox->isLocationArgsPagination($location, $pagination)) { + $location = rtrim($location, "/")."/"; + if (is_null($this->locationsArgs[$location])) { + $this->locationsArgs[$location] = $location; + if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLocations detected location:$location<br/>\n"; + } + } else { + $location = rtrim($location, "0..9"); + if (is_null($this->locationsArgsPagination[$location])) { + $this->locationsArgsPagination[$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($args) { + $statusCode = 0; + list($command, $path, $location) = $args; + if (empty($location) || $location[0]=="/") { + if ($this->checkStaticSettings()) { + $statusCode = $this->checkStaticFiles($path, $location); + } else { + $statusCode = 500; + $this->files = $this->links = 0; + $fileName = $this->yellow->system->get("settingDir").$this->yellow->system->get("systemFile"); + echo "ERROR checking files: Please configure StaticUrl in file '$fileName'!\n"; + } + echo "Yellow $command: $this->files file".($this->files!=1 ? "s" : ""); + echo ", $this->links link".($this->links!=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("staticDir") : $path, "/"); + $this->files = $this->links = 0; + $regex = "/^[^.]+$|".$this->yellow->system->get("staticDefaultFile")."$/"; + $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("staticUrl"); + 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 (!is_null($links[$url])) $links[$url] .= ","; + $links[$url] .= $locationSource; + if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLinks detected url:$url<br/>\n"; + } elseif ($location[0]=="/") { + $url = "$scheme://$address$location"; + if (!is_null($links[$url])) $links[$url] .= ","; + $links[$url] .= $locationSource; + if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLinks detected url:$url<br/>\n"; + } + } + ++$this->files; + if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::analyseLinks location:$locationSource<br/>\n"; + } else { + $statusCode = 500; + echo "ERROR reading files: Can't read file '$fileName'!\n"; + } + } + $this->links = count($links); + } else { + $statusCode = 500; + 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; + $broken = $redirect = $data = array(); + $staticUrl = $this->yellow->system->get("staticUrl"); + $staticUrlLength = strlenu(rtrim($staticUrl, "/")); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $staticLocations = $this->getContentLocations(true); + uksort($links, "strnatcasecmp"); + foreach ($links as $url=>$value) { + if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::analyseStatus url:$url\n"; + 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)) { + $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; + } + } + } + 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($args) { + $statusCode = 0; + list($command, $path, $location) = $args; + if (empty($location) || $location[0]=="/") { + $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("staticDir") : $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($args) { + $statusCode = 0; + foreach ($this->yellow->extensions->extensions as $key=>$value) { + if ($key=="command") continue; + if (method_exists($value["obj"], "onCommand")) { + $statusCode = $value["obj"]->onCommand(func_get_args()); + if ($statusCode!=0) break; + } + } + return $statusCode; + } + + // Process command to start built-in web server + public function processCommandServe($args) { + list($command, $url) = $args; + 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"; + system("php -S $address yellow.php", $returnStatus); + $statusCode = $returnStatus!=0 ? 500 : 200; + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Check static settings + public function checkStaticSettings() { + return !empty($this->yellow->system->get("staticUrl")); + } + + // Check static directory + public function checkStaticDirectory($path) { + $ok = false; + if (!empty($path)) { + if ($path==rtrim($this->yellow->system->get("staticDir"), "/")) $ok = true; + if ($path==rtrim($this->yellow->system->get("trashDir"), "/")) $ok = true; + if (is_file("$path/".$this->yellow->system->get("staticDefaultFile"))) $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) = explode(" ", $line); + if (!empty($command) && is_null($data[$command])) $data[$command] = $line; + } + } + } + uksort($data, "strnatcasecmp"); + return $data; + } + + // Return extensions version + public function getExtensionsVersion($latest = false) { + $data = array(); + if ($this->yellow->extensions->isExisting("update")) { + list($statusCode, $data) = $this->yellow->extensions->get("update")->getExtensionsVersion($latest); + } else { + $statusCode = 200; + $data = $this->yellow->extensions->getData(); + } + return array($statusCode, $data); + } + + // Return human readable status + public function getStatusFormatted($statusCode) { + return $this->yellow->toolbox->getHttpStatusFormatted($statusCode, true); + } + + // 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("staticDefaultFile"); + } elseif ($statusCode==404) { + $fileName = $path."/".$this->yellow->system->get("staticErrorFile"); + } + return $fileName; + } + + // Return static location + public function getStaticLocation($path, $fileName) { + $location = substru($fileName, strlenu($path)); + if (basename($location)==$this->yellow->system->get("staticDefaultFile")) { + $defaultFileLength = strlenu($this->yellow->system->get("staticDefaultFile")); + $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("staticUrl"); + 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 (($page->get("status")!="ignore" && $page->get("status")!="draft") || $includeAll) { + array_push($locations, $page->location); + } + } + if (!$this->yellow->content->find("/") && $this->yellow->system->get("multiLanguageMode")) array_unshift($locations, "/"); + return $locations; + } + + // Return media locations + public function getMediaLocations() { + $locations = array(); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->system->get("mediaDir"), "/.*/", 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)$/"; + $extensionDirLength = strlenu($this->yellow->system->get("extensionDir")); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->system->get("extensionDir"), $regex, false, false); + foreach ($fileNames as $fileName) { + array_push($locations, $this->yellow->system->get("extensionLocation").substru($fileName, $extensionDirLength)); + } + $resourceDirLength = strlenu($this->yellow->system->get("resourceDir")); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->system->get("resourceDir"), $regex, false, false); + foreach ($fileNames as $fileName) { + array_push($locations, $this->yellow->system->get("resourceLocation").substru($fileName, $resourceDirLength)); + } + return $locations; + } + + // Return extra locations + public function getExtraLocations() { + $locations = array(); + $pathIgnore = "(".$this->yellow->system->get("staticDir")."|". + $this->yellow->system->get("cacheDir")."|". + $this->yellow->system->get("contentDir")."|". + $this->yellow->system->get("mediaDir")."|". + $this->yellow->system->get("systemDir").")"; + $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 @@ -0,0 +1,3185 @@ +<?php +// Core extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/core +// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +class YellowCore { + const VERSION = "0.8.2"; + 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 + public $pages; //TODO: remove later, for backwards compatibility + public $files; //TODO: remove later, for backwards compatibility + public $config; //TODO: remove later, for backwards compatibility + public $plugins; //TODO: remove later, for backwards compatibility + public $themes; //TODO: remove later, for backwards compatibility + + public function __construct() { + $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->lookup = new YellowLookup($this); + $this->toolbox = new YellowToolbox(); + $this->extensions = new YellowExtensions($this); + $this->pages = new YellowPages($this); + $this->files = new YellowFiles($this); + $this->config = new YellowConfig($this); + $this->plugins = new YellowPlugins($this); + $this->themes = new YellowThemes($this); + $this->system->setDefault("sitename", "Yellow"); + $this->system->setDefault("author", "Yellow"); + $this->system->setDefault("email", "webmaster"); + $this->system->setDefault("language", "en"); + $this->system->setDefault("timezone", "UTC"); + $this->system->setDefault("staticUrl", ""); + $this->system->setDefault("staticDefaultFile", "index.html"); + $this->system->setDefault("staticErrorFile", "404.html"); + $this->system->setDefault("staticDir", "public/"); + $this->system->setDefault("cacheDir", "cache/"); + $this->system->setDefault("mediaLocation", "/media/"); + $this->system->setDefault("downloadLocation", "/media/downloads/"); + $this->system->setDefault("imageLocation", "/media/images/"); + $this->system->setDefault("extensionLocation", "/media/extensions/"); + $this->system->setDefault("resourceLocation", "/media/resources/"); + $this->system->setDefault("mediaDir", "media/"); + $this->system->setDefault("downloadDir", "media/downloads/"); + $this->system->setDefault("imageDir", "media/images/"); + $this->system->setDefault("systemDir", "system/"); + $this->system->setDefault("extensionDir", "system/extensions/"); + $this->system->setDefault("layoutDir", "system/layouts/"); + $this->system->setDefault("resourceDir", "system/resources/"); + $this->system->setDefault("settingDir", "system/settings/"); + $this->system->setDefault("trashDir", "system/trash/"); + $this->system->setDefault("contentDir", "content/"); + $this->system->setDefault("contentRootDir", "default/"); + $this->system->setDefault("contentHomeDir", "home/"); + $this->system->setDefault("contentSharedDir", "shared/"); + $this->system->setDefault("contentPagination", "page"); + $this->system->setDefault("contentDefaultFile", "page.md"); + $this->system->setDefault("contentExtension", ".md"); + $this->system->setDefault("downloadExtension", ".download"); + $this->system->setDefault("systemFile", "system.ini"); + $this->system->setDefault("textFile", "text.ini"); + $this->system->setDefault("serverUrl", ""); + $this->system->setDefault("layout", "default"); + $this->system->setDefault("theme", "default"); + $this->system->setDefault("parser", "markdown"); + $this->system->setDefault("navigation", "navigation"); + $this->system->setDefault("header", "header"); + $this->system->setDefault("footer", "footer"); + $this->system->setDefault("sidebar", "sidebar"); + $this->system->setDefault("startupUpdate", "none"); + $this->system->setDefault("multiLanguageMode", "0"); + $this->system->setDefault("safeMode", "0"); + } + + public function __destruct() { + $this->shutdown(); + } + + // Handle initialisation + public function load() { + if (defined("DEBUG") && DEBUG>=2) { + $serverVersion = $this->toolbox->getServerVersion(); + echo "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion<br/>\n"; + } + $this->toolbox->timerStart($time); + $this->system->load($this->system->get("settingDir").$this->system->get("systemFile")); + $this->extensions->load($this->system->get("extensionDir")); + $this->text->load($this->system->get("extensionDir")); + $this->text->load($this->system->get("settingDir"), $this->system->get("textFile"), $this->system->get("language")); + $this->lookup->detectFileSystem(); + $this->startup(); + $this->toolbox->timerStop($time); + if (defined("DEBUG") && DEBUG>=2) { + $extensions = count($this->extensions->extensions); + echo "YellowCore::load extensions:$extensions time:$time ms<br/>\n"; + } + } + + // Handle request + 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")) { + $this->lookup->requestHandler = $key; + $statusCode = $value["obj"]->onRequest($scheme, $address, $base, $location, $fileName); + if ($statusCode!=0) break; + } + } + if ($statusCode==0) { + $this->lookup->requestHandler = "core"; + $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName, true); + } + if ($this->page->isExisting("pageError")) $statusCode = $this->processRequestError(); + ob_end_flush(); + $this->toolbox->timerStop($time); + if (defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName)) { + $handler = $this->getRequestHandler(); + echo "YellowCore::request status:$statusCode handler:$handler time:$time ms<br/>\n"; + } + return $statusCode; + } + + // Process request + public function processRequest($scheme, $address, $base, $location, $fileName, $cacheable) { + $statusCode = 0; + if (is_readable($fileName)) { + if ($this->toolbox->isRequestCleanUrl($location)) { + $location = $location.$this->getRequestLocationArgsClean(); + $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->sendStatus(303, $location); + } + } else { + if ($this->lookup->isRedirectLocation($location)) { + $location = $this->lookup->isFileLocation($location) ? "$location/" : "/".$this->getRequestLanguage()."/"; + $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->sendStatus(301, $location); + } + } + 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 (defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName)) { + echo "YellowCore::processRequest file:$fileName<br/>\n"; + } + return $statusCode; + } + + // 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"; + return $statusCode; + } + + // Read page + public function readPage($scheme, $address, $base, $location, $fileName, $cacheable, $statusCode, $pageError) { + if ($statusCode>=400) { + $language = $this->lookup->findLanguageFromFile($fileName, $this->system->get("language")); + if ($this->text->isExisting("error${statusCode}Title", $language)) { + $rawData = "---\nTitle:".$this->text->getText("error${statusCode}Title", $language)."\n"; + $rawData .= "Layout:error\nLanguage:$language\n---\n".$this->text->getText("error${statusCode}Text", $language); + } else { + $rawData = "---\nTitle:".$this->toolbox->getHttpStatusFormatted($statusCode, true)."\n"; + $rawData .= "Layout:error\nLanguage:en\n---\n[yellow error]"; + } + $cacheable = false; + } else { + $rawData = $this->toolbox->readFile($fileName); + } + $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; + } + + // Send page response + public function sendPage() { + $this->page->parsePage(); + $statusCode = $this->page->statusCode; + $lastModifiedFormatted = $this->page->getHeader("Last-Modified"); + if ($statusCode==200 && $this->page->isCacheable() && $this->toolbox->isRequestNotModified($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) { + foreach ($this->page->headerData as $key=>$value) { + echo "YellowCore::sendPage $key: $value<br/>\n"; + } + $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"; + } + return $statusCode; + } + + // Send file response + public function sendFile($statusCode, $fileName, $cacheable) { + $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted($this->toolbox->getFileModified($fileName)); + if ($statusCode==200 && $cacheable && $this->toolbox->isRequestNotModified($lastModifiedFormatted)) { + $statusCode = 304; + @header($this->toolbox->getHttpStatusFormatted($statusCode)); + } else { + @header($this->toolbox->getHttpStatusFormatted($statusCode)); + if (!$cacheable) @header("Cache-Control: no-cache, must-revalidate"); + @header("Content-Type: ".$this->toolbox->getMimeContentType($fileName)); + @header("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, must-revalidate"); + @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)); + 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"; + } + } + return $statusCode; + } + + // Handle command + public function command($args = null) { + $statusCode = 0; + $this->toolbox->timerStart($time); + foreach ($this->extensions->extensions as $key=>$value) { + if (method_exists($value["obj"], "onCommand")) { + $this->lookup->commandHandler = $key; + $statusCode = $value["obj"]->onCommand(func_get_args()); + if ($statusCode!=0) break; + } + } + if ($statusCode==0) { + $this->lookup->commandHandler = "core"; + $statusCode = 400; + list($command) = func_get_args(); + echo "Yellow $command: Command not found\n"; + } + $this->toolbox->timerStop($time); + if (defined("DEBUG") && DEBUG>=1) { + $handler = $this->getCommandHandler(); + echo "YellowCore::command status:$statusCode handler:$handler time:$time ms<br/>\n"; + } + return $statusCode; + } + + // Handle startup + public function startup() { + $this->updateFileSystem(); //TODO: remove later, for backwards compatibility + $tokens = explode(",", $this->system->get("startupUpdate")); + foreach ($this->extensions->extensions as $key=>$value) { + if (method_exists($value["obj"], "onStartup")) $value["obj"]->onStartup(in_array($key, $tokens)); + } + if ($this->system->get("startupUpdate")!="none") { + $fileName = $this->system->get("settingDir").$this->system->get("systemFile"); + $this->system->save($fileName, array("startupUpdate" => "none")); + } + } + + // Handle shutdown + public function shutdown() { + foreach ($this->extensions->extensions as $key=>$value) { + if (method_exists($value["obj"], "onShutdown")) $value["obj"]->onShutdown(); + } + } + + // Update file system, TODO: remove later, for backwards compatibility + public function updateFileSystem() { + $fileData = $fileDataNew = $this->toolbox->readFile("yellow.php"); + $fileDataNew = str_replace("system/plugins/core.php", "system/extensions/core.php", $fileData); + if (is_dir("system/config/") || is_dir("system/themes/") || is_dir("system/plugins/") || $fileData!=$fileDataNew) { + $fileNameError = "system/settings/system-error.log"; + if (is_dir("system/config/")) { + foreach ($this->toolbox->getDirectoryEntriesRecursive("system/config/", "/.*/", true, false) as $entry) { + $entryNew = str_replace("system/config/", "system/settings/", $entry); + $entryNew = str_replace("config.ini", "system.ini", $entryNew); + if (!is_file($entryNew)) $this->toolbox->copyFile($entry, $entryNew, true); + } + if (!$this->toolbox->deleteDirectory("system/config/", "system/trash/")) { + $fileDataError .= "ERROR deleting folder 'system/config/'!\n"; + } + } + if (is_dir("system/themes/")) { + $pathTemplates = "system/themes/templates/"; + foreach ($this->toolbox->getDirectoryEntriesRecursive($pathTemplates, "/.*/", true, false) as $entry) { + $entryNew = str_replace($pathTemplates, "system/layouts/", $entry); + if (!is_file($entryNew)) $this->toolbox->copyFile($entry, $entryNew, true); + } + $pathSnippets = "system/themes/snippets/"; + foreach ($this->toolbox->getDirectoryEntriesRecursive($pathSnippets, "/.*/", true, false) as $entry) { + $entryNew = str_replace($pathSnippets, "system/layouts/", $entry); + $entryNew = str_replace(".php", ".html", $entryNew); + if (!is_file($entryNew)) $this->toolbox->copyFile($entry, $entryNew, true); + } + $pathAssets = "system/themes/assets/"; + foreach ($this->toolbox->getDirectoryEntriesRecursive($pathAssets, "/.*/", true, false) as $entry) { + if (preg_match("/\.php$/", $entry)) { + $entryNew = str_replace($pathAssets, "system/extensions/", $entry); + } else { + $entryNew = str_replace($pathAssets, "system/resources/", $entry); + } + if (!is_file($entryNew)) $this->toolbox->copyFile($entry, $entryNew, true); + } + if (!$this->toolbox->deleteDirectory("system/themes/", "system/trash/")) { + $fileDataError .= "ERROR deleting folder 'system/themes/'!\n"; + } + } + if (is_dir("system/plugins/")) { + foreach ($this->toolbox->getDirectoryEntriesRecursive("system/plugins/", "/.*/", true, false) as $entry) { + $entryNew = str_replace("system/plugins/", "system/extensions/", $entry); + if (!is_file($entryNew)) $this->toolbox->copyFile($entry, $entryNew, true); + } + if (!$this->toolbox->deleteDirectory("system/plugins/", "system/trash/")) { + $fileDataError .= "ERROR deleting folder 'system/plugins/'!\n"; + } + } + if (function_exists("opcache_reset")) opcache_reset(); + if ($fileData!=$fileDataNew && !$this->toolbox->createFile("yellow.php", $fileDataNew)) { + $fileDataError .= "ERROR writing file 'yellow.php'!\n"; + } + foreach ($this->toolbox->getDirectoryEntries("system/extensions/", "/^.*\.php$/", true, false) as $entry) { + $fileData = $fileDataNew = $this->toolbox->readFile($entry); + $fileDataNew = str_replace("class YellowTheme", "class Yellow", $fileData); + if (preg_match("/^core\.php$/", basename($entry))) continue; + if ($fileData!=$fileDataNew) $this->toolbox->createFile($entry, $fileDataNew); + } + $this->system->save("system/settings/system.ini", array("startupUpdate" => "update")); + if (!empty($fileDataError)) $this->toolbox->createFile($fileNameError, $fileDataError); + @header($this->toolbox->getHttpStatusFormatted(empty($fileDataError) ? 200 : 500)); + die(empty($fileDataError) ? "System folder has been updated. Please try again.\n" : + "System folder has not been updated. See errors in file '$fileNameError'!\n"); + } + } + + // Include layout + public function layout($name, $args = null) { + $this->lookup->layoutArgs = func_get_args(); + $this->page->includePageLayout($name); + } + + public function snippet($name, $args = null) { //TODO: remove later, for backwards compatibility + $this->layout($name, $args); + } + + // Return layout arguments + public function getLayoutArgs() { + return $this->lookup->layoutArgs; + } + + public function getSnippetArgs() { //TODO: remove later, for backwards compatibility + return $this->getLayoutArgs(); + } + + // Return request information + public function getRequestInformation($scheme = "", $address = "", $base = "") { + if (empty($scheme) && empty($address) && empty($base)) { + $url = $this->system->get("serverUrl"); + if (empty($url) || $this->isCommandLine()) $url = $this->toolbox->getServerUrl(); + list($scheme, $address, $base) = $this->lookup->getUrlInformation($url); + $this->system->set("serverScheme", $scheme); + $this->system->set("serverAddress", $address); + $this->system->set("serverBase", $base); + if (defined("DEBUG") && DEBUG>=3) echo "YellowCore::getRequestInformation $scheme://$address$base<br/>\n"; + } + $location = substru($this->toolbox->getLocation(), 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 request location + public function getRequestLocationArgsClean() { + return $this->toolbox->getLocationArgsClean($this->system->get("contentPagination")); + } + + // Return request language + public function getRequestLanguage() { + return $this->toolbox->detectBrowserLanguage($this->content->getLanguages(), $this->system->get("language")); + } + + // 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 !empty($this->lookup->commandHandler); + } +} + +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 $safeMode; //page is parsed in safe mode? (boolean) + 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 + + 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; + } + + // Parse page data + public function parseData($rawData, $cacheable, $statusCode, $pageError = "") { + $this->rawData = $rawData; + $this->parser = null; + $this->parserData = ""; + $this->safeMode = intval($this->yellow->system->get("safeMode")); + $this->available = $this->yellow->lookup->isAvailableLocation($this->location, $this->fileName); + $this->visible = $this->yellow->lookup->isVisibleLocation($this->location, $this->fileName); + $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location); + $this->cacheable = $cacheable; + $this->lastModified = 0; + $this->statusCode = $statusCode; + $this->parseMeta($pageError); + } + + // Parse page data update + public function parseDataUpdate() { + if ($this->statusCode==0) { + $this->rawData = $this->yellow->toolbox->readFile($this->fileName); + $this->statusCode = 200; + $this->parseMeta(); + } + } + + // 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", "navigation", "header", "footer", "sidebar")); + $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")=="hidden") $this->available = false; + $this->set("pageRead", $this->yellow->lookup->normaliseUrl( + $this->yellow->system->get("serverScheme"), + $this->yellow->system->get("serverAddress"), + $this->yellow->system->get("serverBase"), + $this->location)); + $this->set("pageEdit", $this->yellow->lookup->normaliseUrl( + $this->yellow->system->get("serverScheme"), + $this->yellow->system->get("serverAddress"), + $this->yellow->system->get("serverBase"), + 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("mediaDir"))); + $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); + } + } + + // 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) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (!empty($matches[1]) && !strempty($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]); + } + } + + // Parse page content on demand + public function parseContent($sizeMax = 0) { + if (!is_object($this->parser)) { + if ($this->yellow->extensions->isExisting($this->get("parser"))) { + $extension = $this->yellow->extensions->extensions[$this->get("parser")]; + if (method_exists($extension["obj"], "onParseContentRaw")) { + $this->parser = $extension["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); + 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")) { + $this->set("description", $this->yellow->toolbox->createTextDescription($this->parserData, 150)); + } + if (!$this->isExisting("keywords")) { + $this->set("keywords", $this->yellow->toolbox->createTextKeywords($this->get("title"), 10)); + } + if (defined("DEBUG") && DEBUG>=3) echo "YellowPage::parseContent location:".$this->location."<br/>\n"; + } + } + + // 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; + } else if (method_exists($value["obj"], "onParseContentBlock")) { //TODO: remove later, for backwards compatibility + $output = $value["obj"]->onParseContentBlock($this, $name, $text, true); + if (!is_null($output)) break; + } + } + if (is_null($output)) { + if ($name=="yellow" && $type=="inline") { + $output = "Datenstrom Yellow ".YellowCore::VERSION; + if ($text=="error") $output = $this->get("pageError"); + if ($text=="about") { + $output = "<span class=\"".htmlspecialchars($name)."\">\n"; + $serverVersion = $this->yellow->toolbox->getServerVersion(); + $output .= "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion<br />\n"; + foreach ($this->yellow->extensions->getData() as $key=>$value) { + $output .= htmlspecialchars(ucfirst($key)." $value")."<br />\n"; + } + $output .= "</span>\n"; + if ($this->safeMode) $this->error(500, "Yellow '$text' is not available in safe mode!"); + } + } + } + if (defined("DEBUG") && DEBUG>=3 && !empty($name)) echo "YellowPage::parseContentShortcut name:$name type:$type<br/>\n"; + return $output; + } + + // Parse page + public function parsePage() { + $this->parsePageLayout($this->get("layout")); + if (!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, must-revalidate"); + 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("resourceDir").$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->yellow->toolbox->isRequestSelf()) { + $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; + } + } + } + + // Parse page layout + public function parsePageLayout($name) { + $this->outputData = null; + if (!$this->isError()) { + foreach ($this->yellow->extensions->extensions as $key=>$value) { + if (method_exists($value["obj"], "onParsePageLayout")) { + $value["obj"]->onParsePageLayout($this, $name); + } elseif (method_exists($value["obj"], "onParsePageTemplate")) { //TODO: remove later, for backwards compatibility + $value["obj"]->onParsePageTemplate($this, $name); + } elseif (method_exists($value["obj"], "onParsePage")) { //TODO: remove later, for backwards compatibility + $value["obj"]->onParsePage(); + } + } + } + if (is_null($this->outputData)) { + ob_start(); + $this->includePageLayout($name); + $this->outputData = ob_get_contents(); + ob_end_clean(); + } + } + + // Include page layout + public function includePageLayout($name) { + $fileNameLayoutBasic = $this->yellow->system->get("layoutDir").$this->yellow->lookup->normaliseName($name).".html"; + $fileNameLayoutTheme = $this->yellow->system->get("layoutDir"). + $this->yellow->lookup->normaliseName($name)."-".$this->yellow->lookup->normaliseName($this->get("theme")).".html"; + if (is_file($fileNameLayoutTheme)) { + $this->setLastModified(filemtime($fileNameLayoutTheme)); + global $yellow; //TODO: remove later, for backwards compatibility + require($fileNameLayoutTheme); + } elseif (is_file($fileNameLayoutBasic)) { + $this->setLastModified(filemtime($fileNameLayoutBasic)); + global $yellow; //TODO: remove later, for backwards compatibility + require($fileNameLayoutBasic); + } else { + $this->error(500, "Layout '$name' does not exist!"); + echo "Layout error<br/>\n"; + } + } + + // 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 format + public function getDate($key, $format = "") { + if (!empty($format)) { + $format = $this->yellow->text->get($format); + } else { + $format = $this->yellow->text->get("dateFormatMedium"); + } + return $this->yellow->text->getDateFormatted(strtotime($this->get($key)), $format); + } + + // Return page setting as language specific date format, HTML encoded + public function getDateHtml($key, $format = "") { + return htmlspecialchars($this->getDate($key, $format)); + } + + // Return page setting as language specific date format, relative to today + public function getDateRelative($key, $format = "", $daysLimit = 0) { + if (!empty($format)) { + $format = $this->yellow->text->get($format); + } else { + $format = $this->yellow->text->get("dateFormatMedium"); + } + return $this->yellow->text->getDateRelative(strtotime($this->get($key)), $format, $daysLimit); + } + + // Return page setting as language specific date format, relative to today, HTML encoded + public function getDateRelativeHtml($key, $format = "", $daysLimit = 0) { + return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit)); + } + + // Return page setting as custom date format + public function getDateFormatted($key, $format) { + return $this->yellow->text->getDateFormatted(strtotime($this->get($key)), $format); + } + + // Return page setting as custom date format, HTML encoded + public function getDateFormattedHtml($key, $format) { + return htmlspecialchars($this->getDateFormatted($key, $format)); + } + + // 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 $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 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 sub pages + 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($pages) { + $this->pageCollection = $pages; + } + + // Return page collection with additional pages + public function getPages() { + return $this->pageCollection; + } + + // Set related page + public function setPage($key, $page) { + $this->pageRelations[$key] = $page; + } + + // Return related page + public function getPage($key) { + return !is_null($this->pageRelations[$key]) ? $this->pageRelations[$key] : $this; + } + + // 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; + } + + // Return page URL + public function getUrl() { + return $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, $this->base, $this->location); + } + + // 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; + } elseif (method_exists($value["obj"], "onExtra")) { //TODO: remove later, for backwards compatibility + $outputExtension = $value["obj"]->onExtra($name); + if (!is_null($outputExtension)) $output .= $outputExtension; + } + } + if ($name=="header") { + $fileNameTheme = $this->yellow->system->get("resourceDir").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; + if (is_file($fileNameTheme)) { + $locationTheme = $this->yellow->system->get("serverBase"). + $this->yellow->system->get("resourceLocation").$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("resourceDir").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; + if (is_file($fileNameScript)) { + $locationScript = $this->yellow->system->get("serverBase"). + $this->yellow->system->get("resourceLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; + $output .= "<script type=\"text/javascript\" src=\"$locationScript\"></script>\n"; + } + $fileNameIcon = $this->yellow->system->get("resourceDir").$this->yellow->lookup->normaliseName($this->get("theme"))."-icon.png"; + if (is_file($fileNameIcon)) { + $locationIcon = $this->yellow->system->get("serverBase"). + $this->yellow->system->get("resourceLocation").$this->yellow->lookup->normaliseName($this->get("theme"))."-icon.png"; + $contentType = $this->yellow->toolbox->getMimeContentType($locationIcon); + $output .= "<link rel=\"icon\" type=\"$contentType\" href=\"$locationIcon\" />\n"; + } + } + return $output; + } + + // Set page response output + public function setOutput($output) { + $this->outputData = $output; + } + + // 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] : ""; + } + + // 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->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; + } + + // 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 $statusCode; + } + + // 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); + } + } + + // 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, must-revalidate"); + } + $this->set("pageClean", (string)$statusCode); + } + } + + // 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 response header exists + public function isHeader($key) { + return !is_null($this->headerData[$key]); + } + + // Check if page setting exists + public function isExisting($key) { + return !is_null($this->metaData[$key]); + } + + // Check if related page exists + public function isPage($key) { + return !is_null($this->pageRelations[$key]); + } +} + +class YellowDataCollection extends ArrayObject { + public function __construct() { + parent::__construct(array()); + } + + // Return array element + public function offsetGet($key) { + if (is_string($key)) $key = lcfirst($key); + return parent::offsetGet($key); + } + + // Set array element + public function offsetSet($key, $value) { + if (is_string($key)) $key = lcfirst($key); + parent::offsetSet($key, $value); + } + + // Remove array element + public function offsetUnset($key) { + if (is_string($key)) $key = lcfirst($key); + parent::offsetUnset($key); + } + + // Check if array element exists + public function offsetExists($key) { + if (is_string($key)) $key = lcfirst($key); + return parent::offsetExists($key); + } +} + +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; + } + + // 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; + } + } + } + } + $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(); + foreach ($array as $page) { + $page->set("sortindex", ++$i); + } + $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; + } + + // Sort page collection by settings similarity + public function similar($page, $ascendingOrder = false) { + $location = $page->location; + $keywords = $this->yellow->toolbox->createTextKeywords($page->get("title")); + $keywords .= ",".$page->get("tag").",".$page->get("author"); + $tokens = array_unique(array_filter(preg_split("/\s*,\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); + } + return $this; + } + + // Merge page collection + public function merge($input) { + $this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input)); + return $this; + } + + // Append to end of page collection + public function append($page) { + parent::append($page); + return $this; + } + + // Prepend to start of page collection + public function prepend($page) { + $array = $this->getArrayCopy(); + array_unshift($array, $page); + $this->exchangeArray($array); + return $this; + } + + // Limit the number of pages in page collection + public function limit($pagesMax) { + $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax)); + return $this; + } + + // Reverse page collection + public function reverse() { + $this->exchangeArray(array_reverse($this->getArrayCopy())); + return $this; + } + + // Randomize page collection + public function shuffle() { + $array = $this->getArrayCopy(); + shuffle($array); + $this->exchangeArray($array); + return $this; + } + + // Paginate page collection + public function pagination($limit, $reverse = true) { + $this->paginationNumber = 1; + $this->paginationCount = ceil($this->count() / $limit); + $pagination = $this->yellow->system->get("contentPagination"); + if (isset($_REQUEST[$pagination])) $this->paginationNumber = intval($_REQUEST[$pagination]); + 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 $this; + } + + // Return current page number in pagination + public function getPaginationNumber() { + return $this->paginationNumber; + } + + // Return highest page number in pagination + public function getPaginationCount() { + return $this->paginationCount; + } + + // Return location for a page in pagination + public function getPaginationLocation($absoluteLocation = true, $pageNumber = 1) { + if ($pageNumber>=1 && $pageNumber<=$this->paginationCount) { + $pagination = $this->yellow->system->get("contentPagination"); + $location = $this->yellow->page->getLocation($absoluteLocation); + $locationArgs = $this->yellow->toolbox->getLocationArgsNew( + $pageNumber>1 ? "$pagination:$pageNumber" : "$pagination:", $pagination); + } + return $location.$locationArgs; + } + + // Return location for previous page in pagination + public function getPaginationPrevious($absoluteLocation = true) { + $pageNumber = $this->paginationNumber-1; + return $this->getPaginationLocation($absoluteLocation, $pageNumber); + } + + // Return location for next page in pagination + public function getPaginationNext($absoluteLocation = true) { + $pageNumber = $this->paginationNumber+1; + return $this->getPaginationLocation($absoluteLocation, $pageNumber); + } + + // 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 $pageNumber; + } + + // Return page in collection, null if none + public function getPage($pageNumber = 1) { + return ($pageNumber>=1 && $pageNumber<=$this->count()) ? $this->offsetGet($pageNumber-1) : null; + } + + // 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 current page filter + public function getFilter() { + return $this->filterValue; + } + + // 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 there is a pagination + public function isPagination() { + return $this->paginationCount>1; + } +} + +class YellowContent { + public $yellow; //access to API + public $pages; //scanned pages + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->pages = array(); + } + + // Scan file system on demand + public function scanLocation($location) { + if (is_null($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) = explode(" ", $rootLocation, 2); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $rootLocation, $fileName); + $page->parseData("", false, 0); + array_push($this->pages[$location], $page); + } + } 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); + } + } + } + return $this->pages[$location]; + } + + // Return page from file system, null if not found + public function find($location, $absoluteLocation = 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; + } + + // 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 page collection with top-level navigation + public function top($showInvisible = false) { + $rootLocation = $this->getRootLocation($this->yellow->page->location); + return $this->getChildren($rootLocation, $showInvisible); + } + + // 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); + } + return $pages; + } + + // Return page with shared content, null if not found + public function shared($location, $absoluteLocation = false, $name = "shared") { + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + $locationShared = $this->yellow->lookup->getDirectoryLocation($location); + $page = $this->find($locationShared.$name); + if ($page==null) { + $locationShared = $this->getHomeLocation($location).$this->yellow->system->get("contentSharedDir"); + $page = $this->find($locationShared.$name); + } + return $page; + } + + // 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); + } + } + } + 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(); + foreach ($this->scanLocation("") as $page) { + if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) array_push($languages, $page->get("language")); + } + return $languages; + } + + // 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; + } + + // 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 root location + public function getRootLocation($location) { + $rootLocation = "root/"; + if ($this->yellow->system->get("multiLanguageMode")) { + 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 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 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; + } +} + +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 (is_null($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("serverBase"); + if (empty($location)) { + $fileNames = array($this->yellow->system->get("mediaDir")); + } 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); + } + } + 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); + } + } + return $this->files[$location]; + } + + // Return page with media file information, null if not found + public function find($location, $absoluteLocation = false) { + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->system->get("serverBase"))); + 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 collection with all media files + public function index($showInvisible = false, $multiPass = false, $levelMax = 0) { + return $this->getChildrenRecursive("", $showInvisible, $levelMax); + } + + // Return page collection that's empty + public function clean() { + return new YellowPageCollection($this->yellow); + } + + // 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 $files; + } + + // 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; + } + + // Return home location + public function getHomeLocation($location) { + return $this->yellow->system->get("mediaLocation"); + } + + // Return parent location + public function getParentLocation($location) { + $token = rtrim($this->yellow->system->get("mediaLocation"), "/"); + if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { + if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; + } + if (empty($parentLocation)) $parentLocation = ""; + return $parentLocation; + } + + // Return top-level location + public function getParentTopLocation($location) { + $token = rtrim($this->yellow->system->get("mediaLocation"), "/"); + if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; + if (empty($parentTopLocation)) $parentTopLocation = "$token/"; + return $parentTopLocation; + } +} + +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(); + } + + // 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; + 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"; + } + } + } + + // 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); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (!empty($matches[1]) && !is_null($settingsNew[$matches[1]])) { + $fileDataNew .= "$matches[1]: ".$settingsNew[$matches[1]]."\n"; + unset($settingsNew[$matches[1]]); + } else { + $fileDataNew .= $line; + } + } + foreach ($settingsNew as $key=>$value) { + $fileDataNew .= ucfirst($key).": $value\n"; + } + return $this->yellow->toolbox->createFile($fileName, $fileDataNew); + } + + // Set default system setting + public function setDefault($key, $value) { + $this->settingsDefaults[$key] = $value; + } + + // Set system setting + public function set($key, $value) { + $this->settings[$key] = $value; + } + + // Return system setting + public function get($key) { + if (!is_null($this->settings[$key])) { + $value = $this->settings[$key]; + } else { + $value = !is_null($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : ""; + } + return $value; + } + + // Return system setting, HTML encoded + public function getHtml($key) { + return htmlspecialchars($this->get($key)); + } + + // 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; + } + + // 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 setting exists + public function isExisting($key) { + return !is_null($this->settings[$key]); + } +} + +class YellowText { + public $yellow; //access to API + public $modified; //text modification date + public $text; //text + public $language; //current language + + 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; + 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 (is_null($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 text setting for specific language + public function getText($key, $language) { + return $this->isExisting($key, $language) ? $this->text[$language][$key] : "[$key]"; + } + + // Return text setting for specific language, HTML encoded + public function getTextHtml($key, $language) { + return htmlspecialchars($this->getText($key, $language)); + } + + // 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 $text; + } + + // Return human readable date, custom date format + public function getDateFormatted($timestamp, $format) { + $dateMonths = preg_split("/\s*,\s*/", $this->get("dateMonths")); + $dateWeekdays = preg_split("/\s*,\s*/", $this->get("dateWeekdays")); + $month = $dateMonths[date("n", $timestamp) - 1]; + $weekday = $dateWeekdays[date("N", $timestamp) - 1]; + $timeZone = $this->yellow->system->get("timezone"); + $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); + } + + // Return human readable date, relative to today + public function getDateRelative($timestamp, $format, $daysLimit) { + $timeDifference = time() - $timestamp; + $days = abs(intval($timeDifference / 86400)); + if ($days<=$daysLimit || $daysLimit==0) { + $tokens = preg_split("/\s*,\s*/", $this->get($timeDifference>=0 ? "datePast" : "dateFuture")); + 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 = $this->getDateFormatted($timestamp, $format); + } + return $output; + } + + // Return languages + public function getLanguages() { + $languages = array(); + foreach ($this->text as $key=>$value) { + array_push($languages, $key); + } + return $languages; + } + + // 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; + } + + // Normalise date into known format + public function normaliseDate($text) { + if (preg_match("/^\d+\-\d+$/", $text)) { + $output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatShort")); + } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) { + $output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatMedium")); + } elseif (preg_match("/^\d+\-\d+\-\d+ \d+\:\d+$/", $text)) { + $output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatLong")); + } else { + $output = $text; + } + return $output; + } + + // Check if language exists + public function isLanguage($language) { + return !is_null($this->text[$language]); + } + + // Check if text setting exists + public function isExisting($key, $language = "") { + if (empty($language)) $language = $this->language; + return !is_null($this->text[$language]) && !is_null($this->text[$language][$key]); + } +} + +class YellowLookup { + public $yellow; //access to API + public $requestHandler; //request handler name + public $commandHandler; //command handler name + public $layoutArgs; //layout arguments + + public function __construct($yellow) { + $this->yellow = $yellow; + } + + // Detect file system + public function detectFileSystem() { + list($pathRoot, $pathHome) = $this->findFileSystemInformation(); + $this->yellow->system->set("contentRootDir", $pathRoot); + $this->yellow->system->set("contentHomeDir", $pathHome); + date_default_timezone_set($this->yellow->system->get("timezone")); + } + + // Return file system information + public function findFileSystemInformation() { + $path = $this->yellow->system->get("contentDir"); + $pathRoot = $this->yellow->system->get("contentRootDir"); + $pathHome = $this->yellow->system->get("contentHomeDir"); + if (!$this->yellow->system->get("multiLanguageMode")) $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; + } + } + $pathHome = $this->normaliseToken($token)."/"; + } + return array($pathRoot, $pathHome); + } + + // Return root locations + public function findRootLocations($includePath = true) { + $locations = array(); + $pathBase = $this->yellow->system->get("contentDir"); + $pathRoot = $this->yellow->system->get("contentRootDir"); + 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"; + } + } else { + array_push($locations, $includePath ? "root/ $pathBase" : "root/"); + } + return $locations; + } + + // Return location from file path + public function findLocationFromFile($fileName) { + $location = "/"; + $pathBase = $this->yellow->system->get("contentDir"); + $pathRoot = $this->yellow->system->get("contentRootDir"); + $pathHome = $this->yellow->system->get("contentHomeDir"); + $fileDefault = $this->yellow->system->get("contentDefaultFile"); + $fileExtension = $this->yellow->system->get("contentExtension"); + if (substru($fileName, 0, strlenu($pathBase))==$pathBase) { + $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); + } + $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"; + } + return $invalid ? "" : $location; + } + + // Return file path from location + public function findFileFromLocation($location, $directory = false) { + $path = $this->yellow->system->get("contentDir"); + $pathRoot = $this->yellow->system->get("contentRootDir"); + $pathHome = $this->yellow->system->get("contentHomeDir"); + $fileDefault = $this->yellow->system->get("contentDefaultFile"); + $fileExtension = $this->yellow->system->get("contentExtension"); + $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"; + } + } + } + return $invalid ? "" : $path; + } + + // 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; + } + } + } + if ($directory) $token .= "/"; + return ($default || $found) ? $token : ""; + } + + // 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; + } + } + } + return $includePath ? "$path/$token" : $token; + } + + // Return children from location + public function findChildrenFromLocation($location) { + $fileNames = array(); + $fileDefault = $this->yellow->system->get("contentDefaultFile"); + $fileExtension = $this->yellow->system->get("contentExtension"); + 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); + } + } + } + return $fileNames; + } + + // Return language from file path + public function findLanguageFromFile($fileName, $languageDefault) { + $language = $languageDefault; + $pathBase = $this->yellow->system->get("contentDir"); + $pathRoot = $this->yellow->system->get("contentRootDir"); + 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; + } + + // Return file path from media location + public function findFileFromMedia($location) { + if ($this->isFileLocation($location)) { + $mediaLocationLength = strlenu($this->yellow->system->get("mediaLocation")); + if (substru($location, 0, $mediaLocationLength)==$this->yellow->system->get("mediaLocation")) { + $fileName = $this->yellow->system->get("mediaDir").substru($location, 7); + } + } + return $fileName; + } + + // Return file path from system location + public function findFileFromSystem($location) { + if (preg_match("/\.(css|gif|ico|js|jpg|png|svg|txt|woff|woff2)$/", $location)) { + $extensionLocationLength = strlenu($this->yellow->system->get("extensionLocation")); + $resourceLocationLength = strlenu($this->yellow->system->get("resourceLocation")); + if (substru($location, 0, $extensionLocationLength)==$this->yellow->system->get("extensionLocation")) { + $fileName = $this->yellow->system->get("extensionDir").substru($location, $extensionLocationLength); + } elseif (substru($location, 0, $resourceLocationLength)==$this->yellow->system->get("resourceLocation")) { + $fileName = $this->yellow->system->get("resourceDir").substru($location, $resourceLocationLength); + } + } + return $fileName; + } + + // Return file path from cache if possible + public function findFileFromCache($location, $fileName, $cacheable) { + if ($cacheable) { + $location .= $this->yellow->toolbox->getLocationArgs(); + $fileNameStatic = rtrim($this->yellow->system->get("cacheDir"), "/").$location; + if (!$this->isFileLocation($location)) $fileNameStatic .= $this->yellow->system->get("staticDefaultFile"); + if (is_readable($fileNameStatic)) $fileName = $fileNameStatic; + } + return $fileName; + } + + // 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); + } + + // 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); + } + + // Normalise prefix + public function normalisePrefix($text) { + if (preg_match("/^([\d\-\_\.]*)(.*)$/", $text, $matches)) $prefix = $matches[1]; + if (!empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-"; + return $prefix; + } + + // 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 $array; + } + + // 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("serverBase").$this->yellow->system->get("mediaLocation"); + if (preg_match("/^\#/", $location)) { + $location = $pageBase.$pageLocation.$location; + } elseif (!preg_match("/^\//", $location)) { + $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; + } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) { + $location = $pageBase.$location; + } + $location = strreplaceu("/./", "/", $location); + $location = strreplaceu(":", $this->yellow->toolbox->getLocationArgsSeparator(), $location); + } else { + if ($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter"; + } + return $location; + } + + // 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 && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter"; + $url = $location; + } + return $url; + } + + // Return URL information + public function getUrlInformation($url) { + if (preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, "/"), $matches)) { + $scheme = $matches[1]; + $address = $matches[2]; + $base = $matches[3]; + } + return array($scheme, $address, $base); + } + + // Return directory location + public function getDirectoryLocation($location) { + return ($pos = strrposu($location, "/")) ? substru($location, 0, $pos+1) : "/"; + } + + // Check if location is specifying root + public function isRootLocation($location) { + return $location[0]!="/"; + } + + // Check if location is specifying file or directory + public function isFileLocation($location) { + return substru($location, -1, 1)!="/"; + } + + // 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("multiLanguageMode"); + } + return $redirect; + } + + // 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; + } + return $nested; + } + + // Check if location is available + public function isAvailableLocation($location, $fileName) { + $available = true; + $pathBase = $this->yellow->system->get("contentDir"); + if (substru($fileName, 0, strlenu($pathBase))==$pathBase) { + $sharedLocation = $this->yellow->content->getHomeLocation($location).$this->yellow->system->get("contentSharedDir"); + if (substru($location, 0, strlenu($sharedLocation))==$sharedLocation) $available = false; + } + return $available; + } + + // Check if location is visible + public function isVisibleLocation($location, $fileName) { + $visible = true; + $pathBase = $this->yellow->system->get("contentDir"); + if (substru($fileName, 0, strlenu($pathBase))==$pathBase) { + $fileName = substru($fileName, strlenu($pathBase)); + $tokens = explode("/", $fileName); + for ($i=0; $i<count($tokens)-1; ++$i) { + if (!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i])) { + $visible = false; + break; + } + } + } else { + $visible = false; + } + return $visible; + } + + // 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; + } + } + return $active; + } + + // Check if file is valid + public function isValidFile($fileName) { + $contentDirLength = strlenu($this->yellow->system->get("contentDir")); + $mediaDirLength = strlenu($this->yellow->system->get("mediaDir")); + $systemDirLength = strlenu($this->yellow->system->get("systemDir")); + return substru($fileName, 0, $contentDirLength)==$this->yellow->system->get("contentDir") || + substru($fileName, 0, $mediaDirLength)==$this->yellow->system->get("mediaDir") || + substru($fileName, 0, $systemDirLength)==$this->yellow->system->get("systemDir"); + } + + // Check if content file + public function isContentFile($fileName) { + $contentDirLength = strlenu($this->yellow->system->get("contentDir")); + return substru($fileName, 0, $contentDirLength)==$this->yellow->system->get("contentDir"); + } + + // Check if media file + public function isMediaFile($fileName) { + $mediaDirLength = strlenu($this->yellow->system->get("mediaDir")); + return substru($fileName, 0, $mediaDirLength)==$this->yellow->system->get("mediaDir"); + } + + // Check if system file + public function isSystemFile($fileName) { + $systemDirLength = strlenu($this->yellow->system->get("systemDir")); + return substru($fileName, 0, $systemDirLength)==$this->yellow->system->get("systemDir"); + } +} + +class YellowToolbox { + + // Return server version from current HTTP request + public function getServerVersion($shortFormat = false) { + $serverVersion = strtoupperu(PHP_SAPI)." ".PHP_OS; + if (preg_match("/^(\S+)/", $_SERVER["SERVER_SOFTWARE"], $matches)) $serverVersion = $matches[1]." ".PHP_OS; + if ($shortFormat && preg_match("/^(\pL+)/u", $serverVersion, $matches)) $serverVersion = $matches[1]; + return $serverVersion; + } + + // Return server URL from current HTTP request + public function getServerUrl() { + $scheme = $this->getScheme(); + $address = $this->getAddress(); + $base = $this->getBase(); + return "$scheme://$address$base/"; + } + + // Return scheme from current HTTP request + public function getScheme() { + $scheme = ""; + if (preg_match("/^HTTP\//", $_SERVER["SERVER_PROTOCOL"])) { + $secure = isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"]!="off"; + $scheme = $secure ? "https" : "http"; + } + return $scheme; + } + + // Return address from current HTTP request + public function getAddress() { + $address = $_SERVER["SERVER_NAME"]; + $port = $_SERVER["SERVER_PORT"]; + if ($port!=80 && $port!=443) $address .= ":$port"; + return $address; + } + + // Return base from current HTTP request + public function getBase() { + $base = ""; + if (preg_match("/^(.*)\/.*\.php$/", $_SERVER["SCRIPT_NAME"], $matches)) $base = $matches[1]; + return $base; + } + + // Return location from current HTTP request + public function getLocation($filterStrict = true) { + $location = $_SERVER["REQUEST_URI"]; + $location = rawurldecode(($pos = strposu($location, "?")) ? substru($location, 0, $pos) : $location); + if ($filterStrict) { + $location = $this->normaliseTokens($location, true); + $separator = $this->getLocationArgsSeparator(); + if (preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches)) { + $_SERVER["LOCATION"] = $location = $matches[1]; + $_SERVER["LOCATION_ARGS"] = $matches[2]; + foreach (explode("/", $matches[2]) as $token) { + 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_ARGS"] = ""; + } + } + return $location; + } + + // Return location arguments from current HTTP request + public function getLocationArgs() { + return $_SERVER["LOCATION_ARGS"]; + } + + // Return location arguments from current HTTP request, modify existing arguments + public function getLocationArgsNew($arg, $pagination) { + $separator = $this->getLocationArgsSeparator(); + preg_match("/^(.*?):(.*)$/", $arg, $args); + foreach (explode("/", $_SERVER["LOCATION_ARGS"]) as $token) { + preg_match("/^(.*?)$separator(.*)$/", $token, $matches); + if ($matches[1]==$args[1]) { + $matches[2] = $args[2]; + $found = true; + } + if (!empty($matches[1]) && !strempty($matches[2])) { + if (!empty($locationArgs)) $locationArgs .= "/"; + $locationArgs .= "$matches[1]:$matches[2]"; + } + } + if (!$found && !empty($args[1]) && !strempty($args[2])) { + if (!empty($locationArgs)) $locationArgs .= "/"; + $locationArgs .= "$args[1]:$args[2]"; + } + if (!empty($locationArgs)) { + $locationArgs = $this->normaliseArgs($locationArgs, false, false); + if (!$this->isLocationArgsPagination($locationArgs, $pagination)) $locationArgs .= "/"; + } + return $locationArgs; + } + + // Return location arguments from current HTTP request, convert form parameters + public function getLocationArgsClean($pagination) { + foreach (array_merge($_GET, $_POST) as $key=>$value) { + if (!empty($key) && !strempty($value)) { + if (!empty($locationArgs)) $locationArgs .= "/"; + $key = strreplaceu(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $key); + $value = strreplaceu(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $value); + $locationArgs .= "$key:$value"; + } + } + if (!empty($locationArgs)) { + $locationArgs = $this->normaliseArgs($locationArgs, false, false); + if (!$this->isLocationArgsPagination($locationArgs, $pagination)) $locationArgs .= "/"; + } + return $locationArgs; + } + + // Return location arguments separator + public function getLocationArgsSeparator() { + return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ":" : "="; + } + + // Check if there are location arguments in current HTTP request + public function isLocationArgs($location = "") { + $location = empty($location) ? $_SERVER["LOCATION"].$_SERVER["LOCATION_ARGS"] : $location; + $separator = $this->getLocationArgsSeparator(); + return preg_match("/[^\/]+$separator.*$/", $location); + } + + // Check if there are pagination arguments in current HTTP request + public function isLocationArgsPagination($location, $pagination) { + $separator = $this->getLocationArgsSeparator(); + return preg_match("/^(.*\/)?$pagination$separator.*$/", $location); + } + + // Check if script location is requested + public function isRequestSelf() { + return substru($_SERVER["REQUEST_URI"], -10, 10)=="yellow.php"; + } + + // Check if clean URL is requested + public function isRequestCleanUrl($location) { + return (isset($_GET["clean-url"]) || isset($_POST["clean-url"])) && substru($location, -1, 1)=="/"; + } + + // Check if unmodified since last HTTP request + public function isRequestNotModified($lastModifiedFormatted) { + return isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && $_SERVER["HTTP_IF_MODIFIED_SINCE"]==$lastModifiedFormatted; + } + + // Normalise path or location, take care of relative path tokens + public function normaliseTokens($text, $prependSlash = false) { + $textFiltered = ""; + if ($prependSlash && $text[0]!="/") $textFiltered .= "/"; + for ($pos=0; $pos<strlenb($text); ++$pos) { + if ($text[$pos]=="/" || $pos==0) { + 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; + } + } + } + $textFiltered .= $text[$pos]; + } + return $textFiltered; + } + + // Normalise location arguments + public function normaliseArgs($text, $appendSlash = true, $filterStrict = true) { + if ($appendSlash) $text .= "/"; + if ($filterStrict) $text = strreplaceu(" ", "-", strtoloweru($text)); + $text = strreplaceu(":", $this->getLocationArgsSeparator(), $text); + return strreplaceu(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text)); + } + + // 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 $text; + } + + // Return timezone + public function getTimezone() { + $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 human readable HTTP date + public function getHttpDateFormatted($timestamp) { + return gmdate("D, d M Y H:i:s", $timestamp)." GMT"; + } + + // 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"; + } + $serverProtocol = $_SERVER["SERVER_PROTOCOL"]; + if (!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1"; + return $shortFormat ? $text : "$serverProtocol $statusCode $text"; + } + + // 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]; + } + return $contentType; + } + + // Return file type + public function getFileType($fileName) { + return strtoloweru(($pos = strrposu($fileName, ".")) ? substru($fileName, $pos+1) : ""); + } + + // Return file group + public function getFileGroup($fileName, $path) { + preg_match("#^$path(.+?)\/#", $fileName, $matches); + return strtoloweru($matches[1]); + } + + // 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(); + $dirHandle = @opendir($path); + if ($dirHandle) { + $path = rtrim($path, "/"); + while (($entry = readdir($dirHandle))!==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); + } + } + } + if ($sort) natcasesort($entries); + closedir($dirHandle); + } + 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)); + } + } + return $entries; + } + + // Read file, empty string if not found + public function readFile($fileName, $sizeMax = 0) { + $fileData = ""; + $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); + } + fclose($fileHandle); + $ok = true; + } + return $ok; + } + + // 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 @copy($fileNameSource, $fileNameDestination); + } + + // 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 @rename($fileNameSource, $fileNameDestination); + } + + // Rename directory + public function renameDirectory($pathSource, $pathDestination, $mkdir = false) { + return $pathSource==$pathDestination || $this->renameFile($pathSource, $pathDestination, $mkdir); + } + + // 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); + } + 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->isDir()) { + @rmdir($file->getRealPath()); + } else { + @unlink($file->getRealPath()); + } + } + $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); + } + return $ok; + } + + // Set file modification date, Unix time + public function modifyFile($fileName, $modified) { + clearstatcache(true, $fileName); + return @touch($fileName, $modified); + } + + // Return file modification date, Unix time + public function getFileModified($fileName) { + return is_file($fileName) ? filemtime($fileName) : 0; + } + + // 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; + } + + // Return arguments from text string, space separated + public function getTextArgs($text, $optional = "-") { + $text = preg_replace("/\s+/s", " ", trim($text)); + $tokens = str_getcsv($text, " ", "\""); + foreach ($tokens as $key=>$value) { + if ($value==$optional) $tokens[$key] = ""; + } + return $tokens; + } + + // 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); + } + + // Create description from text string + public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") { + if (preg_match("/^<h1>.*?<\/h1>(.*)$/si", $text, $matches)) $text = $matches[1]; + if ($lengthMax==0) $lengthMax = strlenu($text); + if ($removeHtml) { + while (true) { + $elementFound = preg_match("/<\s*?([\/!]?\w*)(.*?)\s*?\>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $element = $matches[0][0]; + $elementName = $matches[1][0]; + $elementText = $matches[2][0]; + $elementOffsetBytes = $elementFound ? $matches[0][1] : strlenb($text); + $string = html_entity_decode(substrb($text, $offsetBytes, $elementOffsetBytes - $offsetBytes), ENT_QUOTES, "UTF-8"); + if (preg_match("/^(blockquote|br|div|h\d|hr|li|ol|p|pre|ul)/i", $elementName)) $string .= " "; + if (preg_match("/^\/(code|pre)/i", $elementName)) $string = preg_replace("/^(\d+\n){2,}$/", "", $string); + $string = preg_replace("/\s+/s", " ", $string); + if (substru($string, 0, 1)==" " && (empty($output) || substru($output, -1)==" ")) $string = substru($string, 1); + $length = strlenu($string); + $output .= substru($string, 0, $length<$lengthMax ? $length : $lengthMax-1); + $lengthMax -= $length; + if (!empty($element) && $element==$endMarker) { + $lengthMax = 0; + $endMarkerFound = true; + } + if ($lengthMax<=0 || !$elementFound) break; + $offsetBytes = $elementOffsetBytes + strlenb($element); + } + $output = rtrim($output); + if ($lengthMax<=0) $output .= $endMarkerFound ? $endMarkerText : "…"; + } else { + $elementsOpen = array(); + while (true) { + $elementFound = preg_match("/&.*?\;|<\s*?([\/!]?\w*)(.*?)\s*?\>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $element = $matches[0][0]; + $elementName = $matches[1][0]; + $elementText = $matches[2][0]; + $elementOffsetBytes = $elementFound ? $matches[0][1] : strlenb($text); + $string = substrb($text, $offsetBytes, $elementOffsetBytes - $offsetBytes); + $length = strlenu($string); + $output .= substru($string, 0, $length<$lengthMax ? $length : $lengthMax-1); + $lengthMax -= $length + ($element[0]=="&" ? 1 : 0); + if (!empty($element) && $element==$endMarker) { + $lengthMax = 0; + $endMarkerFound = true; + } + if ($lengthMax<=0 || !$elementFound) break; + if (!empty($elementName) && substru($elementText, -1)!="/" && + !preg_match("/^(area|br|col|hr|img|input|col|param|!)/i", $elementName)) { + if ($elementName[0]!="/") { + array_push($elementsOpen, $elementName); + } else { + array_pop($elementsOpen); + } + } + $output .= $element; + $offsetBytes = $elementOffsetBytes + strlenb($element); + } + $output = rtrim($output); + for ($i=count($elementsOpen)-1; $i>=0; --$i) { + if (!preg_match("/^(dl|ol|ul|table|tbody|thead|tfoot|tr)/i", $elementsOpen[$i])) break; + $output .= "</".$elementsOpen[$i].">"; + } + if ($lengthMax<=0) $output .= $endMarkerFound ? $endMarkerText : "…"; + for (; $i>=0; --$i) { + $output .= "</".$elementsOpen[$i].">"; + } + } + return $output; + } + + // Create keywords from text string + public function createTextKeywords($text, $keywordsMax = 0) { + $tokens = array_unique(preg_split("/[,\s\(\)\+\-]/", strtoloweru($text))); + foreach ($tokens as $key=>$value) { + if (strlenu($value)<3) unset($tokens[$key]); + } + if ($keywordsMax) $tokens = array_slice($tokens, 0, $keywordsMax); + return implode(", ", $tokens); + } + + // Create title from text string + public function createTextTitle($text) { + if (preg_match("/^.*\/([\w\-]+)/", $text, $matches)) $text = strreplaceu("-", " ", ucfirst($matches[1])); + return $text; + } + + // 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); + } + if (empty($dataBuffer) && function_exists("mcrypt_create_iv")) { + $dataBuffer = @mcrypt_create_iv($dataBufferSize, MCRYPT_DEV_URANDOM); + } + if (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 { + $salt = substrb(bin2hex($dataBuffer), 0, $length); + } + } + 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; + } + 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; + } + 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]; + } + } + return $ok; + } + + // 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) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (lcfirst($matches[1])==$key && !strempty($matches[2])) { + $value = $matches[2]; + break; + } + } + } + return $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)) { + $key = lcfirst($key); + foreach ($this->getTextLines($parts[2]) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (lcfirst($matches[1])==$key) { + $rawDataNew .= "$matches[1]: $value\n"; + $found = true; + } else { + $rawDataNew .= $line; + } + } + if (!$found) $rawDataNew .= ucfirst($key).": $value\n"; + $rawDataNew = $parts[1]."---\n".$rawDataNew."---\n".$parts[3]; + } else { + $rawDataNew = $rawData; + } + return $rawDataNew; + } + + // Detect web browser language + public function detectBrowserLanguage($languages, $languageDefault) { + $languageFound = $languageDefault; + if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) { + foreach (preg_split("/\s*,\s*/", $_SERVER["HTTP_ACCEPT_LANGUAGE"]) as $string) { + list($language) = explode(";", $string); + if (in_array($language, $languages)) { + $languageFound = $language; + break; + } + } + } + return $languageFound; + } + + // 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 (.*?)>/", $dataBuffer, $matches)) { + if (preg_match("/ width=\"(\d+)\"/", $matches[1], $tokens)) $width = $tokens[1]; + if (preg_match("/ height=\"(\d+)\"/", $matches[1], $tokens)) $height = $tokens[1]; + $type = $fileType; + } + } + fclose($fileHandle); + } + return array($width, $height, $type); + } + + // Start timer + public function timerStart(&$time) { + $time = microtime(true); + } + + // Stop timer and calculate elapsed time in milliseconds + public function timerStop(&$time) { + $time = intval((microtime(true)-$time) * 1000); + } +} + +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(); + } + + // 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"; + if (preg_match("/^core\.php$/", basename($entry)) && class_exists("YellowCore")) { //TODO: remove later, for backwards compatibility + $this->register("core", "YellowCore"); + continue; + } + $this->modified = max($this->modified, filemtime($entry)); + global $yellow; //TODO: remove later, for backwards compatibility + 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("pluginLocation", "/media/extensions/"); //TODO: remove later, for backwards compatibility + $this->yellow->system->set("themeLocation", "/media/extensions/"); + $this->yellow->system->set("assetLocation", "/media/resources/"); + $this->yellow->system->set("pluginDir", "system/extensions/"); + $this->yellow->system->set("themeDir", "system/extensions/"); + $this->yellow->system->set("assetDir", "system/resources/"); + $this->yellow->system->set("configDir", "system/settings/"); + } + + // 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 extension + public function get($name) { + return $this->extensions[$name]["obj"]; + } + + // 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 extensions modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; + } + + // Check if extension exists + public function isExisting($name) { + return !is_null($this->extensions[$name]); + } +} + +class YellowPages { //TODO: remove later, for backwards compatibility + public $yellow; + public function __construct($yellow) { $this->yellow = $yellow; } + public function find($location, $absoluteLocation = false) { return $this->yellow->content->find($location, $absoluteLocation); } + public function index($showInvisible = false, $multiLanguage = false, $levelMax = 0) { return $this->yellow->content->index($showInvisible, $multiLanguage, $levelMax); } + public function top($showInvisible = false) { return $this->yellow->content->top($showInvisible); } + public function path($location, $absoluteLocation = false) { return $this->yellow->content->path($location, $absoluteLocation); } + public function shared($location, $absoluteLocation = false, $name = "shared") { return $this->yellow->content->shared($location, $absoluteLocation, $name); } + public function multi($location, $absoluteLocation = false, $showInvisible = false) { return $this->yellow->content->multi($location, $absoluteLocation, $showInvisible); } + public function clean() { return $this->yellow->content->clean(); } + public function getHomeLocation($location) { return $this->yellow->content->getHomeLocation($location); } +} + +class YellowFiles { //TODO: remove later, for backwards compatibility + public $yellow; + public function __construct($yellow) { $this->yellow = $yellow; } + public function find($location, $absoluteLocation = false) { return $this->yellow->media->find($location, $absoluteLocation); } + public function index($showInvisible = false, $multiPass = false, $levelMax = 0) { return $this->yellow->media->index($showInvisible, $multiPass, $levelMax); } + public function clean() { return $this->yellow->media->clean(); } + public function getHomeLocation($location) { return $this->yellow->media->getHomeLocation($location); } +} + +class YellowConfig { //TODO: remove later, for backwards compatibility + public $yellow; + public function __construct($yellow) { $this->yellow = $yellow; } + public function load($fileName) { $this->yellow->system->load($fileName); } + public function save($fileName, $config) { return $this->yellow->system->save($fileName, $config); } + public function setDefault($key, $value) { $this->yellow->system->setDefault($key, $value); } + public function set($key, $value) { $this->yellow->system->set($key, $value); } + public function get($key) { return $this->yellow->system->get($key); } + public function getHtml($key) { return $this->yellow->system->getHtml($key); } + public function getData($filterStart = "", $filterEnd = "") { return $this->yellow->system->getData($filterStart, $filterEnd); } + public function getModified($httpFormat = false) { return $this->yellow->system->getModified($httpFormat); } + public function isExisting($key) { return $this->yellow->system->isExisting($key); } +} + +class YellowPlugins { //TODO: remove later, for backwards compatibility + public $yellow; + public $plugins; + public function __construct($yellow) { $this->yellow = $yellow; $this->plugins = array(); } + public function load($path = "") { $this->yellow->extensions->load($this->yellow->system->get("extensionDir")); } + public function register($name, $plugin, $obsoleteVersion = 0, $obsoletePriority = 0) { } + public function get($name) { return $this->yellow->extensions->get($name); } + public function getData() { return $this->yellow->extensions->getData(); } + public function getModified($httpFormat = false) { return $this->yellow->extensions->getModified($httpFormat); } + public function isExisting($name) { return $this->yellow->extensions->isExisting($name); } +} + +class YellowThemes { //TODO: remove later, for backwards compatibility + public $yellow; + public $themes; + public function __construct($yellow) { $this->yellow = $yellow; $this->themes = array(); } + public function load($path = "") { $this->yellow->extensions->load($this->yellow->system->get("extensionDir")); } + public function register($name, $theme, $obsoleteVersion = 0, $obsoletePriority = 0) { } + public function get($name) { return $this->yellow->extensions->get($name); } + public function getData() { return $this->yellow->extensions->getData(); } + public function getModified($httpFormat = false) { return $this->yellow->extensions->getModified($httpFormat); } + public function isExisting($name) { return $this->yellow->extensions->isExisting($name); } +} + +// Unicode support for PHP +mb_internal_encoding("UTF-8"); +function strempty($string) { + return is_null($string) || $string===""; +} +function strencode($string) { + return addcslashes($string, "\'\"\\\/"); +} +function strreplaceu() { + return call_user_func_array("str_replace", func_get_args()); +} +function strtoloweru() { + return call_user_func_array("mb_strtolower", func_get_args()); +} +function strtoupperu() { + return call_user_func_array("mb_strtoupper", func_get_args()); +} +function strlenu() { + return call_user_func_array("mb_strlen", func_get_args()); +} +function strlenb() { + return call_user_func_array("strlen", func_get_args()); +} +function strposu() { + return call_user_func_array("mb_strpos", func_get_args()); +} +function strposb() { + return call_user_func_array("strpos", func_get_args()); +} +function strrposu() { + return call_user_func_array("mb_strrpos", func_get_args()); +} +function strrposb() { + return call_user_func_array("strrpos", func_get_args()); +} +function substru() { + return call_user_func_array("mb_substr", func_get_args()); +} +function substrb() { + return call_user_func_array("substr", func_get_args()); +} + +// Error reporting for PHP +error_reporting(E_ALL ^ E_NOTICE); diff --git a/system/extensions/edit.css b/system/extensions/edit.css @@ -0,0 +1,553 @@ +/* Edit extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/edit */ +/* Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se */ +/* This file may be used and distributed under the terms of the public license. */ + +.yellow-bar { + position: relative; + line-height: 2em; + margin-bottom: 10px; +} +.yellow-bar-left { + display: block; + float: left; +} +.yellow-bar-right { + display: block; + float: right; +} +.yellow-bar-right a { + margin-left: 1em; +} +.yellow-bar-right #yellow-pane-create-link { + padding: 0 0.5em; +} +.yellow-bar-right #yellow-pane-delete-link { + padding: 0 0.5em; +} +.yellow-bar-banner { + clear: both; +} +.yellow-body-modal-open { + overflow: hidden; +} +.yellow-pane { + position: absolute; + display: none; + z-index: 100; + padding: 10px; + background-color: #fff; + color: #000; + border: 1px solid #bbb; + border-radius: 4px; + box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2); +} +.yellow-pane h1 { + color: #000; + font-size: 2em; + margin: 0 1em; +} +.yellow-pane p { + margin: 0.5em; +} +.yellow-pane .yellow-status { + margin-bottom: 1em; +} +.yellow-pane .yellow-fields { + width: 15em; + text-align: left; + margin: 0 auto; +} +.yellow-pane .yellow-form-control { + width: 15em; + box-sizing: border-box; +} +.yellow-pane .yellow-fields .yellow-btn { + width: 15em; + margin: 1em 0 0.5em 0; +} +.yellow-pane .yellow-buttons .yellow-btn { + width: 15em; + margin: 0.5em 0; +} +.yellow-close { + position: absolute; + top: 0.8em; + right: 1em; + cursor: pointer; + font-size: 0.9em; + color: #bbb; + text-decoration: none; +} +.yellow-close:hover { + color: #000; + text-decoration: none; +} +.yellow-arrow { + position: absolute; + top: 0; + left: 0; +} +.yellow-arrow:after, +.yellow-arrow:before { + position: absolute; + pointer-events: none; + bottom: 100%; + height: 0; + width: 0; + border: solid transparent; + content: ""; +} +.yellow-arrow:after { + border-color: rgba(255, 255, 255, 0); + border-bottom-color: #fff; + border-width: 10px; + margin-left: -10px; +} +.yellow-arrow:before { + border-color: rgba(187, 187, 187, 0); + border-bottom-color: #bbb; + border-width: 11px; + margin-left: -11px; +} +.yellow-popup { + position: absolute; + display: none; + z-index: 200; + padding: 10px 0; + background-color: #fff; + color: #000; + border: 1px solid #bbb; + border-radius: 4px; + box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2); +} +.yellow-dropdown { + list-style: none; + margin: 0; + padding: 0; +} +.yellow-dropdown span { + display: block; + margin: 0; + padding: 0.25em 1em; +} +.yellow-dropdown a { + display: block; + padding: 0.2em 1em; + text-decoration: none; +} +.yellow-dropdown a:hover { + color: #fff; + background-color: #18e; + text-decoration: none; +} +.yellow-dropdown-menu a { + color: #000; +} +.yellow-toolbar { + list-style: none; + margin: 0; + padding: 0; +} +.yellow-toolbar-left { + display: inline-block; + float: left; +} +.yellow-toolbar-right { + display: inline-block; + float: right; +} +.yellow-toolbar-banner { + clear: both; +} +.yellow-toolbar li { + display: inline-block; + vertical-align: top; +} +.yellow-toolbar a { + display: inline-block; + padding: 6px 16px; + text-decoration: none; + background-color: #fff; + color: #000; + font-size: 0.9em; + font-weight: normal; + border: 1px solid #bbb; + border-radius: 4px; +} +.yellow-toolbar a:hover { + background-color: #18e; + background-image: none; + border-color: #18e; + color: #fff; + text-decoration: none; +} +.yellow-toolbar-left a { + margin-right: 4px; + margin-bottom: 10px; +} +.yellow-toolbar-right a { + margin-left: 4px; + margin-bottom: 10px; +} +.yellow-toolbar .yellow-icon { + font-size: 0.9em; + min-width: 1em; + text-align: center; +} +.yellow-toolbar .yellow-toolbar-btn { + padding: 6px 10px; + min-width: 4em; + text-align: center; +} +.yellow-toolbar .yellow-toolbar-btn-edit { + background-color: #29f; + border-color: #29f; + color: #fff; +} +.yellow-toolbar .yellow-toolbar-btn-create { + background-color: #29f; + border-color: #29f; + color: #fff; +} +.yellow-toolbar .yellow-toolbar-btn-delete { + background-color: #e55; + border-color: #e55; + color: #fff; +} +.yellow-toolbar .yellow-toolbar-btn-delete:hover { + background-color: #d44; + border-color: #d44; +} +.yellow-toolbar .yellow-toolbar-btn-separator { + visibility: hidden; + padding: 6px; +} +.yellow-toolbar .yellow-toolbar-checked { + background-color: #666; + border-color: #666; + color: #fff; +} +.yellow-toolbar-tooltip { + position: relative; +} +.yellow-toolbar-tooltip::after, +.yellow-toolbar-tooltip::before { + position: absolute; + z-index: 300; + display: none; + pointer-events: none; +} +.yellow-toolbar-tooltip::after { + padding: 2px 9px; + font-weight: normal; + font-size: 0.9em; + text-align: center; + white-space: nowrap; + content: attr(aria-label); + background-color: #111; + color: #ddd; + border-radius: 3px; + top: 100%; + right: 50%; + margin-top: 6px; + transform: translateX(50%); +} +.yellow-toolbar-tooltip::before { + width: 0; + height: 0; + content: ""; + border: 4px solid transparent; + top: auto; + right: 50%; + bottom: -6px; + margin-right: -4px; + border-bottom-color: #111; +} +.yellow-toolbar-tooltip:hover::before, +.yellow-toolbar-tooltip:hover::after { + display: inline-block; +} +.yellow-toolbar-selected.yellow-toolbar-tooltip::before, +.yellow-toolbar-selected.yellow-toolbar-tooltip::after { + display: none; +} +.yellow-form-control { + margin: 0; + padding: 2px 4px; + display: inline-block; + background-color: #fff; + color: #000; + 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; +} +.yellow-btn { + margin: 0; + padding: 4px 22px; + display: inline-block; + min-width: 8em; + 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; +} +.yellow-btn:hover, +.yellow-btn:focus, +.yellow-btn:active { + color: #333333; + background-image: none; + text-decoration: none; +} +.yellow-btn:active { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Specific panes */ + +#yellow-pane-login, +#yellow-pane-signup, +#yellow-pane-forgot, +#yellow-pane-recover, +#yellow-pane-settings, +#yellow-pane-version, +#yellow-pane-quit { + text-align: center; +} +#yellow-pane-edit-toolbar-title { + margin: -5px 0 0 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +#yellow-pane-edit-text { + padding: 0 2px; + outline: none; + resize: none; + border: none; +} +#yellow-pane-edit-preview { + padding: 0; + overflow: auto; +} +#yellow-pane-edit-preview h1 { + margin: 0.67em 0; +} +#yellow-pane-edit-preview p { + margin: 1em 0; +} +#yellow-pane-edit-preview .content { + margin: 0; + padding: 0; +} +#yellow-pane-user { + padding: 10px 0; +} + +/* Specific popups */ + +#yellow-popup-format, +#yellow-popup-heading, +#yellow-popup-list { + width: 16em; +} +#yellow-popup-format a, +#yellow-popup-heading a { + padding: 0.25em 16px; +} +#yellow-popup-format #yellow-popup-format-h1, +#yellow-popup-heading #yellow-popup-heading-h1 { + font-size: 2em; + font-weight: bold; +} +#yellow-popup-format #yellow-popup-format-h2, +#yellow-popup-heading #yellow-popup-heading-h2 { + font-size: 1.6em; + font-weight: bold; +} +#yellow-popup-format #yellow-popup-format-h3, +#yellow-popup-heading #yellow-popup-heading-h3 { + font-size: 1.3em; + font-weight: bold; +} +#yellow-popup-format #yellow-popup-format-quote { + font-style: italic; +} +#yellow-popup-format #yellow-popup-format-pre { + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 0.9em; + line-height: 1.8; +} +#yellow-popup-emojiawesome { + padding: 10px; + width: 14em; +} +#yellow-popup-emojiawesome a { + padding: 0.2em; +} +#yellow-popup-emojiawesome .yellow-dropdown li { + display: inline-block; +} +#yellow-popup-fontawesome { + padding: 10px; + width: 13em; +} +#yellow-popup-fontawesome a { + padding: 0.18em 0.3em; + min-width: 1em; + text-align: center; +} +#yellow-popup-fontawesome .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; +} +.yellow-spin { + -webkit-animation: yellow-spin 1s infinite steps(16); + animation: yellow-spin 1s infinite steps(16); +} +@-webkit-keyframes yellow-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes yellow-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + 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-italic:before { + content: "\f0f7"; +} +.yellow-icon-strikethrough:before { + content: "\f108"; +} +.yellow-icon-quote:before { + content: "\f109"; +} +.yellow-icon-code:before { + content: "\f10a"; +} +.yellow-icon-pre:before { + content: "\f10a"; +} +.yellow-icon-link:before { + content: "\f10b"; +} +.yellow-icon-file:before { + content: "\f10c"; +} +.yellow-icon-list:before { + content: "\f10d"; +} +.yellow-icon-ul:before { + content: "\f10d"; +} +.yellow-icon-ol:before { + content: "\f10e"; +} +.yellow-icon-tl:before { + content: "\f10f"; +} +.yellow-icon-hr:before { + content: "\f110"; +} +.yellow-icon-table:before { + content: "\f111"; +} +.yellow-icon-emojiawesome:before { + content: "\f112"; +} +.yellow-icon-fontawesome:before { + content: "\f113"; +} +.yellow-icon-draft:before { + content: "\f114"; +} +.yellow-icon-undo:before { + content: "\f115"; +} +.yellow-icon-redo:before { + content: "\f116"; +} +.yellow-icon-spinner:before { + content: "\f200"; +} +.yellow-icon-search:before { + content: "\f201"; +} +.yellow-icon-close:before { + content: "\f202"; +} +.yellow-icon-help:before { + content: "\f203"; +} +.yellow-icon-markdown:before { + content: "\f203"; +} +.yellow-icon-logo:before { + content: "\f8ff"; +} diff --git a/system/extensions/edit.js b/system/extensions/edit.js @@ -0,0 +1,1317 @@ +// Edit extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/edit +// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +var yellow = { + + // Main event handlers + action: function(action, status, args) { yellow.edit.action(action, status, args); }, + onLoad: function() { yellow.edit.load(); }, + onClickAction: function(e) { yellow.edit.clickAction(e); }, + onClick: function(e) { yellow.edit.click(e); }, + onKeydown: function(e) { yellow.edit.keydown(e); }, + onDrag: function(e) { yellow.edit.drag(e); }, + onDrop: function(e) { yellow.edit.drop(e); }, + onUpdate: function() { yellow.edit.updatePane(yellow.edit.paneId, yellow.edit.paneAction, yellow.edit.paneStatus); }, + onResize: function() { yellow.edit.resizePane(yellow.edit.paneId, yellow.edit.paneAction, yellow.edit.paneStatus); } +}; + +yellow.edit = { + paneId: 0, //visible pane ID + paneActionOld: 0, //previous pane action + paneAction: 0, //current pane action + paneStatus: 0, //current pane status + popupId: 0, //visible popup ID + intervalId: 0, //timer interval ID + + // Handle initialisation + load: function() { + var body = document.getElementsByTagName("body")[0]; + if (body && body.firstChild && !document.getElementById("yellow-bar")) { + this.createBar("yellow-bar"); + this.createPane("yellow-pane-edit", "none", "none"); + this.action(yellow.page.action, yellow.page.status); + clearInterval(this.intervalId); + } + }, + + // Handle action + action: function(action, status, args) { + status = status ? status : "none"; + args = args ? args : "none"; + switch (action) { + case "login": this.showPane("yellow-pane-login", action, status); break; + case "logout": this.sendPane("yellow-pane-logout", action); break; + case "signup": this.showPane("yellow-pane-signup", action, status); break; + case "confirm": this.showPane("yellow-pane-signup", action, status); break; + case "approve": this.showPane("yellow-pane-signup", action, status); break; + case "forgot": this.showPane("yellow-pane-forgot", action, status); break; + case "recover": this.showPane("yellow-pane-recover", action, status); break; + case "reactivate": this.showPane("yellow-pane-settings", action, status); break; + case "settings": this.showPane("yellow-pane-settings", action, status); break; + case "verify": this.showPane("yellow-pane-settings", action, status); break; + case "change": this.showPane("yellow-pane-settings", action, status); break; + case "version": this.showPane("yellow-pane-version", action, status); break; + case "update": this.sendPane("yellow-pane-update", action, status, args); break; + case "quit": this.showPane("yellow-pane-quit", action, status); break; + case "remove": this.showPane("yellow-pane-quit", action, status); break; + case "create": this.showPane("yellow-pane-edit", action, status, true); break; + case "edit": this.showPane("yellow-pane-edit", action, status, true); break; + case "delete": this.showPane("yellow-pane-edit", action, status, true); break; + case "user": this.showPane("yellow-pane-user", action, status); break; + case "send": this.sendPane(this.paneId, this.paneAction); break; + case "close": this.hidePane(this.paneId); break; + case "toolbar": this.processToolbar(status, args); break; + case "help": this.processHelp(); break; + } + }, + + // Handle action clicked + clickAction: function(e) { + e.stopPropagation(); + e.preventDefault(); + var element = e.target; + for (; element; element=element.parentNode) { + if (element.tagName=="A") break; + } + this.action(element.getAttribute("data-action"), element.getAttribute("data-status"), element.getAttribute("data-args")); + }, + + // Handle mouse clicked + click: function(e) { + if (this.popupId && !document.getElementById(this.popupId).contains(e.target)) this.hidePopup(this.popupId, true); + if (this.paneId && !document.getElementById(this.paneId).contains(e.target)) this.hidePane(this.paneId, true); + }, + + // Handle keyboard + keydown: function(e) { + if (this.paneId=="yellow-pane-edit") this.processShortcut(e); + if (this.paneId && e.keyCode==27) this.hidePane(this.paneId); + }, + + // Handle drag + drag: function(e) { + e.stopPropagation(); + e.preventDefault(); + }, + + // Handle drop + drop: function(e) { + e.stopPropagation(); + e.preventDefault(); + var elementText = document.getElementById("yellow-pane-edit-text"); + var files = e.dataTransfer ? e.dataTransfer.files : e.target.files; + for (var i=0; i<files.length; i++) this.uploadFile(elementText, files[i]); + }, + + // Create bar + createBar: function(barId) { + if (yellow.system.debug) console.log("yellow.edit.createBar id:"+barId); + var elementBar = document.createElement("div"); + elementBar.className = "yellow-bar"; + elementBar.setAttribute("id", barId); + if (barId=="yellow-bar") { + yellow.toolbox.addEvent(document, "click", yellow.onClick); + yellow.toolbox.addEvent(document, "keydown", yellow.onKeydown); + yellow.toolbox.addEvent(window, "resize", yellow.onResize); + } + var elementDiv = document.createElement("div"); + elementDiv.setAttribute("id", barId+"-content"); + if (yellow.system.userName) { + elementDiv.innerHTML = + "<div class=\"yellow-bar-left\">"+ + "<a href=\"#\" id=\"yellow-pane-edit-link\" data-action=\"edit\">"+this.getText("Edit")+"</a>"+ + "</div>"+ + "<div class=\"yellow-bar-right\">"+ + "<a href=\"#\" id=\"yellow-pane-create-link\" data-action=\"create\">"+this.getText("Create")+"</a>"+ + "<a href=\"#\" id=\"yellow-pane-delete-link\" data-action=\"delete\">"+this.getText("Delete")+"</a>"+ + "<a href=\"#\" id=\"yellow-pane-user-link\" data-action=\"user\">"+yellow.toolbox.encodeHtml(yellow.system.userName)+"</a>"+ + "</div>"+ + "<div class=\"yellow-bar-banner\"></div>"; + } + elementBar.appendChild(elementDiv); + yellow.toolbox.insertBefore(elementBar, document.getElementsByTagName("body")[0].firstChild); + this.bindActions(elementBar); + }, + + // Create pane + createPane: function(paneId, paneAction, paneStatus) { + if (yellow.system.debug) console.log("yellow.edit.createPane id:"+paneId); + var elementPane = document.createElement("div"); + elementPane.className = "yellow-pane"; + elementPane.setAttribute("id", paneId); + elementPane.style.display = "none"; + if (paneId=="yellow-pane-edit") { + yellow.toolbox.addEvent(elementPane, "input", yellow.onUpdate); + yellow.toolbox.addEvent(elementPane, "dragenter", yellow.onDrag); + yellow.toolbox.addEvent(elementPane, "dragover", yellow.onDrag); + yellow.toolbox.addEvent(elementPane, "drop", yellow.onDrop); + } + if (paneId=="yellow-pane-edit" || paneId=="yellow-pane-user") { + var elementArrow = document.createElement("span"); + elementArrow.className = "yellow-arrow"; + elementArrow.setAttribute("id", paneId+"-arrow"); + elementPane.appendChild(elementArrow); + } + var elementDiv = document.createElement("div"); + elementDiv.className = "yellow-content"; + elementDiv.setAttribute("id", paneId+"-content"); + switch (paneId) { + 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>"+ + "<div class=\"yellow-title\"><h1>"+this.getText("LoginTitle")+"</h1></div>"+ + "<div class=\"yellow-fields\" id=\"yellow-pane-login-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"login\" />"+ + "<p><label for=\"yellow-pane-login-email\">"+this.getText("LoginEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-login-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(yellow.system.editLoginEmail)+"\" /></p>"+ + "<p><label for=\"yellow-pane-login-password\">"+this.getText("LoginPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-login-password\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(yellow.system.editLoginPassword)+"\" /></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("LoginButton")+"\" /></p>"+ + "</div>"+ + "<div class=\"yellow-actions\" id=\"yellow-pane-login-actions\">"+ + "<p><a href=\"#\" id=\"yellow-pane-login-forgot\" data-action=\"forgot\">"+this.getText("LoginForgot")+"</a><br /><a href=\"#\" id=\"yellow-pane-login-signup\" data-action=\"signup\">"+this.getText("LoginSignup")+"</a></p>"+ + "</div>"+ + "</form>"; + break; + 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>"+ + "<div class=\"yellow-title\"><h1>"+this.getText("SignupTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-signup-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-fields\" id=\"yellow-pane-signup-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"signup\" />"+ + "<p><label for=\"yellow-pane-signup-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-signup-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-signup-email\">"+this.getText("SignupEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-signup-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-signup-password\">"+this.getText("SignupPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-signup-password\" maxlength=\"64\" value=\"\" /></p>"+ + "<p><input type=\"checkbox\" name=\"consent\" value=\"consent\" id=\"consent\""+(this.getRequest("consent") ? " checked=\"checked\"" : "")+"> <label for=\"consent\">"+this.getText("SignupConsent")+"</label></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("SignupButton")+"\" /></p>"+ + "</div>"+ + "<div class=\"yellow-buttons\" id=\"yellow-pane-signup-buttons\">"+ + "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ + "</div>"+ + "</form>"; + break; + 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>"+ + "<div class=\"yellow-title\"><h1>"+this.getText("ForgotTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-forgot-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-fields\" id=\"yellow-pane-forgot-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"forgot\" />"+ + "<p><label for=\"yellow-pane-forgot-email\">"+this.getText("ForgotEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-forgot-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+ + "</div>"+ + "<div class=\"yellow-buttons\" id=\"yellow-pane-forgot-buttons\">"+ + "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ + "</div>"+ + "</form>"; + break; + 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>"+ + "<div class=\"yellow-title\"><h1>"+this.getText("RecoverTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-recover-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-fields\" id=\"yellow-pane-recover-fields\">"+ + "<p><label for=\"yellow-pane-recover-password\">"+this.getText("RecoverPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-recover-password\" maxlength=\"64\" value=\"\" /></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+ + "</div>"+ + "<div class=\"yellow-buttons\" id=\"yellow-pane-recover-buttons\">"+ + "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-settings": + var rawDataLanguages = ""; + if (yellow.system.serverLanguages && Object.keys(yellow.system.serverLanguages).length>1) { + rawDataLanguages += "<p>"; + for (var language in yellow.system.serverLanguages) { + var checked = language==this.getRequest("language") ? " checked=\"checked\"" : ""; + rawDataLanguages += "<label for=\"yellow-pane-settings-"+language+"\"><input type=\"radio\" name=\"language\" id=\"yellow-pane-settings-"+language+"\" value=\""+language+"\""+checked+"> "+yellow.toolbox.encodeHtml(yellow.system.serverLanguages[language])+"</label><br />"; + } + rawDataLanguages += "</p>"; + } + 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-settings-title\">"+this.getText("SettingsTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-settings-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-fields\" id=\"yellow-pane-settings-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"settings\" />"+ + "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+ + "<p><label for=\"yellow-pane-settings-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-settings-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-settings-email\">"+this.getText("SignupEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-settings-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ + "<p><label for=\"yellow-pane-settings-password\">"+this.getText("SignupPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-settings-password\" maxlength=\"64\" value=\"\" /></p>"+rawDataLanguages+ + "<p>"+this.getText("SettingsQuit")+" <a href=\"#\" data-action=\"quit\">"+this.getText("SettingsMore")+"</a></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+ + "</div>"+ + "<div class=\"yellow-buttons\" id=\"yellow-pane-settings-buttons\">"+ + "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-version": + 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-version-title\">"+yellow.toolbox.encodeHtml(yellow.system.serverVersion)+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-version-status\" class=\""+paneStatus+"\">"+this.getText("VersionStatus", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-output\" id=\"yellow-pane-version-output\">"+yellow.page.rawDataOutput+"</div>"+ + "<div class=\"yellow-buttons\" id=\"yellow-pane-version-buttons\">"+ + "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ + "</div>"+ + "</form>"; + break; + 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>"+ + "<div class=\"yellow-title\"><h1>"+this.getText("QuitTitle")+"</h1></div>"+ + "<div class=\"yellow-status\"><p id=\"yellow-pane-quit-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ + "<div class=\"yellow-fields\" id=\"yellow-pane-quit-fields\">"+ + "<input type=\"hidden\" name=\"action\" value=\"quit\" />"+ + "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+ + "<p><label for=\"yellow-pane-quit-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-quit-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+ + "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("DeleteButton")+"\" /></p>"+ + "</div>"+ + "<div class=\"yellow-buttons\" id=\"yellow-pane-quit-buttons\">"+ + "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ + "</div>"+ + "</form>"; + break; + case "yellow-pane-edit": + var rawDataButtons = ""; + if (yellow.system.editToolbarButtons && yellow.system.editToolbarButtons!="none") { + var tokens = yellow.system.editToolbarButtons.split(","); + for (var i=0; i<tokens.length; i++) { + var token = tokens[i].trim(); + if (token!="separator") { + rawDataButtons += "<li><a href=\"#\" id=\"yellow-toolbar-"+yellow.toolbox.encodeHtml(token)+"\" class=\"yellow-toolbar-btn-icon yellow-toolbar-tooltip\" data-action=\"toolbar\" data-status=\""+yellow.toolbox.encodeHtml(token)+"\" aria-label=\""+this.getText("Toolbar", "", token)+"\"><i class=\"yellow-icon yellow-icon-"+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>"; + } else { + rawDataButtons += "<li><a href=\"#\" class=\"yellow-toolbar-btn-separator\"></a></li>"; + } + } + if (yellow.system.debug) console.log("yellow.edit.createPane buttons:"+yellow.system.editToolbarButtons); + } + elementDiv.innerHTML = + "<form method=\"post\">"+ + "<div id=\"yellow-pane-edit-toolbar\">"+ + "<h1 id=\"yellow-pane-edit-toolbar-title\" class=\"yellow-toolbar yellow-toolbar-left\">"+this.getText("Edit")+"</h1>"+ + "<ul id=\"yellow-pane-edit-toolbar-buttons\" class=\"yellow-toolbar yellow-toolbar-left\">"+rawDataButtons+"</ul>"+ + "<ul id=\"yellow-pane-edit-toolbar-main\" class=\"yellow-toolbar yellow-toolbar-right\">"+ + "<li><a href=\"#\" id=\"yellow-pane-edit-cancel\" class=\"yellow-toolbar-btn\" data-action=\"close\">"+this.getText("CancelButton")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-pane-edit-send\" class=\"yellow-toolbar-btn\" data-action=\"send\">"+this.getText("EditButton")+"</a></li>"+ + "</ul>"+ + "<ul class=\"yellow-toolbar yellow-toolbar-banner\"></ul>"+ + "</div>"+ + "<textarea id=\"yellow-pane-edit-text\" class=\"yellow-form-control\"></textarea>"+ + "<div id=\"yellow-pane-edit-preview\"></div>"+ + "</form>"; + break; + case "yellow-pane-user": + elementDiv.innerHTML = + "<ul class=\"yellow-dropdown\">"+ + "<li><span>"+yellow.toolbox.encodeHtml(yellow.system.userEmail)+"</span></li>"+ + "<li><a href=\"#\" data-action=\"settings\">"+this.getText("SettingsTitle")+"</a></li>" + + "<li><a href=\"#\" data-action=\"help\">"+this.getText("UserHelp")+"</a></li>" + + "<li><a href=\"#\" data-action=\"logout\">"+this.getText("UserLogout")+"</a></li>"+ + "</ul>"; + break; + } + elementPane.appendChild(elementDiv); + yellow.toolbox.insertAfter(elementPane, document.getElementsByTagName("body")[0].firstChild); + this.bindActions(elementPane); + }, + + // Update pane + updatePane: function(paneId, paneAction, paneStatus, init) { + if (yellow.system.debug) console.log("yellow.edit.updatePane id:"+paneId); + var showFields = paneStatus!="next" && paneStatus!="done"; + switch (paneId) { + case "yellow-pane-login": + if (yellow.system.editLoginRestrictions) { + yellow.toolbox.setVisible(document.getElementById("yellow-pane-login-signup"), false); + } + break; + case "yellow-pane-signup": + yellow.toolbox.setVisible(document.getElementById("yellow-pane-signup-fields"), showFields); + yellow.toolbox.setVisible(document.getElementById("yellow-pane-signup-buttons"), !showFields); + break; + case "yellow-pane-forgot": + yellow.toolbox.setVisible(document.getElementById("yellow-pane-forgot-fields"), showFields); + yellow.toolbox.setVisible(document.getElementById("yellow-pane-forgot-buttons"), !showFields); + break; + case "yellow-pane-recover": + yellow.toolbox.setVisible(document.getElementById("yellow-pane-recover-fields"), showFields); + yellow.toolbox.setVisible(document.getElementById("yellow-pane-recover-buttons"), !showFields); + break; + case "yellow-pane-settings": + yellow.toolbox.setVisible(document.getElementById("yellow-pane-settings-fields"), showFields); + yellow.toolbox.setVisible(document.getElementById("yellow-pane-settings-buttons"), !showFields); + if (paneStatus=="none") { + document.getElementById("yellow-pane-settings-status").innerHTML = "<a href=\"#\" data-action=\"version\">"+this.getText("VersionTitle")+"</a>"; + document.getElementById("yellow-pane-settings-name").value = yellow.system.userName; + document.getElementById("yellow-pane-settings-email").value = yellow.system.userEmail; + document.getElementById("yellow-pane-settings-"+yellow.system.userLanguage).checked = true; + } + break; + case "yellow-pane-version": + if (paneStatus=="none" && this.isExtension("update")) { + document.getElementById("yellow-pane-version-status").innerHTML = this.getText("VersionStatusCheck"); + document.getElementById("yellow-pane-version-output").innerHTML = ""; + setTimeout("yellow.action('send');", 500); + } + if (paneStatus=="updates" && this.isExtension("update")) { + document.getElementById("yellow-pane-version-status").innerHTML = "<a href=\"#\" data-action=\"update\">"+this.getText("VersionStatusUpdates")+"</a>"; + } + break; + case "yellow-pane-quit": + yellow.toolbox.setVisible(document.getElementById("yellow-pane-quit-fields"), showFields); + yellow.toolbox.setVisible(document.getElementById("yellow-pane-quit-buttons"), !showFields); + if (paneStatus=="none") { + document.getElementById("yellow-pane-quit-status").innerHTML = this.getText("QuitStatusNone"); + document.getElementById("yellow-pane-quit-name").value = ""; + } + break; + case "yellow-pane-edit": + document.getElementById("yellow-pane-edit-text").focus(); + if (init) { + yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-text"), true); + yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-preview"), false); + document.getElementById("yellow-pane-edit-toolbar-title").innerHTML = yellow.toolbox.encodeHtml(yellow.page.title); + document.getElementById("yellow-pane-edit-text").value = paneAction=="create" ? yellow.page.rawDataNew : yellow.page.rawDataEdit; + var matches = document.getElementById("yellow-pane-edit-text").value.match(/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+/); + var position = document.getElementById("yellow-pane-edit-text").value.indexOf("\n", matches ? matches[0].length : 0); + document.getElementById("yellow-pane-edit-text").setSelectionRange(position, position); + if (yellow.system.editToolbarButtons!="none") { + yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-toolbar-title"), false); + this.updateToolbar(0, "yellow-toolbar-checked"); + } + if (yellow.system.userRestrictions) { + yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-send"), false); + document.getElementById("yellow-pane-edit-text").readOnly = true; + } + } + if (!yellow.system.userRestrictions) { + var key, className; + switch (this.getAction(paneId, paneAction)) { + case "create": key = "CreateButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-create"; break; + case "edit": key = "EditButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-edit"; break; + case "delete": key = "DeleteButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-delete"; break; + } + if (document.getElementById("yellow-pane-edit-send").className != className) { + document.getElementById("yellow-pane-edit-send").innerHTML = this.getText(key); + document.getElementById("yellow-pane-edit-send").className = className; + this.resizePane(paneId, paneAction, paneStatus); + } + } + break; + } + this.bindActions(document.getElementById(paneId)); + }, + + // Resize pane + resizePane: function(paneId, paneAction, paneStatus) { + var elementBar = document.getElementById("yellow-bar-content"); + var paneLeft = yellow.toolbox.getOuterLeft(elementBar); + var paneTop = yellow.toolbox.getOuterTop(elementBar) + yellow.toolbox.getOuterHeight(elementBar) + 10; + var paneWidth = yellow.toolbox.getOuterWidth(elementBar); + 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-login": + case "yellow-pane-signup": + case "yellow-pane-forgot": + case "yellow-pane-recover": + case "yellow-pane-settings": + case "yellow-pane-version": + case "yellow-pane-quit": + yellow.toolbox.setOuterLeft(document.getElementById(paneId), paneLeft); + yellow.toolbox.setOuterTop(document.getElementById(paneId), paneTop); + yellow.toolbox.setOuterWidth(document.getElementById(paneId), paneWidth); + break; + case "yellow-pane-edit": + yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-edit"), paneLeft); + yellow.toolbox.setOuterTop(document.getElementById("yellow-pane-edit"), paneTop); + yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit"), paneHeight); + yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit"), paneWidth); + var elementWidth = yellow.toolbox.getWidth(document.getElementById("yellow-pane-edit")); + yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-text"), elementWidth); + yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-preview"), elementWidth); + var buttonsWidth = 0; + var buttonsWidthMax = yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-edit-toolbar")) - + yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-edit-toolbar-main")) - 1; + var element = document.getElementById("yellow-pane-edit-toolbar-buttons").firstChild; + for (; element; element=element.nextSibling) { + element.removeAttribute("style"); + buttonsWidth += yellow.toolbox.getOuterWidth(element); + if (buttonsWidth>buttonsWidthMax) yellow.toolbox.setVisible(element, false); + } + yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-toolbar-title"), buttonsWidthMax); + var height1 = yellow.toolbox.getHeight(document.getElementById("yellow-pane-edit")); + var height2 = yellow.toolbox.getOuterHeight(document.getElementById("yellow-pane-edit-toolbar")); + yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit-text"), height1 - height2); + yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit-preview"), height1 - height2); + var elementLink = document.getElementById("yellow-pane-"+paneAction+"-link"); + var position = yellow.toolbox.getOuterLeft(elementLink) + yellow.toolbox.getOuterWidth(elementLink)/2; + position -= yellow.toolbox.getOuterLeft(document.getElementById("yellow-pane-edit")) + 1; + yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-edit-arrow"), position); + break; + case "yellow-pane-user": + yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-user"), paneLeft + paneWidth - yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-user"))); + yellow.toolbox.setOuterTop(document.getElementById("yellow-pane-user"), paneTop); + var elementLink = document.getElementById("yellow-pane-user-link"); + var position = yellow.toolbox.getOuterLeft(elementLink) + yellow.toolbox.getOuterWidth(elementLink)/2; + position -= yellow.toolbox.getOuterLeft(document.getElementById("yellow-pane-user")); + yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-user-arrow"), position); + break; + } + }, + + // Show or hide pane + showPane: function(paneId, paneAction, paneStatus, modal) { + if (this.paneId!=paneId || this.paneAction!=paneAction) { + this.hidePane(this.paneId); + 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); + yellow.toolbox.setVisible(element, true); + if (modal) { + yellow.toolbox.addClass(document.body, "yellow-body-modal-open"); + yellow.toolbox.addValue("meta[name=viewport]", "content", ", maximum-scale=1, user-scalable=0"); + } + this.paneId = paneId; + this.paneAction = paneAction; + this.paneStatus = paneStatus; + this.updatePane(paneId, paneAction, paneStatus, this.paneActionOld!=this.paneAction); + this.resizePane(paneId, paneAction, paneStatus); + } + } else { + this.hidePane(this.paneId, true); + } + }, + + // Hide pane + hidePane: function(paneId, fadeout) { + var element = document.getElementById(paneId); + if (yellow.toolbox.isVisible(element)) { + 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); + this.paneId = 0; + this.paneActionOld = this.paneAction; + this.paneAction = 0; + this.paneStatus = 0; + } + this.hidePopup(this.popupId); + }, + + // Send pane + sendPane: function(paneId, paneAction, paneStatus, paneArgs) { + if (yellow.system.debug) console.log("yellow.edit.sendPane id:"+paneId); + var args = { "action":paneAction, "csrftoken":this.getCookie("csrftoken") }; + if (paneId=="yellow-pane-edit") { + args.action = this.getAction(paneId, paneAction); + args.rawdatasource = yellow.page.rawDataSource; + args.rawdataedit = document.getElementById("yellow-pane-edit-text").value; + args.rawdataendofline = yellow.page.rawDataEndOfLine; + } + if (paneArgs) { + var tokens = paneArgs.split("/"); + for (var i=0; i<tokens.length; i++) { + var pair = tokens[i].split(/[:=]/); + if (!pair[0] || !pair[1]) continue; + args[pair[0]] = pair[1]; + } + } + yellow.toolbox.submitForm(args); + }, + + // Process help + processHelp: function() { + this.hidePane(this.paneId); + window.open(this.getText("HelpUrl", "yellow"), "_self"); + }, + + // Process shortcut + processShortcut: function(e) { + var shortcut = yellow.toolbox.getEventShortcut(e); + if (shortcut) { + var tokens = yellow.system.editKeyboardShortcuts.split(","); + for (var i=0; i<tokens.length; i++) { + var pair = tokens[i].trim().split(" "); + if (shortcut==pair[0] || shortcut.replace("meta+", "ctrl+")==pair[0]) { + e.stopPropagation(); + e.preventDefault(); + this.processToolbar(pair[1]); + } + } + } + }, + + // Process toolbar + processToolbar: function(status, args) { + if (yellow.system.debug) console.log("yellow.edit.processToolbar status:"+status); + var elementText = document.getElementById("yellow-pane-edit-text"); + var elementPreview = document.getElementById("yellow-pane-edit-preview"); + if (!yellow.system.userRestrictions && this.paneAction!="delete" && !yellow.toolbox.isVisible(elementPreview)) { + switch (status) { + case "h1": yellow.editor.setMarkdown(elementText, "# ", "insert-multiline-block", true); break; + case "h2": yellow.editor.setMarkdown(elementText, "## ", "insert-multiline-block", true); break; + case "h3": yellow.editor.setMarkdown(elementText, "### ", "insert-multiline-block", true); break; + case "paragraph": yellow.editor.setMarkdown(elementText, "", "remove-multiline-block"); + yellow.editor.setMarkdown(elementText, "", "remove-fenced-block"); break; + case "quote": yellow.editor.setMarkdown(elementText, "> ", "insert-multiline-block", true); break; + case "pre": yellow.editor.setMarkdown(elementText, "```\n", "insert-fenced-block", true); break; + case "bold": yellow.editor.setMarkdown(elementText, "**", "insert-inline", true); break; + case "italic": yellow.editor.setMarkdown(elementText, "*", "insert-inline", true); break; + case "strikethrough": yellow.editor.setMarkdown(elementText, "~~", "insert-inline", true); break; + case "code": yellow.editor.setMarkdown(elementText, "`", "insert-autodetect", true); break; + case "ul": yellow.editor.setMarkdown(elementText, "* ", "insert-multiline-block", true); break; + case "ol": yellow.editor.setMarkdown(elementText, "1. ", "insert-multiline-block", true); break; + case "tl": yellow.editor.setMarkdown(elementText, "- [ ] ", "insert-multiline-block", true); break; + case "link": yellow.editor.setMarkdown(elementText, "[link](url)", "insert", false, yellow.editor.getMarkdownLink); break; + case "text": yellow.editor.setMarkdown(elementText, args, "insert"); break; + case "draft": yellow.editor.setMetaData(elementText, "status", "draft", true); break; + case "file": this.showFileDialog(); break; + case "undo": yellow.editor.undo(); break; + case "redo": yellow.editor.redo(); break; + } + } + if (status=="preview") this.showPreview(elementText, elementPreview); + if (status=="save" && !yellow.system.userRestrictions && this.paneAction!="delete") this.action("send"); + if (status=="help") window.open(this.getText("HelpUrl", "yellow"), "_blank"); + if (status=="markdown") window.open(this.getText("MarkdownUrl", "yellow"), "_blank"); + if (status=="format" || status=="heading" || status=="list" || status=="emojiawesome" || status=="fontawesome") { + this.showPopup("yellow-popup-"+status, status); + } else { + this.hidePopup(this.popupId); + } + }, + + // Update toolbar + updateToolbar: function(status, name) { + if (status) { + var element = document.getElementById("yellow-toolbar-"+status); + if (element) yellow.toolbox.addClass(element, name); + } else { + var elements = document.getElementsByClassName(name); + for (var i=0, l=elements.length; i<l; i++) { + yellow.toolbox.removeClass(elements[i], name); + } + } + }, + + // Create popup + createPopup: function(popupId) { + if (yellow.system.debug) console.log("yellow.edit.createPopup id:"+popupId); + var elementPopup = document.createElement("div"); + elementPopup.className = "yellow-popup"; + elementPopup.setAttribute("id", popupId); + elementPopup.style.display = "none"; + var elementDiv = document.createElement("div"); + elementDiv.setAttribute("id", popupId+"-content"); + switch (popupId) { + case "yellow-popup-format": + elementDiv.innerHTML = + "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+ + "<li><a href=\"#\" id=\"yellow-popup-format-h1\" data-action=\"toolbar\" data-status=\"h1\">"+this.getText("ToolbarH1")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-h2\" data-action=\"toolbar\" data-status=\"h2\">"+this.getText("ToolbarH2")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-h3\" data-action=\"toolbar\" data-status=\"h3\">"+this.getText("ToolbarH3")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-paragraph\" data-action=\"toolbar\" data-status=\"paragraph\">"+this.getText("ToolbarParagraph")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-pre\" data-action=\"toolbar\" data-status=\"pre\">"+this.getText("ToolbarPre")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-format-quote\" data-action=\"toolbar\" data-status=\"quote\">"+this.getText("ToolbarQuote")+"</a></li>"+ + "</ul>"; + break; + case "yellow-popup-heading": + elementDiv.innerHTML = + "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+ + "<li><a href=\"#\" id=\"yellow-popup-heading-h1\" data-action=\"toolbar\" data-status=\"h1\">"+this.getText("ToolbarH1")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-heading-h2\" data-action=\"toolbar\" data-status=\"h2\">"+this.getText("ToolbarH2")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-heading-h3\" data-action=\"toolbar\" data-status=\"h3\">"+this.getText("ToolbarH3")+"</a></li>"+ + "</ul>"; + break; + case "yellow-popup-list": + elementDiv.innerHTML = + "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+ + "<li><a href=\"#\" id=\"yellow-popup-list-ul\" data-action=\"toolbar\" data-status=\"ul\">"+this.getText("ToolbarUl")+"</a></li>"+ + "<li><a href=\"#\" id=\"yellow-popup-list-ol\" data-action=\"toolbar\" data-status=\"ol\">"+this.getText("ToolbarOl")+"</a></li>"+ + "</ul>"; + break; + case "yellow-popup-emojiawesome": + var rawDataEmojis = ""; + if (yellow.system.emojiawesomeToolbarButtons && yellow.system.emojiawesomeToolbarButtons!="none") { + var tokens = yellow.system.emojiawesomeToolbarButtons.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-args=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"ea ea-"+yellow.toolbox.encodeHtml(className)+"\"></i></a></li>"; + } + } + elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataEmojis+"</ul>"; + break; + case "yellow-popup-fontawesome": + var rawDataIcons = ""; + if (yellow.system.fontawesomeToolbarButtons && yellow.system.fontawesomeToolbarButtons!="none") { + var tokens = yellow.system.fontawesomeToolbarButtons.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-args=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"fa "+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>"; + } + } + elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataIcons+"</ul>"; + break; + } + elementPopup.appendChild(elementDiv); + yellow.toolbox.insertAfter(elementPopup, document.getElementsByTagName("body")[0].firstChild); + this.bindActions(elementPopup); + }, + + // Show or hide popup + showPopup: function(popupId, status) { + if (this.popupId!=popupId) { + 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); + yellow.toolbox.setVisible(element, true); + this.popupId = popupId; + this.updateToolbar(status, "yellow-toolbar-selected"); + var elementParent = document.getElementById("yellow-toolbar-"+status); + var popupLeft = yellow.toolbox.getOuterLeft(elementParent); + var popupTop = yellow.toolbox.getOuterTop(elementParent) + yellow.toolbox.getOuterHeight(elementParent) - 1; + yellow.toolbox.setOuterLeft(document.getElementById(popupId), popupLeft); + yellow.toolbox.setOuterTop(document.getElementById(popupId), popupTop); + } else { + this.hidePopup(this.popupId, true); + } + }, + + // Hide popup + hidePopup: function(popupId, fadeout) { + var element = document.getElementById(popupId); + if (yellow.toolbox.isVisible(element)) { + yellow.toolbox.setVisible(element, false, fadeout); + this.popupId = 0; + this.updateToolbar(0, "yellow-toolbar-selected"); + } + }, + + // Show or hide preview + showPreview: function(elementText, elementPreview) { + if (!yellow.toolbox.isVisible(elementPreview)) { + var thisObject = this; + var formData = new FormData(); + formData.append("action", "preview"); + formData.append("csrftoken", this.getCookie("csrftoken")); + formData.append("rawdataedit", elementText.value); + formData.append("rawdataendofline", yellow.page.rawDataEndOfLine); + var request = new XMLHttpRequest(); + request.open("POST", window.location.pathname, true); + request.onload = function() { if (this.status==200) thisObject.showPreviewDone.call(thisObject, elementText, elementPreview, this.responseText); }; + request.send(formData); + } else { + this.showPreviewDone(elementText, elementPreview, ""); + } + }, + + // Preview done + showPreviewDone: function(elementText, elementPreview, responseText) { + var showPreview = responseText.length!=0; + yellow.toolbox.setVisible(elementText, !showPreview); + yellow.toolbox.setVisible(elementPreview, showPreview); + if (showPreview) { + this.updateToolbar("preview", "yellow-toolbar-checked"); + elementPreview.innerHTML = responseText; + dispatchEvent(new Event("load")); + } else { + this.updateToolbar(0, "yellow-toolbar-checked"); + elementText.focus(); + } + }, + + // Show file dialog and trigger upload + showFileDialog: function() { + var element = document.createElement("input"); + element.setAttribute("id", "yellow-file-dialog"); + element.setAttribute("type", "file"); + element.setAttribute("accept", yellow.system.editUploadExtensions); + element.setAttribute("multiple", "multiple"); + yellow.toolbox.addEvent(element, "change", yellow.onDrop); + element.click(); + }, + + // Upload file + uploadFile: function(elementText, file) { + 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.serverFileSizeMax && extensions.indexOf(extension)!=-1) { + var text = this.getText("UploadProgress")+"\u200b"; + yellow.editor.setMarkdown(elementText, text, "insert"); + var thisObject = this; + var formData = new FormData(); + formData.append("action", "upload"); + formData.append("csrftoken", this.getCookie("csrftoken")); + formData.append("file", file); + var request = new XMLHttpRequest(); + request.open("POST", window.location.pathname, true); + request.onload = function() { if (this.status==200) { thisObject.uploadFileDone.call(thisObject, elementText, this.responseText); } else { thisObject.uploadFileError.call(thisObject, elementText, this.responseText); } }; + request.send(formData); + } + }, + + // Upload done + uploadFileDone: function(elementText, responseText) { + var result = JSON.parse(responseText); + if (result) { + var textOld = this.getText("UploadProgress")+"\u200b"; + var textNew; + if (result.location.substring(0, yellow.system.imageLocation.length)==yellow.system.imageLocation) { + textNew = "[image "+result.location.substring(yellow.system.imageLocation.length)+"]"; + } else { + textNew = "[link]("+result.location+")"; + } + yellow.editor.replace(elementText, textOld, textNew); + } + }, + + // Upload error + uploadFileError: function(elementText, responseText) { + var result = JSON.parse(responseText); + if (result) { + var textOld = this.getText("UploadProgress")+"\u200b"; + var textNew = "["+result.error+"]"; + yellow.editor.replace(elementText, textOld, textNew); + } + }, + + // Bind actions to links + bindActions: function(element) { + var elements = element.getElementsByTagName("a"); + for (var i=0, l=elements.length; i<l; i++) { + 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(); }; + } + }, + + // Return action + getAction: function(paneId, paneAction) { + var action = ""; + if (paneId=="yellow-pane-edit") { + switch (paneAction) { + case "create": action = "create"; break; + case "edit": action = document.getElementById("yellow-pane-edit-text").value.length!=0 ? "edit" : "delete"; break; + case "delete": action = "delete"; break; + } + if (yellow.page.statusCode==434 && paneAction!="delete") action = "create"; + } + return action; + }, + + // Return request string + getRequest: function(key, prefix) { + if (!prefix) prefix = "request"; + key = prefix + yellow.toolbox.toUpperFirst(key); + return (key in yellow.page) ? yellow.page[key] : ""; + }, + + // Return text string + 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 cookie string + getCookie: function(name) { + return yellow.toolbox.getCookie(name); + }, + + // Check if extension exists + isExtension: function(name) { + return name in yellow.system.serverExtensions; + } +}; + +yellow.editor = { + + // Set Markdown formatting + setMarkdown: function(element, prefix, type, toggle, callback) { + var information = this.getMarkdownInformation(element, prefix, type); + var selectionStart = (information.type.indexOf("block")!=-1) ? information.top : information.start; + var selectionEnd = (information.type.indexOf("block")!=-1) ? information.bottom : information.end; + if (information.found && toggle) information.type = information.type.replace("insert", "remove"); + if (information.type=="remove-fenced-block" || information.type=="remove-inline") { + selectionStart -= information.prefix.length; selectionEnd += information.prefix.length; + } + var text = information.text; + var textSelectionBefore = text.substring(0, selectionStart); + var textSelection = text.substring(selectionStart, selectionEnd); + var textSelectionAfter = text.substring(selectionEnd, text.length); + var textSelectionNew, selectionStartNew, selectionEndNew; + switch (information.type) { + case "insert-multiline-block": + textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information); + selectionStartNew = information.start + this.getMarkdownDifference(textSelection, textSelectionNew, true); + selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew); + if (information.start==information.top && information.start!=information.end) selectionStartNew = information.top; + if (information.end==information.top && information.start!=information.end) selectionEndNew = information.top; + break; + case "remove-multiline-block": + textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information); + selectionStartNew = information.start + this.getMarkdownDifference(textSelection, textSelectionNew, true); + selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew); + if (selectionStartNew<=information.top) selectionStartNew = information.top; + if (selectionEndNew<=information.top) selectionEndNew = information.top; + break; + case "insert-fenced-block": + textSelectionNew = this.getMarkdownFencedBlock(textSelection, information); + selectionStartNew = information.start + information.prefix.length; + selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew) - information.prefix.length; + break; + case "remove-fenced-block": + textSelectionNew = this.getMarkdownFencedBlock(textSelection, information); + selectionStartNew = information.start - information.prefix.length; + selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew) + information.prefix.length; + break; + case "insert-inline": + textSelectionNew = information.prefix + textSelection + information.prefix; + selectionStartNew = information.start + information.prefix.length; + selectionEndNew = information.end + information.prefix.length; + break; + case "remove-inline": + textSelectionNew = text.substring(information.start, information.end); + selectionStartNew = information.start - information.prefix.length; + selectionEndNew = information.end - information.prefix.length; + break; + case "insert": + textSelectionNew = callback ? callback(textSelection, information) : information.prefix; + selectionStartNew = information.start + textSelectionNew.length; + selectionEndNew = selectionStartNew; + } + if (textSelection!=textSelectionNew || selectionStart!=selectionStartNew || selectionEnd!=selectionEndNew) { + element.focus(); + element.setSelectionRange(selectionStart, selectionEnd); + document.execCommand("insertText", false, textSelectionNew); + element.value = textSelectionBefore + textSelectionNew + textSelectionAfter; + element.setSelectionRange(selectionStartNew, selectionEndNew); + } + if (yellow.system.debug) console.log("yellow.editor.setMarkdown type:"+information.type); + }, + + // Return Markdown formatting information + getMarkdownInformation: function(element, prefix, type) { + var text = element.value; + var start = element.selectionStart; + var end = element.selectionEnd; + var top = start, bottom = end; + while (text.charAt(top-1)!="\n" && top>0) top--; + if (bottom==top && bottom<text.length) bottom++; + while (text.charAt(bottom-1)!="\n" && bottom<text.length) bottom++; + if (type=="insert-autodetect") { + if (text.substring(start, end).indexOf("\n")!=-1) { + type = "insert-fenced-block"; prefix = "```\n"; + } else { + type = "insert-inline"; prefix = "`"; + } + } + var found = false; + if (type.indexOf("multiline-block")!=-1) { + if (text.substring(top, top+prefix.length)==prefix) found = true; + } else if (type.indexOf("fenced-block")!=-1) { + if (text.substring(top-prefix.length, top)==prefix && text.substring(bottom, bottom+prefix.length)==prefix) { + found = true; + } + } else { + if (text.substring(start-prefix.length, start)==prefix && text.substring(end, end+prefix.length)==prefix) { + if (prefix=="*") { + var lettersBefore = 0, lettersAfter = 0; + for (var index=start-1; text.charAt(index)=="*"; index--) lettersBefore++; + for (var index=end; text.charAt(index)=="*"; index++) lettersAfter++; + found = lettersBefore!=2 && lettersAfter!=2; + } else { + found = true; + } + } + } + return { "text":text, "prefix":prefix, "type":type, "start":start, "end":end, "top":top, "bottom":bottom, "found":found }; + }, + + // Return Markdown length difference + getMarkdownDifference: function(textSelection, textSelectionNew, firstTextLine) { + var textSelectionLength, textSelectionLengthNew; + if (firstTextLine) { + var position = textSelection.indexOf("\n"); + var positionNew = textSelectionNew.indexOf("\n"); + textSelectionLength = position!=-1 ? position+1 : textSelection.length+1; + textSelectionLengthNew = positionNew!=-1 ? positionNew+1 : textSelectionNew.length+1; + } else { + var position = textSelection.indexOf("\n"); + var positionNew = textSelectionNew.indexOf("\n"); + textSelectionLength = position!=-1 ? textSelection.length : textSelection.length+1; + textSelectionLengthNew = positionNew!=-1 ? textSelectionNew.length : textSelectionNew.length+1; + } + return textSelectionLengthNew - textSelectionLength; + }, + + // Return Markdown for multiline block + getMarkdownMultilineBlock: function(textSelection, information) { + var textSelectionNew = ""; + var lines = yellow.toolbox.getTextLines(textSelection); + for (var i=0; i<lines.length; i++) { + var matches = lines[i].match(/^(\s*[\#\*\-\>\s]+)?(\s+\[.\]|\s*\d+\.)?[ \t]+/); + if (matches) { + textSelectionNew += lines[i].substring(matches[0].length); + } else { + textSelectionNew += lines[i]; + } + } + textSelection = textSelectionNew; + if (information.type.indexOf("remove")==-1) { + textSelectionNew = ""; + var linePrefix = information.prefix; + lines = yellow.toolbox.getTextLines(textSelection.length!=0 ? textSelection : "\n"); + for (var i=0; i<lines.length; i++) { + textSelectionNew += linePrefix+lines[i]; + if (information.prefix=="1. ") { + var matches = linePrefix.match(/^(\d+)\.\s/); + if (matches) linePrefix = (parseInt(matches[1])+1)+". "; + } + } + textSelection = textSelectionNew; + } + return textSelection; + }, + + // Return Markdown for fenced block + getMarkdownFencedBlock: function(textSelection, information) { + var textSelectionNew = ""; + var lines = yellow.toolbox.getTextLines(textSelection); + for (var i=0; i<lines.length; i++) { + var matches = lines[i].match(/^```/); + if (!matches) textSelectionNew += lines[i]; + } + textSelection = textSelectionNew; + if (information.type.indexOf("remove")==-1) { + if (textSelection.length==0) textSelection = "\n"; + textSelection = information.prefix + textSelection + information.prefix; + } + return textSelection; + }, + + // Return Markdown for link + getMarkdownLink: function(textSelection, information) { + return textSelection.length!=0 ? information.prefix.replace("link", textSelection) : information.prefix; + }, + + // Set meta data + setMetaData: function(element, key, value, toggle) { + var information = this.getMetaDataInformation(element, key); + if (information.bottom!=0) { + var selectionStart = information.found ? information.start : information.bottom; + var selectionEnd = information.found ? information.end : information.bottom; + var text = information.text; + var textSelectionBefore = text.substring(0, selectionStart); + var textSelection = text.substring(selectionStart, selectionEnd); + var textSelectionAfter = text.substring(selectionEnd, text.length); + var textSelectionNew = yellow.toolbox.toUpperFirst(key)+": "+value+"\n"; + if (information.found && information.value==value && toggle) textSelectionNew = ""; + var selectionStartNew = selectionStart; + var selectionEndNew = selectionStart + textSelectionNew.trim().length; + element.focus(); + element.setSelectionRange(selectionStart, selectionEnd); + document.execCommand("insertText", false, textSelectionNew); + element.value = textSelectionBefore + textSelectionNew + textSelectionAfter; + element.setSelectionRange(selectionStartNew, selectionEndNew); + element.scrollTop = 0; + if (yellow.system.debug) console.log("yellow.editor.setMetaData key:"+key); + } + }, + + // Return meta data information + getMetaDataInformation: function(element, key) { + var text = element.value; + var value = ""; + var start = 0, end = 0, top = 0, bottom = 0; + var found = false; + var parts = text.match(/^(\xEF\xBB\xBF)?(\-\-\-[\r\n]+)([\s\S]+?)\-\-\-[\r\n]+/); + if (parts) { + key = yellow.toolbox.toLowerFirst(key); + start = end = top = ((parts[1] ? parts[1] : "")+parts[2]).length; + bottom = ((parts[1] ? parts[1] : "")+parts[2]+parts[3]).length; + var lines = yellow.toolbox.getTextLines(parts[3]); + for (var i=0; i<lines.length; i++) { + var matches = lines[i].match(/^\s*(.*?)\s*:\s*(.*?)\s*$/); + if (matches && yellow.toolbox.toLowerFirst(matches[1])==key && matches[2].length!=0) { + value = matches[2]; + end = start + lines[i].length; + found = true; + break; + } + start = end = start + lines[i].length; + } + } + return { "text":text, "value":value, "start":start, "end":end, "top":top, "bottom":bottom, "found":found }; + }, + + // Replace text + replace: function(element, textOld, textNew) { + var text = element.value; + var selectionStart = element.selectionStart; + var selectionEnd = element.selectionEnd; + var selectionStartFound = text.indexOf(textOld); + var selectionEndFound = selectionStartFound + textOld.length; + if (selectionStartFound!=-1) { + var selectionStartNew = selectionStart<selectionStartFound ? selectionStart : selectionStart+textNew.length-textOld.length; + var selectionEndNew = selectionEnd<selectionEndFound ? selectionEnd : selectionEnd+textNew.length-textOld.length; + var textBefore = text.substring(0, selectionStartFound); + var textAfter = text.substring(selectionEndFound, text.length); + if (textOld!=textNew) { + element.focus(); + element.setSelectionRange(selectionStartFound, selectionEndFound); + document.execCommand("insertText", false, textNew); + element.value = textBefore + textNew + textAfter; + element.setSelectionRange(selectionStartNew, selectionEndNew); + } + } + }, + + // Undo changes + undo: function() { + document.execCommand("undo"); + }, + + // Redo changes + redo: function() { + document.execCommand("redo"); + } +}; + +yellow.toolbox = { + + // Insert element before reference element + insertBefore: function(element, elementReference) { + elementReference.parentNode.insertBefore(element, elementReference); + }, + + // Insert element after reference element + insertAfter: function(element, elementReference) { + elementReference.parentNode.insertBefore(element, elementReference.nextSibling); + }, + + // Add element class + addClass: function(element, name) { + element.classList.add(name); + }, + + // Remove element class + removeClass: function(element, name) { + element.classList.remove(name); + }, + + // Add attribute information + addValue: function(selector, name, value) { + var element = document.querySelector(selector); + element.setAttribute(name, element.getAttribute(name) + value); + }, + + // Remove attribute information + removeValue: function(selector, name, value) { + var element = document.querySelector(selector); + element.setAttribute(name, element.getAttribute(name).replace(value, "")); + }, + + // Add event handler + addEvent: function(element, type, handler) { + element.addEventListener(type, handler, false); + }, + + // Remove event handler + removeEvent: function(element, type, handler) { + element.removeEventListener(type, handler, false); + }, + + // Return shortcut from keyboard event, alphanumeric only + getEventShortcut: function(e) { + var shortcut = ""; + if (e.keyCode>=48 && e.keyCode<=90) { + shortcut += (e.ctrlKey ? "ctrl+" : "")+(e.metaKey ? "meta+" : "")+(e.altKey ? "alt+" : "")+(e.shiftKey ? "shift+" : ""); + shortcut += String.fromCharCode(e.keyCode).toLowerCase(); + } + return shortcut; + }, + + // Return element width in pixel + getWidth: function(element) { + return element.offsetWidth - this.getBoxSize(element).width; + }, + + // Return element height in pixel + getHeight: function(element) { + return element.offsetHeight - this.getBoxSize(element).height; + }, + + // Set element width in pixel, including padding and border + setOuterWidth: function(element, width) { + element.style.width = Math.max(0, width - this.getBoxSize(element).width) + "px"; + }, + + // Set element height in pixel, including padding and border + setOuterHeight: function(element, height) { + element.style.height = Math.max(0, height - this.getBoxSize(element).height) + "px"; + }, + + // Return element width in pixel, including padding and border + getOuterWidth: function(element, includeMargin) { + var width = element.offsetWidth; + if (includeMargin) width += this.getMarginSize(element).width; + return width; + }, + + // Return element height in pixel, including padding and border + getOuterHeight: function(element, includeMargin) { + var height = element.offsetHeight; + if (includeMargin) height += this.getMarginSize(element).height; + return height; + }, + + // Set element left position in pixel + setOuterLeft: function(element, left) { + element.style.left = Math.max(0, left) + "px"; + }, + + // Set element top position in pixel + setOuterTop: function(element, top) { + element.style.top = Math.max(0, top) + "px"; + }, + + // Return element left position in pixel + getOuterLeft: function(element) { + return element.getBoundingClientRect().left + window.pageXOffset; + }, + + // Return element top position in pixel + getOuterTop: function(element) { + return element.getBoundingClientRect().top + window.pageYOffset; + }, + + // Return window width in pixel + getWindowWidth: function() { + return window.innerWidth; + }, + + // Return window height in pixel + getWindowHeight: function() { + return window.innerHeight; + }, + + // Return element CSS property + getStyle: function(element, property) { + return window.getComputedStyle(element).getPropertyValue(property); + }, + + // Return element CSS padding and border + getBoxSize: function(element) { + var paddingLeft = parseFloat(this.getStyle(element, "padding-left")) || 0; + var paddingRight = parseFloat(this.getStyle(element, "padding-right")) || 0; + var borderLeft = parseFloat(this.getStyle(element, "border-left-width")) || 0; + var borderRight = parseFloat(this.getStyle(element, "border-right-width")) || 0; + var width = paddingLeft + paddingRight + borderLeft + borderRight; + var paddingTop = parseFloat(this.getStyle(element, "padding-top")) || 0; + var paddingBottom = parseFloat(this.getStyle(element, "padding-bottom")) || 0; + var borderTop = parseFloat(this.getStyle(element, "border-top-width")) || 0; + var borderBottom = parseFloat(this.getStyle(element, "border-bottom-width")) || 0; + var height = paddingTop + paddingBottom + borderTop + borderBottom; + return { "width":width, "height":height }; + }, + + // Return element CSS margin + getMarginSize: function(element) { + var marginLeft = parseFloat(this.getStyle(element, "margin-left")) || 0; + var marginRight = parseFloat(this.getStyle(element, "margin-right")) || 0; + var width = marginLeft + marginRight; + var marginTop = parseFloat(this.getStyle(element, "margin-top")) || 0; + var marginBottom = parseFloat(this.getStyle(element, "margin-bottom")) || 0; + var height = marginTop + marginBottom; + return { "width":width, "height":height }; + }, + + // Set element visibility + setVisible: function(element, show, fadeout) { + if (fadeout && !show) { + var opacity = 1; + function renderFrame() { + opacity -= .1; + if (opacity<=0) { + element.style.opacity = "initial"; + element.style.display = "none"; + } else { + element.style.opacity = opacity; + requestAnimationFrame(renderFrame); + } + } + renderFrame(); + } else { + element.style.display = show ? "block" : "none"; + } + }, + + // Check if element exists and is visible + isVisible: function(element) { + return element && element.style.display!="none"; + }, + + // Convert first letter to lowercase + toLowerFirst: function(string) { + return string.charAt(0).toLowerCase()+string.slice(1); + }, + + // Convert first letter to uppercase + toUpperFirst: function(string) { + return string.charAt(0).toUpperCase()+string.slice(1); + }, + + // Return lines from text string, including newline + getTextLines: function(string) { + var lines = string.split("\n"); + for (var i=0; i<lines.length; i++) lines[i] = lines[i]+"\n"; + if (string.length==0 || string.charAt(string.length-1)=="\n") lines.pop(); + return lines; + }, + + // Return cookie string + getCookie: function(name) { + var matches = document.cookie.match("(^|; )"+name+"=([^;]+)"); + return matches ? unescape(matches[2]) : ""; + }, + + // Encode HTML special characters + encodeHtml: function(string) { + return string + .replace(/&/g, "&amp;") + .replace(/</g, "&lt;") + .replace(/>/g, "&gt;") + .replace(/"/g, "&quot;"); + }, + + // Submit form with post method + submitForm: function(args) { + var elementForm = document.createElement("form"); + elementForm.setAttribute("method", "post"); + for (var key in args) { + if (!args.hasOwnProperty(key)) continue; + var elementInput = document.createElement("input"); + elementInput.setAttribute("type", "hidden"); + elementInput.setAttribute("name", key); + elementInput.setAttribute("value", args[key]); + elementForm.appendChild(elementInput); + } + document.body.appendChild(elementForm); + elementForm.submit(); + } +}; + +yellow.edit.intervalId = setInterval("yellow.onLoad()", 1); diff --git a/system/extensions/edit.php b/system/extensions/edit.php @@ -0,0 +1,1862 @@ +<?php +// Edit extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/edit +// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +class YellowEdit { + const VERSION = "0.8.2"; + const TYPE = "feature"; + public $yellow; //access to API + public $response; //web response + public $users; //user accounts + public $merge; //text merge + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->response = new YellowResponse($yellow); + $this->users = new YellowUsers($yellow); + $this->merge = new YellowMerge($yellow); + $this->yellow->system->setDefault("editLocation", "/edit/"); + $this->yellow->system->setDefault("editUploadNewLocation", "/media/@group/@filename"); + $this->yellow->system->setDefault("editUploadExtensions", ".gif, .jpg, .pdf, .png, .svg, .tgz, .zip"); + $this->yellow->system->setDefault("editKeyboardShortcuts", "ctrl+b bold, ctrl+i italic, ctrl+e code, ctrl+k link, ctrl+s save, ctrl+shift+p preview"); + $this->yellow->system->setDefault("editToolbarButtons", "auto"); + $this->yellow->system->setDefault("editEndOfLine", "auto"); + $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("editUserHome", "/"); + $this->yellow->system->setDefault("editNewFile", "page-new-(.*).md"); + $this->yellow->system->setDefault("editLoginRestrictions", "0"); + $this->yellow->system->setDefault("editLoginSessionTimeout", "2592000"); + $this->yellow->system->setDefault("editBruteForceProtection", "25"); + $this->users->load($this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile")); + } + + // Handle startup + public function onStartup($update) { + if ($update) { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $fileData = $this->yellow->toolbox->readFile($fileNameUser); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (!empty($matches[1]) && !empty($matches[2]) && $matches[1][0]!="#") { + list($hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) = explode(",", $matches[2]); + if ($status!="active" && $status!="inactive") { + unset($this->users->users[$matches[1]]); + continue; + } + $pending = "none"; + $this->users->set($matches[1], $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home); + $fileDataNew .= "$matches[1]: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n"; + } else { + $fileDataNew .= $line; + } + } + if ($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileNameUser, $fileDataNew); + } + } + + // Handle request + public function onRequest($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->checkRequest($location)) { + $scheme = $this->yellow->system->get("serverScheme"); + $address = $this->yellow->system->get("serverAddress"); + $base = rtrim($this->yellow->system->get("serverBase").$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); + $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName); + } + return $statusCode; + } + + // Handle page meta data + public function onParseMeta($page) { + if ($page==$this->yellow->page && $this->response->isActive()) { + if ($this->response->isUser()) { + if (empty($this->response->rawDataSource)) $this->response->rawDataSource = $page->rawData; + if (empty($this->response->rawDataEdit)) $this->response->rawDataEdit = $page->rawData; + if (empty($this->response->rawDataEndOfLine)) $this->response->rawDataEndOfLine = $this->response->getEndOfLine($page->rawData); + if ($page->statusCode==434) $this->response->rawDataEdit = $this->response->getRawDataNew($page, true); + } + if (empty($this->response->language)) $this->response->language = $page->get("language"); + if (empty($this->response->action)) $this->response->action = $this->response->isUser() ? "none" : "login"; + if (empty($this->response->status)) $this->response->status = "none"; + if ($this->response->status=="error") $this->response->action = "error"; + } + } + + // 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>"; + } + return $output; + } + + // Handle page extra data + public function onParsePageExtra($page, $name) { + $output = null; + if ($name=="header" && $this->response->isActive()) { + $extensionLocation = $this->yellow->system->get("serverBase").$this->yellow->system->get("extensionLocation"); + $output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" data-bundle=\"none\" href=\"{$extensionLocation}edit.css\" />\n"; + $output .= "<script type=\"text/javascript\" data-bundle=\"none\" src=\"{$extensionLocation}edit.js\"></script>\n"; + $output .= "<script type=\"text/javascript\">\n"; + $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 .= "// ]]>\n"; + $output .= "</script>\n"; + } + return $output; + } + + // Handle command + public function onCommand($args) { + list($command) = $args; + switch ($command) { + case "user": $statusCode = $this->processCommandUser($args); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + return "user [option email password name]\n"; + } + + // Process command to update user account + public function processCommandUser($args) { + list($command, $option) = $args; + switch ($option) { + case "": $statusCode = $this->userShow($args); break; + case "add": $statusCode = $this->userAdd($args); break; + case "change": $statusCode = $this->userChange($args); break; + case "remove": $statusCode = $this->userRemove($args); break; + default: $statusCode = 400; echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Show user accounts + public function userShow($args) { + list($command) = $args; + foreach ($this->users->getData() as $line) { + echo "$line\n"; + } + if (!$this->users->getNumber()) echo "Yellow $command: No user accounts\n"; + return 200; + } + + // Add user account + public function userAdd($args) { + $status = "ok"; + list($command, $option, $email, $password, $name) = $args; + if (empty($email) || empty($password)) $status = $this->response->status = "incomplete"; + if ($status=="ok") $status = $this->getUserAccount($email, $password, "add"); + if ($status=="ok" && $this->users->isTaken($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; + case "taken": echo "ERROR updating settings: Please enter a different email!\n"; break; + case "weak": echo "ERROR updating settings: Please enter a different password!\n"; break; + } + if ($status=="ok") { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $status = $this->users->save($fileNameUser, $email, $password, $name, "", "active") ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; + } + if ($status=="ok") { + $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); + $status = substru($this->users->getHash($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; + echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."added\n"; + return $statusCode; + } + + // Change user account + public function userChange($args) { + $status = "ok"; + list($command, $option, $email, $password, $name) = $args; + 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"; + 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; + case "weak": echo "ERROR updating settings: Please enter a different password!\n"; break; + } + if ($status=="ok") { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $status = $this->users->save($fileNameUser, $email, $password, $name) ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; + } + $statusCode = $status=="ok" ? 200 : 500; + echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."changed\n"; + return $statusCode; + } + + // Remove user account + public function userRemove($args) { + $status = "ok"; + list($command, $option, $email) = $args; + if (empty($email)) $status = $this->response->status = "invalid"; + if ($status=="ok") $status = $this->getUserAccount($email, "", "remove"); + if ($status=="ok" && !$this->users->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("settingDir").$this->yellow->system->get("editUserFile"); + $status = $this->users->remove($fileNameUser, $email) ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; + } + $statusCode = $status=="ok" ? 200 : 500; + echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."removed\n"; + return $statusCode; + } + + // Process request + public function processRequest($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->checkUserAuth($scheme, $address, $base, $location, $fileName)) { + switch ($_REQUEST["action"]) { + case "": $statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break; + case "login": $statusCode = $this->processRequestLogin($scheme, $address, $base, $location, $fileName); break; + case "logout": $statusCode = $this->processRequestLogout($scheme, $address, $base, $location, $fileName); break; + case "settings": $statusCode = $this->processRequestSettings($scheme, $address, $base, $location, $fileName); break; + case "version": $statusCode = $this->processRequestVersion($scheme, $address, $base, $location, $fileName); break; + case "update": $statusCode = $this->processRequestUpdate($scheme, $address, $base, $location, $fileName); break; + case "quit": $statusCode = $this->processRequestQuit($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 "preview": $statusCode = $this->processRequestPreview($scheme, $address, $base, $location, $fileName); break; + case "upload": $statusCode = $this->processRequestUpload($scheme, $address, $base, $location, $fileName); break; + } + } elseif ($this->checkUserUnauth($scheme, $address, $base, $location, $fileName)) { + $this->yellow->lookup->requestHandler = "core"; + switch ($_REQUEST["action"]) { + case "": $statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break; + case "signup": $statusCode = $this->processRequestSignup($scheme, $address, $base, $location, $fileName); break; + case "forgot": $statusCode = $this->processRequestForgot($scheme, $address, $base, $location, $fileName); break; + case "confirm": $statusCode = $this->processRequestConfirm($scheme, $address, $base, $location, $fileName); break; + case "approve": $statusCode = $this->processRequestApprove($scheme, $address, $base, $location, $fileName); break; + case "recover": $statusCode = $this->processRequestRecover($scheme, $address, $base, $location, $fileName); break; + case "reactivate": $statusCode = $this->processRequestReactivate($scheme, $address, $base, $location, $fileName); break; + case "verify": $statusCode = $this->processRequestVerify($scheme, $address, $base, $location, $fileName); break; + case "change": $statusCode = $this->processRequestChange($scheme, $address, $base, $location, $fileName); break; + case "remove": $statusCode = $this->processRequestRemove($scheme, $address, $base, $location, $fileName); break; + } + } + if ($statusCode==0) $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + $this->checkUserFailed($scheme, $address, $base, $location, $fileName); + return $statusCode; + } + + // Process request to show file + public function processRequestShow($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if (is_readable($fileName)) { + $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->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(301, $location); + } else { + $this->yellow->page->error($this->response->isUserRestrictions() ? 404 : 434); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request for user login + public function processRequestLogin($scheme, $address, $base, $location, $fileName) { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + if ($this->users->save($fileNameUser, $this->response->userEmail)) { + $home = $this->users->getHome($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); + } else { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $home); + $statusCode = $this->yellow->sendStatus(302, $location); + } + } else { + $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + return $statusCode; + } + + // Process request for user logout + public function processRequestLogout($scheme, $address, $base, $location, $fileName) { + $this->response->userEmail = ""; + $this->response->destroyCookies($scheme, $address, $base); + $location = $this->yellow->lookup->normaliseUrl( + $this->yellow->system->get("serverScheme"), + $this->yellow->system->get("serverAddress"), + $this->yellow->system->get("serverBase"), + $location); + $statusCode = $this->yellow->sendStatus(302, $location); + return $statusCode; + } + + // Process request for user signup + public function processRequestSignup($scheme, $address, $base, $location, $fileName) { + $this->response->action = "signup"; + $this->response->status = "ok"; + $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"])); + $email = trim($_REQUEST["email"]); + $password = trim($_REQUEST["password"]); + $consent = trim($_REQUEST["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 ($this->response->status=="ok" && $this->response->isLoginRestrictions()) $this->response->status = "next"; + if ($this->response->status=="ok" && $this->users->isTaken($email)) $this->response->status = "next"; + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $email, $password, $name, "", "unconfirmed") ? "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->getHash($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") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "confirm") ? "next" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to confirm user signup + public function processRequestConfirm($scheme, $address, $base, $location, $fileName) { + $this->response->action = "confirm"; + $this->response->status = "ok"; + $email = $_REQUEST["email"]; + $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "unapproved") ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "approve") ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to approve user signup + public function processRequestApprove($scheme, $address, $base, $location, $fileName) { + $this->response->action = "approve"; + $this->response->status = "ok"; + $email = $_REQUEST["email"]; + $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "welcome") ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request for forgotten password + public function processRequestForgot($scheme, $address, $base, $location, $fileName) { + $this->response->action = "forgot"; + $this->response->status = "ok"; + $email = trim($_REQUEST["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->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!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to recover password + public function processRequestRecover($scheme, $address, $base, $location, $fileName) { + $this->response->action = "recover"; + $this->response->status = "ok"; + $email = trim($_REQUEST["email"]); + $password = trim($_REQUEST["password"]); + $this->response->status = $this->getUserStatus($email, $_REQUEST["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 ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $email, $password) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $this->response->destroyCookies($scheme, $address, $base); + $this->response->status = "done"; + } + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to reactivate account + public function processRequestReactivate($scheme, $address, $base, $location, $fileName) { + $this->response->action = "reactivate"; + $this->response->status = "ok"; + $email = $_REQUEST["email"]; + $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "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); + return $statusCode; + } + + // Process request to change settings + public function processRequestSettings($scheme, $address, $base, $location, $fileName) { + $this->response->action = "settings"; + $this->response->status = "ok"; + $email = trim($_REQUEST["email"]); + $emailSource = $this->response->userEmail; + $password = trim($_REQUEST["password"]); + $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"])); + $language = trim($_REQUEST["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 ($this->response->status=="ok" && $email!=$emailSource) { + $pending = $emailSource; + $home = $this->users->getHome($emailSource); + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $email, "no", $name, $language, "unverified", "", "", "", $pending, $home) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $pending = $email.":".(empty($password) ? $this->users->getHash($emailSource) : $this->users->createHash($password)); + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $emailSource, "", $name, $language, "", "", "", "", $pending) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $action = $email!=$emailSource ? "verify" : "change"; + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, $action) ? "next" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + } else { + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $email, "", $name, $language) ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + } + if ($this->response->status=="done") { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + return $statusCode; + } + + // Process request to verify email + public function processRequestVerify($scheme, $address, $base, $location, $fileName) { + $this->response->action = "verify"; + $this->response->status = "ok"; + $email = $emailSource = $_REQUEST["email"]; + $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); + if ($this->response->status=="ok") { + $emailSource = $this->users->getPending($email); + if ($this->users->getStatus($emailSource)!="active") $this->response->status = "done"; + } + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "unchanged") ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $emailSource, "change") ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to change email or password + public function processRequestChange($scheme, $address, $base, $location, $fileName) { + $this->response->action = "change"; + $this->response->status = "ok"; + $email = $emailSource = trim($_REQUEST["email"]); + $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); + if ($this->response->status=="ok") { + list($email, $hash) = explode(":", $this->users->getPending($email), 2); + if (!$this->users->isExisting($email) || empty($hash)) $this->response->status = "done"; + } + if ($this->response->status=="ok") { + $this->users->users[$email]["hash"] = $hash; + $this->users->users[$email]["pending"] = "none"; + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "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("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->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") { + $this->response->destroyCookies($scheme, $address, $base); + $this->response->status = "done"; + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to show version + public function processRequestVersion($scheme, $address, $base, $location, $fileName) { + $this->response->action = "version"; + $this->response->status = "ok"; + 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); + if ($this->response->isUserWebmaster()) { + foreach ($dataCurrent as $key=>$value) { + if (strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0) { + ++$updates; + $rawData = htmlspecialchars(ucfirst($key)." $dataLatest[$key]")."<br />\n"; + $this->response->rawDataOutput .= $rawData; + } + } + if ($updates==0) { + foreach ($dataCurrent as $key=>$value) { + if (!is_null($dataModified[$key]) && !is_null($dataLatest[$key])) { + $rawData = $this->yellow->text->getTextHtml("editVersionUpdateModified", $this->response->language)." - <a href=\"#\" data-action=\"update\" data-status=\"update\" data-args=\"".$this->yellow->toolbox->normaliseArgs("extension:$key/option:force")."\">".$this->yellow->text->getTextHtml("editVersionUpdateForce", $this->response->language)."</a><br />\n"; + $rawData = preg_replace("/@extension/i", htmlspecialchars(ucfirst($key)." $dataLatest[$key]"), $rawData); + $this->response->rawDataOutput .= $rawData; + } + } + } + $this->response->status = $updates ? "updates" : "done"; + } else { + foreach ($dataCurrent as $key=>$value) { + if (strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0) ++$updates; + } + $this->response->status = $updates ? "warning" : "done"; + } + if ($statusCode!=200) $this->response->status = "error"; + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to update website + public function processRequestUpdate($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->yellow->extensions->isExisting("update") && $this->response->isUserWebmaster()) { + $extension = trim($_REQUEST["extension"]); + $option = trim($_REQUEST["option"]); + $statusCode = $this->yellow->command("update", $extension, $option); + if ($statusCode==200) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } + } + return $statusCode; + } + + // Process request to quit account + public function processRequestQuit($scheme, $address, $base, $location, $fileName) { + $this->response->action = "quit"; + $this->response->status = "ok"; + $name = trim($_REQUEST["name"]); + $email = $this->response->userEmail; + if (empty($name)) $this->response->status = "none"; + if ($this->response->status=="ok" && $name!=$this->users->getName($email)) $this->response->status = "mismatch"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, "", $this->response->action); + 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!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to remove account + public function processRequestRemove($scheme, $address, $base, $location, $fileName) { + $this->response->action = "remove"; + $this->response->status = "ok"; + $email = $_REQUEST["email"]; + $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "removed") ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + 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("settingDir").$this->yellow->system->get("editUserFile"); + $this->response->status = $this->users->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") { + $this->response->destroyCookies($scheme, $address, $base); + $this->response->status = "done"; + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + return $statusCode; + } + + // Process request to create page + public function processRequestCreate($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if (!$this->response->isUserRestrictions() && !empty($_REQUEST["rawdataedit"])) { + $this->response->rawDataSource = $_REQUEST["rawdatasource"]; + $this->response->rawDataEdit = $_REQUEST["rawdatasource"]; + $this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"]; + $rawData = $_REQUEST["rawdataedit"]; + $page = $this->response->getPageNew($scheme, $address, $base, $location, $fileName, + $rawData, $this->response->getEndOfLine()); + if (!$page->isError()) { + if ($this->yellow->toolbox->createFile($page->fileName, $page->rawData, true)) { + $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")); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request to edit page + public function processRequestEdit($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if (!$this->response->isUserRestrictions() && !empty($_REQUEST["rawdataedit"])) { + $this->response->rawDataSource = $_REQUEST["rawdatasource"]; + $this->response->rawDataEdit = $_REQUEST["rawdataedit"]; + $this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"]; + $rawDataFile = $this->yellow->toolbox->readFile($fileName); + $page = $this->response->getPageEdit($scheme, $address, $base, $location, $fileName, + $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); + } + } 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); + } + } + } else { + $this->yellow->page->error(500, $page->get("pageError")); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request to delete page + public function processRequestDelete($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if (!$this->response->isUserRestrictions() && is_file($fileName)) { + $this->response->rawDataSource = $_REQUEST["rawdatasource"]; + $this->response->rawDataEdit = $_REQUEST["rawdatasource"]; + $this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"]; + $rawDataFile = $this->yellow->toolbox->readFile($fileName); + $page = $this->response->getPageDelete($scheme, $address, $base, $location, $fileName, + $rawDataFile, $this->response->rawDataEndOfLine); + if (!$page->isError()) { + if ($this->yellow->lookup->isFileLocation($location)) { + if ($this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("trashDir"))) { + $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 { + if ($this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->system->get("trashDir"))) { + $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")); + $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, + $_REQUEST["rawdataedit"], $_REQUEST["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"; + } + return $statusCode; + } + + // Process request to upload file + public function processRequestUpload($scheme, $address, $base, $location, $fileName) { + $data = array(); + $fileNameTemp = $_FILES["file"]["tmp_name"]; + $fileNameShort = preg_replace("/[^\pL\d\-\.]/u", "-", basename($_FILES["file"]["name"])); + $fileSizeMax = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize")); + $extension = strtoloweru(($pos = strrposu($fileNameShort, ".")) ? substru($fileNameShort, $pos) : ""); + $extensions = preg_split("/\s*,\s*/", $this->yellow->system->get("editUploadExtensions")); + if (!$this->response->isUserRestrictions() && is_uploaded_file($fileNameTemp) && + filesize($fileNameTemp)<=$fileSizeMax && in_array($extension, $extensions)) { + $file = $this->response->getFileUpload($scheme, $address, $base, $location, $fileNameTemp, $fileNameShort); + if (!$file->isError() && $this->yellow->toolbox->copyFile($fileNameTemp, $file->fileName, true)) { + $data["location"] = $file->getLocation(); + } else { + $data["error"] = "Can't write file '$file->fileName'!"; + } + } else { + $data["error"] = "Can't write file '$fileNameShort'!"; + } + $statusCode = $this->yellow->sendData(is_null($data["error"]) ? 200 : 500, 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(); + } + + // Check user authentication + public function checkUserAuth($scheme, $address, $base, $location, $fileName) { + if ($this->isRequestSameSite("POST", $scheme, $address) || $_REQUEST["action"]=="") { + if ($_REQUEST["action"]=="login") { + $email = $_REQUEST["email"]; + $password = $_REQUEST["password"]; + if ($this->users->checkAuthLogin($email, $password)) { + $this->response->createCookies($scheme, $address, $base, $email); + $this->response->userEmail = $email; + $this->response->userRestrictions = $this->getUserRestrictions($email, $location, $fileName); + $this->response->language = $this->getUserLanguage($email); + } else { + $this->response->userFailedError = "login"; + $this->response->userFailedEmail = $email; + $this->response->userFailedExpire = PHP_INT_MAX; + } + } elseif (isset($_COOKIE["authtoken"]) && isset($_COOKIE["csrftoken"])) { + if ($this->users->checkAuthToken($_COOKIE["authtoken"], $_COOKIE["csrftoken"], $_POST["csrftoken"], $_REQUEST["action"]=="")) { + $this->response->userEmail = $email = $this->users->getAuthEmail($_COOKIE["authtoken"]); + $this->response->userRestrictions = $this->getUserRestrictions($email, $location, $fileName); + $this->response->language = $this->getUserLanguage($email); + } else { + $this->response->userFailedError = "auth"; + $this->response->userFailedEmail = $this->users->getAuthEmail($_COOKIE["authtoken"]); + $this->response->userFailedExpire = $this->users->getAuthExpire($_COOKIE["authtoken"]); + } + } + } + return $this->response->isUser(); + } + + // Check user without authentication + public function checkUserUnauth($scheme, $address, $base, $location, $fileName) { + $ok = false; + if ($_REQUEST["action"]=="" || $_REQUEST["action"]=="signup" || $_REQUEST["action"]=="forgot") { + $ok = true; + } elseif (isset($_REQUEST["actiontoken"])) { + if ($this->users->checkActionToken($_REQUEST["actiontoken"], $_REQUEST["email"], $_REQUEST["action"], $_REQUEST["expire"])) { + $ok = true; + $this->response->language = $this->getUserLanguage($_REQUEST["email"]); + } else { + $this->response->userFailedError = "action"; + $this->response->userFailedEmail = $_REQUEST["email"]; + $this->response->userFailedExpire = $_REQUEST["expire"]; + } + } + return $ok; + } + + // 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)) { + $email = $this->response->userFailedEmail; + $modified = $this->users->getModified($email); + $errors = $this->users->getErrors($email)+1; + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $status = $this->users->save($fileNameUser, $email, "", "", "", "", "", $modified, $errors) ? "ok" : "error"; + if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + if ($errors==$this->yellow->system->get("editBruteForceProtection")) { + $statusBeforeProtection = $this->users->getStatus($email); + $statusAfterProtection = ($statusBeforeProtection=="active" || $statusBeforeProtection=="inactive") ? "inactive" : "failed"; + if ($status=="ok") { + $status = $this->users->save($fileNameUser, $email, "", "", "", $statusAfterProtection, "", $modified, $errors) ? "ok" : "error"; + if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($status=="ok" && $statusBeforeProtection=="active") { + $status = $this->response->sendMail($scheme, $address, $base, $email, "reactivate") ? "done" : "error"; + if ($status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + } + } + if ($this->response->userFailedError=="login" || $this->response->userFailedError=="auth") { + $this->response->destroyCookies($scheme, $address, $base); + $this->response->status = "error"; + $this->yellow->page->error(430); + } else { + $this->response->status = "error"; + $this->yellow->page->error(500, "Link has expired!"); + } + } + } + + // Return user status changes + public function getUserStatus($email, $action) { + switch ($action) { + case "confirm": $statusExpected = "unconfirmed"; break; + case "approve": $statusExpected = "unapproved"; break; + case "recover": $statusExpected = "active"; break; + case "reactivate": $statusExpected = "inactive"; break; + case "verify": $statusExpected = "unverified"; break; + case "change": $statusExpected = "active"; break; + case "remove": $statusExpected = "active"; break; + } + return $this->users->getStatus($email)==$statusExpected ? "ok" : "done"; + } + + // Return user account changes + public function getUserAccount($email, $password, $action) { + $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); + if (!is_null($status)) break; + } + } + if (is_null($status)) { + $status = "ok"; + if (!empty($password) && strlenu($password)<$this->yellow->system->get("editUserPasswordMinLength")) $status = "weak"; + if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) $status = "invalid"; + } + return $status; + } + + // Return user restrictions + public function getUserRestrictions($email, $location, $fileName) { + $userRestrictions = null; + foreach ($this->yellow->extensions->extensions as $key=>$value) { + if (method_exists($value["obj"], "onEditUserRestrictions")) { + $userRestrictions = $value["obj"]->onEditUserRestrictions($email, $location, $fileName, $this->users); + if (!is_null($userRestrictions)) break; + } + } + if (is_null($userRestrictions)) { + $userRestrictions = substru($location, 0, strlenu($this->users->getHome($email)))!=$this->users->getHome($email); + $userRestrictions |= empty($fileName) || strlenu(dirname($fileName))>128 || strlenu(basename($fileName))>128; + } + return $userRestrictions; + } + + // Return user language + public function getUserLanguage($email) { + $language = $this->users->getLanguage($email); + if (!$this->yellow->text->isLanguage($language)) $language = $this->yellow->system->get("language"); + return $language; + } + + // Check if request came from same site + public function isRequestSameSite($method, $scheme, $address) { + if (preg_match("#^(\w+)://([^/]+)(.*)$#", $_SERVER["HTTP_REFERER"], $matches)) $origin = "$matches[1]://$matches[2]"; + if (isset($_SERVER["HTTP_ORIGIN"])) $origin = $_SERVER["HTTP_ORIGIN"]; + return $_SERVER["REQUEST_METHOD"]==$method && $origin=="$scheme://$address"; + } +} + +class YellowResponse { + public $yellow; //access to API + public $extension; //access to extension + public $active; //location is active? (boolean) + public $userEmail; //user email + public $userRestrictions; //user can change page? (boolean) + 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 $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"); + } + + // Return new page + public function getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0); + $this->editContentFile($page, "create"); + if ($this->yellow->content->find($page->location)) { + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation")); + $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); + while ($this->yellow->content->find($page->location) || empty($page->fileName)) { + $rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData)); + $page->rawData = $this->normaliseLines($rawData, $endOfLine); + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation")); + $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)) { + $page->error(500, "Page '".$page->get("title")."' is not possible!"); + } + } else { + $page->fileName = $this->getPageNewFile($page->location); + } + if ($this->extension->getUserRestrictions($this->userEmail, $page->location, $page->fileName)) { + $page->error(500, "Page '".$page->get("title")."' is restricted!"); + } + return $page; + } + + // Return modified page + public function getPageEdit($scheme, $address, $base, $location, $fileName, $rawDataSource, $rawDataEdit, $rawDataFile, $endOfLine) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $rawData = $this->extension->merge->merge( + $this->normaliseLines($rawDataSource, $endOfLine), + $this->normaliseLines($rawDataEdit, $endOfLine), + $this->normaliseLines($rawDataFile, $endOfLine)); + $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0); + $pageSource = new YellowPage($this->yellow); + $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName); + $pageSource->parseData($this->normaliseLines($rawDataSource, $endOfLine), false, 0); + $this->editContentFile($page, "edit"); + 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->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); + if ($page->location!=$pageSource->location && ($this->yellow->content->find($page->location) || 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 ($this->extension->getUserRestrictions($this->userEmail, $page->location, $page->fileName) || + $this->extension->getUserRestrictions($this->userEmail, $pageSource->location, $pageSource->fileName)) { + $page->error(500, "Page '".$page->get("title")."' is restricted!"); + } + return $page; + } + + // Return deleted page + public function getPageDelete($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0); + $this->editContentFile($page, "delete"); + if ($this->extension->getUserRestrictions($this->userEmail, $page->location, $page->fileName)) { + $page->error(500, "Page '".$page->get("title")."' is restricted!"); + } + return $page; + } + + // Return preview page + public function getPagePreview($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 200); + $this->yellow->text->setLanguage($page->get("language")); + $page->set("pageClass", "page-preview"); + $page->set("pageClass", $page->get("pageClass")." layout-".$page->get("layout")); + $output = "<div class=\"".$page->getHtml("pageClass")."\"><div class=\"content\">"; + if ($this->yellow->system->get("editToolbarButtons")!="none") $output .= "<h1>".$page->getHtml("titleContent")."</h1>\n"; + $output .= $page->getContent(); + $output .= "</div></div>"; + $page->setOutput($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->set("fileNameShort", $fileNameShort); + $this->editMediaFile($file, "upload"); + $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation")); + $file->fileName = substru($file->location, 1); + while (is_file($file->fileName)) { + $fileNameShort = $this->getFileNext(basename($file->fileName)); + $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation")); + $file->fileName = substru($file->location, 1); + if (++$fileCounter>999) break; + } + if (is_file($file->fileName)) $file->error(500, "File '".$file->get("fileNameShort")."' is not possible!"); + return $file; + } + + // Return page data including status information + public function getPageData($page) { + $data = array(); + if ($this->isUser()) { + $data["title"] = $this->yellow->toolbox->getMetaData($this->rawDataEdit, "title"); + $data["rawDataSource"] = $this->rawDataSource; + $data["rawDataEdit"] = $this->rawDataEdit; + $data["rawDataNew"] = $this->getRawDataNew($page); + $data["rawDataOutput"] = strval($this->rawDataOutput); + $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; + $data["safeMode"] = $this->yellow->page->safeMode; + } + if ($this->action!="none") $data = array_merge($data, $this->getRequestData()); + $data["action"] = $this->action; + $data["status"] = $this->status; + $data["statusCode"] = $this->yellow->page->statusCode; + return $data; + } + + // Return system data including user information + public function getSystemData() { + $data = $this->yellow->system->getData("", "Location"); + if ($this->isUser()) { + $data["userEmail"] = $this->userEmail; + $data["userName"] = $this->extension->users->getName($this->userEmail); + $data["userLanguage"] = $this->extension->users->getLanguage($this->userEmail); + $data["userStatus"] = $this->extension->users->getStatus($this->userEmail); + $data["userHome"] = $this->extension->users->getHome($this->userEmail); + $data["userRestrictions"] = intval($this->isUserRestrictions()); + $data["userWebmaster"] = intval($this->isUserWebmaster()); + $data["serverScheme"] = $this->yellow->system->get("serverScheme"); + $data["serverAddress"] = $this->yellow->system->get("serverAddress"); + $data["serverBase"] = $this->yellow->system->get("serverBase"); + $data["serverFileSizeMax"] = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize")); + $data["serverVersion"] = "Datenstrom Yellow ".YellowCore::VERSION; + $data["serverExtensions"] = array(); + foreach ($this->yellow->extensions->extensions as $key=>$value) { + $data["serverExtensions"][$key] = $value["type"]; + } + $data["serverLanguages"] = array(); + foreach ($this->yellow->text->getLanguages() as $language) { + $data["serverLanguages"][$language] = $this->yellow->text->getTextHtml("languageDescription", $language); + } + $data["editUploadExtensions"] = $this->yellow->system->get("editUploadExtensions"); + $data["editKeyboardShortcuts"] = $this->yellow->system->get("editKeyboardShortcuts"); + $data["editToolbarButtons"] = $this->getToolbarButtons("edit"); + $data["emojiawesomeToolbarButtons"] = $this->getToolbarButtons("emojiawesome"); + $data["fontawesomeToolbarButtons"] = $this->getToolbarButtons("fontawesome"); + } else { + $data["editLoginEmail"] = $this->yellow->page->get("editLoginEmail"); + $data["editLoginPassword"] = $this->yellow->page->get("editLoginPassword"); + $data["editLoginRestrictions"] = intval($this->isLoginRestrictions()); + } + if (defined("DEBUG") && DEBUG>=1) $data["debug"] = DEBUG; + return $data; + } + + // Return request strings + public function getRequestData() { + $data = array(); + foreach ($_REQUEST as $key=>$value) { + if ($key=="password" || $key=="authtoken" || $key=="csrftoken" || $key=="actiontoken" || substru($key, 0, 7)=="rawdata") continue; + $data["request".ucfirst($key)] = trim($value); + } + return $data; + } + + // Return text strings + public function getTextData() { + $textLanguage = $this->yellow->text->getData("language", $this->language); + $textEdit = $this->yellow->text->getData("edit", $this->language); + $textYellow = $this->yellow->text->getData("yellow", $this->language); + return array_merge($textLanguage, $textEdit, $textYellow); + } + + // Return toolbar buttons + public function getToolbarButtons($name) { + if ($name=="edit") { + $toolbarButtons = $this->yellow->system->get("editToolbarButtons"); + if ($toolbarButtons=="auto") { + $toolbarButtons = ""; + if ($this->yellow->extensions->isExisting("markdown")) $toolbarButtons = "preview, format, bold, italic, code, list, link, file"; + if ($this->yellow->extensions->isExisting("emojiawesome")) $toolbarButtons .= ", emojiawesome"; + if ($this->yellow->extensions->isExisting("fontawesome")) $toolbarButtons .= ", fontawesome"; + if ($this->yellow->extensions->isExisting("draft")) $toolbarButtons .= ", draft"; + if ($this->yellow->extensions->isExisting("markdown")) $toolbarButtons .= ", markdown"; + } + } else { + $toolbarButtons = $this->yellow->system->get("{$name}ToolbarButtons"); + } + return $toolbarButtons; + } + + // Return end of line format + public function getEndOfLine($rawData = "") { + $endOfLine = $this->yellow->system->get("editEndOfLine"); + if ($endOfLine=="auto") { + $rawData = empty($rawData) ? PHP_EOL : substru($rawData, 0, 4096); + $endOfLine = strposu($rawData, "\r")===false ? "lf" : "crlf"; + } + return $endOfLine; + } + + // Return raw data for new page + public function getRawDataNew($page, $customTitle = false) { + 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("contentSharedDir"); + $fileName = $this->yellow->lookup->findFileFromLocation($location, true).$this->yellow->system->get("editNewFile"); + $fileName = strreplaceu("(.*)", $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("contentSharedDir"); + $fileName = $this->yellow->lookup->findFileFromLocation($location, true).$this->yellow->system->get("editNewFile"); + $fileName = strreplaceu("(.*)", $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->getName($this->userEmail), " "), $rawData); + $rawData = preg_replace("/@username/i", $this->extension->users->getName($this->userEmail), $rawData); + $rawData = preg_replace("/@userlanguage/i", $this->extension->users->getLanguage($this->userEmail), $rawData); + } else { + $rawData = "---\nTitle: Page\n---\n"; + } + if ($customTitle) { + $title = $this->yellow->toolbox->createTextTitle($page->location); + $rawData = $this->yellow->toolbox->setMetaData($rawData, "title", $title); + } + return $rawData; + } + + // Return location for new/modified page + public function getPageNewLocation($rawData, $pageLocation, $pageNewLocation, $pageMatchLocation = false) { + $location = empty($pageNewLocation) ? "@title" : $pageNewLocation; + $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); + if (!preg_match("/^\//", $location)) { + if ($this->yellow->lookup->isFileLocation($pageLocation) || !$pageMatchLocation) { + $location = $this->yellow->lookup->getDirectoryLocation($pageLocation).$location; + } else { + $location = $this->yellow->lookup->getDirectoryLocation(rtrim($pageLocation, "/")).$location; + } + } + if ($pageMatchLocation) { + $location = rtrim($location, "/").($this->yellow->lookup->isFileLocation($pageLocation) ? "" : "/"); + } + return $location; + } + + // Return title for new/modified page + 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); + return trim(preg_replace("/-+/", "-", $value), "-"); + } + + // Return data for new/modified page + public function getPageNewData($rawData, $key, $filterFirst = false, $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); + 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)) { + if (!is_dir(dirname($fileName))) { + $path = ""; + $tokens = explode("/", $fileName); + for ($i=0; $i<count($tokens)-1; ++$i) { + if (!is_dir($path.$tokens[$i])) { + if (!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i])) { + $number = 1; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^[\d\-\_\.]+(.*)$/", true, true, false) as $entry) { + if ($number!=1 && $number!=intval($entry)) break; + $number = intval($entry)+1; + } + $tokens[$i] = "$number-".$tokens[$i]; + } + $tokens[$i] = $this->yellow->lookup->normaliseName($tokens[$i], false, false, true); + } + $path .= $tokens[$i]."/"; + } + $fileName = $path.$tokens[$i]; + $pageFileName = empty($pageFileName) ? $fileName : $pageFileName; + } + $prefix = $this->getPageNewPrefix($location, $pageFileName, $pagePrefix); + if ($this->yellow->lookup->isFileLocation($location)) { + preg_match("#^(.*)\/(.+?)$#", $fileName, $matches); + $path = $matches[1]; + $text = $this->yellow->lookup->normaliseName($matches[2], true, true); + if (preg_match("/^[\d\-\_\.]*$/", $text)) $prefix = ""; + $fileName = $path."/".$prefix.$text.$this->yellow->system->get("contentExtension"); + } else { + preg_match("#^(.*)\/(.+?)$#", dirname($fileName), $matches); + $path = $matches[1]; + $text = $this->yellow->lookup->normaliseName($matches[2], true, false); + if (preg_match("/^[\d\-\_\.]*$/", $text)) $prefix = ""; + $fileName = $path."/".$prefix.$text."/".$this->yellow->system->get("contentDefaultFile"); + } + } + return $fileName; + } + + // Return prefix for new/modified page + public function getPageNewPrefix($location, $pageFileName, $pagePrefix) { + if (empty($pagePrefix)) { + if ($this->yellow->lookup->isFileLocation($location)) { + preg_match("#^(.*)\/(.+?)$#", $pageFileName, $matches); + $pagePrefix = $matches[2]; + } else { + preg_match("#^(.*)\/(.+?)$#", dirname($pageFileName), $matches); + $pagePrefix = $matches[2]; + } + } + return $this->yellow->lookup->normalisePrefix($pagePrefix, true); + } + + // Return location for new file + public function getFileNewLocation($fileNameShort, $pageLocation, $fileNewLocation) { + $location = empty($fileNewLocation) ? $this->yellow->system->get("editUploadNewLocation") : $fileNewLocation; + $location = preg_replace("/@timestamp/i", time(), $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); + $location = preg_replace("/@filename/i", strtoloweru($fileNameShort), $location); + if (!preg_match("/^\//", $location)) { + $location = $this->yellow->system->get("mediaLocation").$location; + } + return $location; + } + + // Return group for new file + public function getFileNewGroup($fileNameShort) { + $path = $this->yellow->system->get("mediaDir"); + $fileType = $this->yellow->toolbox->getFileType($fileNameShort); + $fileName = $this->yellow->system->get(preg_match("/(gif|jpg|png|svg)$/", $fileType) ? "imageDir" : "downloadDir").$fileNameShort; + preg_match("#^$path(.+?)\/#", $fileName, $matches); + return strtoloweru($matches[1]); + } + + // Return folder for new file + public function getFileNewFolder($pageLocation) { + $parentTopLocation = $this->yellow->content->getParentTopLocation($pageLocation); + if ($parentTopLocation==$this->yellow->content->getHomeLocation($pageLocation)) $parentTopLocation .= "home"; + return strtoloweru(trim($parentTopLocation, "/")); + } + + // Return next file name + public function getFileNext($fileNameShort) { + preg_match("/^(.*?)(\d*)(\..*?)?$/", $fileNameShort, $matches); + $fileText = $matches[1]; + $fileNumber = strempty($matches[2]) ? "-2" : $matches[2]+1; + $fileExtension = $matches[3]; + return $fileText.$fileNumber.$fileExtension; + } + + // Return next title + public function getTitleNext($rawData) { + preg_match("/^(.*?)(\d*)$/", $this->yellow->toolbox->getMetaData($rawData, "title"), $matches); + $titleText = $matches[1]; + $titleNumber = strempty($matches[2]) ? " 2" : $matches[2]+1; + return $titleText.$titleNumber; + } + + // 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 $text; + } + + // 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(); + 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); + } + + // Send mail to user + public function sendMail($scheme, $address, $base, $email, $action) { + if ($action=="welcome" || $action=="goodbye") { + $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/actiontoken:$actionToken/"; + } + if ($action=="approve") { + $account = $email; + $name = $this->yellow->system->get("author"); + $email = $this->yellow->system->get("email"); + } else { + $account = $email; + $name = $this->extension->users->getName($email); + } + $language = $this->extension->users->getLanguage($email); + if (!$this->yellow->text->isLanguage($language)) $language = $this->yellow->system->get("language"); + $sitename = $this->yellow->system->get("sitename"); + $prefix = "edit".ucfirst($action); + $message = $this->yellow->text->getText("{$prefix}Message", $language); + $message = strreplaceu("\\n", "\n", $message); + $message = preg_replace("/@useraccount/i", $account, $message); + $message = preg_replace("/@usershort/i", strtok($name, " "), $message); + $message = preg_replace("/@username/i", $name, $message); + $message = preg_replace("/@userlanguage/i", $language, $message); + $mailTo = mb_encode_mimeheader("$name")." <$email>"; + $mailSubject = mb_encode_mimeheader($this->yellow->text->getText("{$prefix}Subject", $language)); + $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"; + $mailMessage = "$message\r\n\r\n$url\r\n-- \r\n$sitename"; + return mail($mailTo, $mailSubject, $mailMessage, $mailHeaders); + } + + // 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); + } + } + } + + // 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 is webmaster + public function isUserWebmaster() { + return !empty($this->userEmail) && $this->userEmail==$this->yellow->system->get("email"); + } + + // Check if user has restrictions + public function isUserRestrictions() { + return empty($this->userEmail) || $this->userRestrictions; + } + + // Check if login has restrictions + public function isLoginRestrictions() { + return $this->yellow->system->get("editLoginRestrictions"); + } +} + +class YellowUsers { + 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 "YellowUsers::load file:$fileName<br/>\n"; + $fileData = $this->yellow->toolbox->readFile($fileName); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\#/", $line)) continue; + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (!empty($matches[1]) && !empty($matches[2])) { + list($hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) = explode(",", $matches[2]); + $this->set($matches[1], $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home); + if (defined("DEBUG") && DEBUG>=3) echo "YellowUsers::load email:$matches[1]<br/>\n"; + } + } + } + + // Save user to file + public function save($fileName, $email, $password = "", $name = "", $language = "", $status = "", $stamp = "", $modified = "", $errors = "", $pending = "", $home = "") { + if (!empty($password)) $hash = $this->createHash($password); + if ($this->isExisting($email)) { + $email = strreplaceu(",", "-", $email); + $hash = strreplaceu(",", "-", empty($hash) ? $this->users[$email]["hash"] : $hash); + $name = strreplaceu(",", "-", empty($name) ? $this->users[$email]["name"] : $name); + $language = strreplaceu(",", "-", empty($language) ? $this->users[$email]["language"] : $language); + $status = strreplaceu(",", "-", empty($status) ? $this->users[$email]["status"] : $status); + $stamp = strreplaceu(",", "-", empty($stamp) ? $this->users[$email]["stamp"] : $stamp); + $modified = strreplaceu(",", "-", empty($modified) ? time() : $modified); + $errors = strreplaceu(",", "-", empty($errors) ? "0" : $errors); + $pending = strreplaceu(",", "-", empty($pending) ? $this->users[$email]["pending"] : $pending); + $home = strreplaceu(",", "-", empty($home) ? $this->users[$email]["home"] : $home); + } else { + $email = strreplaceu(",", "-", empty($email) ? "none" : $email); + $hash = strreplaceu(",", "-", empty($hash) ? "none" : $hash); + $name = strreplaceu(",", "-", empty($name) ? $this->yellow->system->get("sitename") : $name); + $language = strreplaceu(",", "-", empty($language) ? $this->yellow->system->get("language") : $language); + $status = strreplaceu(",", "-", empty($status) ? "active" : $status); + $stamp = strreplaceu(",", "-", empty($stamp) ? $this->createStamp() : $stamp); + $modified = strreplaceu(",", "-", empty($modified) ? time() : $modified); + $errors = strreplaceu(",", "-", empty($errors) ? "0" : $errors); + $pending = strreplaceu(",", "-", empty($pending) ? "none" : $pending); + $home = strreplaceu(",", "-", empty($home) ? $this->yellow->system->get("editUserHome") : $home); + } + $this->set($email, $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home); + $fileData = $this->yellow->toolbox->readFile($fileName); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (!empty($matches[1]) && $matches[1]==$email) { + $fileDataNew .= "$email: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n"; + $found = true; + } else { + $fileDataNew .= $line; + } + } + if (!$found) $fileDataNew .= "$email: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n"; + return $this->yellow->toolbox->createFile($fileName, $fileDataNew); + } + + // Remove user from file + public function remove($fileName, $email) { + unset($this->users[$email]); + $fileData = $this->yellow->toolbox->readFile($fileName); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (!empty($matches[1]) && !empty($matches[2]) && $matches[1]!=$email) $fileDataNew .= $line; + } + return $this->yellow->toolbox->createFile($fileName, $fileDataNew); + } + + // Set user data + public function set($email, $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) { + $this->users[$email] = array(); + $this->users[$email]["email"] = $email; + $this->users[$email]["hash"] = $hash; + $this->users[$email]["name"] = $name; + $this->users[$email]["language"] = $language; + $this->users[$email]["status"] = $status; + $this->users[$email]["stamp"] = $stamp; + $this->users[$email]["modified"] = $modified; + $this->users[$email]["errors"] = $errors; + $this->users[$email]["pending"] = $pending; + $this->users[$email]["home"] = $home; + } + + // 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, $ignoreCsrfToken) { + $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) || $ignoreCsrfToken); + } + + // 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); + } + + // 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->getStamp($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"; + return substrb($signature, 4); + } + + // Create CSRF token + public function createCsrfToken() { + return $this->yellow->toolbox->createSalt(64); + } + + // Create password hash + public function createHash($password) { + $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"; + return $hash; + } + + // Create user stamp + public function createStamp() { + $stamp = $this->yellow->toolbox->createSalt(20); + while ($this->getAuthEmail("none", $stamp)) { + $stamp = $this->yellow->toolbox->createSalt(20); + } + return $stamp; + } + + // Return user email from authentication, timing attack safe email lookup + public function getAuthEmail($authToken, $stamp = "") { + if (empty($stamp)) $stamp = substrb($authToken, 96, 20); + foreach ($this->users as $key=>$value) { + if ($this->yellow->toolbox->verifyToken($value["stamp"], $stamp)) $email = $key; + } + return $email; + } + + // Return expiration time from authentication + public function getAuthExpire($authToken) { + return hexdec(substrb($authToken, 96+20)); + } + + // Return user hash + public function getHash($email) { + return $this->isExisting($email) ? $this->users[$email]["hash"] : ""; + } + + // Return user name + public function getName($email) { + return $this->isExisting($email) ? $this->users[$email]["name"] : ""; + } + + // Return user language + public function getLanguage($email) { + return $this->isExisting($email) ? $this->users[$email]["language"] : ""; + } + + // Return user status + public function getStatus($email) { + return $this->isExisting($email) ? $this->users[$email]["status"] : ""; + } + + // Return user stamp + public function getStamp($email) { + return $this->isExisting($email) ? $this->users[$email]["stamp"] : ""; + } + + // Return user modified + public function getModified($email) { + return $this->isExisting($email) ? $this->users[$email]["modified"] : ""; + } + + // Return user errors + public function getErrors($email) { + return $this->isExisting($email) ? $this->users[$email]["errors"] : ""; + } + + // Return user pending + public function getPending($email) { + return $this->isExisting($email) ? $this->users[$email]["pending"] : ""; + } + + // Return user home + public function getHome($email) { + return $this->isExisting($email) ? $this->users[$email]["home"] : ""; + } + + // Return number of users + public function getNumber() { + return count($this->users); + } + + // Return user data + public function getData() { + $data = array(); + foreach ($this->users as $key=>$value) { + $name = $value["name"]; + $status = $value["status"]; + if (preg_match("/\s/", $name)) $name = "\"$name\""; + if (preg_match("/\s/", $status)) $status = "\"$status\""; + $data[$key] = "$value[email] $name $status"; + } + 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 = $this->users[$email]["modified"] + 60*60*24; + if ($status=="active" || $status=="inactive" || $reserved>time()) $taken = true; + } + return $taken; + } + + // Check if user exists + public function isExisting($email) { + return !is_null($this->users[$email]); + } +} + +class YellowMerge { + public $yellow; //access to API + const ADD = "+"; //merge types + const MODIFY = "*"; + const REMOVE = "-"; + const SAME = " "; + + public function __construct($yellow) { + $this->yellow = $yellow; + } + + // Merge text, null if not possible + public function merge($textSource, $textMine, $textYours, $showDiff = false) { + if ($textMine!=$textYours) { + $diffMine = $this->buildDiff($textSource, $textMine); + $diffYours = $this->buildDiff($textSource, $textYours); + $diff = $this->mergeDiff($diffMine, $diffYours); + $output = $this->getOutput($diff, $showDiff); + } else { + $output = $textMine; + } + return $output; + } + + // Build differences to common source + public function buildDiff($textSource, $textOther) { + $diff = array(); + $lastRemove = -1; + $textStart = 0; + $textSource = $this->yellow->toolbox->getTextLines($textSource); + $textOther = $this->yellow->toolbox->getTextLines($textOther); + $sourceEnd = $sourceSize = count($textSource); + $otherEnd = $otherSize = count($textOther); + while ($textStart<$sourceEnd && $textStart<$otherEnd && $textSource[$textStart]==$textOther[$textStart]) { + ++$textStart; + } + while ($textStart<$sourceEnd && $textStart<$otherEnd && $textSource[$sourceEnd-1]==$textOther[$otherEnd-1]) { + --$sourceEnd; + --$otherEnd; + } + for ($pos=0; $pos<$textStart; ++$pos) { + array_push($diff, array(YellowMerge::SAME, $textSource[$pos], false)); + } + $lcs = $this->buildDiffLCS($textSource, $textOther, $textStart, $sourceEnd-$textStart, $otherEnd-$textStart); + for ($x=0,$y=0,$xEnd=$otherEnd-$textStart,$yEnd=$sourceEnd-$textStart; $x<$xEnd || $y<$yEnd;) { + $max = $lcs[$y][$x]; + if ($y<$yEnd && $lcs[$y+1][$x]==$max) { + array_push($diff, array(YellowMerge::REMOVE, $textSource[$textStart+$y], false)); + if ($lastRemove==-1) $lastRemove = count($diff)-1; + ++$y; + continue; + } + if ($x<$xEnd && $lcs[$y][$x+1]==$max) { + if ($lastRemove==-1 || $diff[$lastRemove][0]!=YellowMerge::REMOVE) { + array_push($diff, array(YellowMerge::ADD, $textOther[$textStart+$x], false)); + $lastRemove = -1; + } else { + $diff[$lastRemove] = array(YellowMerge::MODIFY, $textOther[$textStart+$x], false); + ++$lastRemove; + if (count($diff)==$lastRemove) $lastRemove = -1; + } + ++$x; + continue; + } + array_push($diff, array(YellowMerge::SAME, $textSource[$textStart+$y], false)); + $lastRemove = -1; + ++$x; + ++$y; + } + for ($pos=$sourceEnd;$pos<$sourceSize; ++$pos) { + array_push($diff, array(YellowMerge::SAME, $textSource[$pos], false)); + } + return $diff; + } + + // Build longest common subsequence + public function buildDiffLCS($textSource, $textOther, $textStart, $yEnd, $xEnd) { + $lcs = array_fill(0, $yEnd+1, array_fill(0, $xEnd+1, 0)); + for ($y=$yEnd-1; $y>=0; --$y) { + for ($x=$xEnd-1; $x>=0; --$x) { + if ($textSource[$textStart+$y]==$textOther[$textStart+$x]) { + $lcs[$y][$x] = $lcs[$y+1][$x+1]+1; + } else { + $lcs[$y][$x] = max($lcs[$y][$x+1], $lcs[$y+1][$x]); + } + } + } + return $lcs; + } + + // Merge differences + public function mergeDiff($diffMine, $diffYours) { + $diff = array(); + $posMine = $posYours = 0; + while ($posMine<count($diffMine) && $posYours<count($diffYours)) { + $typeMine = $diffMine[$posMine][0]; + $typeYours = $diffYours[$posYours][0]; + if ($typeMine==YellowMerge::SAME) { + array_push($diff, $diffYours[$posYours]); + } elseif ($typeYours==YellowMerge::SAME) { + array_push($diff, $diffMine[$posMine]); + } elseif ($typeMine==YellowMerge::ADD && $typeYours==YellowMerge::ADD) { + $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false); + } elseif ($typeMine==YellowMerge::MODIFY && $typeYours==YellowMerge::MODIFY) { + $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false); + } elseif ($typeMine==YellowMerge::REMOVE && $typeYours==YellowMerge::REMOVE) { + array_push($diff, $diffMine[$posMine]); + } elseif ($typeMine==YellowMerge::ADD) { + array_push($diff, $diffMine[$posMine]); + } elseif ($typeYours==YellowMerge::ADD) { + array_push($diff, $diffYours[$posYours]); + } else { + $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], true); + } + if (defined("DEBUG") && DEBUG>=2) echo "YellowMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n"; + if ($typeMine==YellowMerge::ADD || $typeYours==YellowMerge::ADD) { + if ($typeMine==YellowMerge::ADD) ++$posMine; + if ($typeYours==YellowMerge::ADD) ++$posYours; + } else { + ++$posMine; + ++$posYours; + } + } + for (;$posMine<count($diffMine); ++$posMine) { + array_push($diff, $diffMine[$posMine]); + $typeMine = $diffMine[$posMine][0]; + $typeYours = " "; + if (defined("DEBUG") && DEBUG>=2) echo "YellowMerge::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 "YellowMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n"; + } + return $diff; + } + + // Merge potential conflict + public function mergeConflict(&$diff, $diffMine, $diffYours, $conflict) { + if (!$conflict && $diffMine[1]==$diffYours[1]) { + array_push($diff, $diffMine); + } else { + array_push($diff, array($diffMine[0], $diffMine[1], true)); + array_push($diff, array($diffYours[0], $diffYours[1], true)); + } + } + + // Return merged text, null if not possible + public function getOutput($diff, $showDiff = false) { + $output = ""; + if (!$showDiff) { + for ($i=0; $i<count($diff); ++$i) { + if ($diff[$i][0]!=YellowMerge::REMOVE) $output .= $diff[$i][1]; + $conflict |= $diff[$i][2]; + } + } else { + for ($i=0; $i<count($diff); ++$i) { + $output .= $diff[$i][2] ? "! " : $diff[$i][0]." "; + $output .= $diff[$i][1]; + } + } + return !$conflict ? $output : null; + } +} diff --git a/system/plugins/edit.woff b/system/extensions/edit.woff Binary files differ. diff --git a/system/extensions/flatsite.php b/system/extensions/flatsite.php @@ -0,0 +1,9 @@ +<?php +// Flatsite extension, https://github.com/datenstrom/yellow-extensions/tree/master/themes/flatsite +// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +class YellowFlatsite { + const VERSION = "0.8.2"; + const TYPE = "theme"; +} diff --git a/system/extensions/image.php b/system/extensions/image.php @@ -0,0 +1,261 @@ +<?php +// Image extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/image +// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +class YellowImage { + const VERSION = "0.8.2"; + const TYPE = "feature"; + public $yellow; //access to API + public $graphicsLibrary; //graphics library support? (boolean) + + // 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("imageThumbnailDir", "media/thumbnails/"); + $this->yellow->system->setDefault("imageThumbnailJpgQuality", "80"); + $this->graphicsLibrary = $this->isGraphicsLibrary(); + } + + // Handle page content of shortcut + public function onParseContentShortcut($page, $name, $text, $type) { + $output = null; + if ($name=="image" && $type=="inline") { + if (!$this->graphicsLibrary) { + $this->yellow->page->error(500, "Image extension requires GD library with gif/jpg/png support!"); + return $output; + } + list($name, $alt, $style, $width, $height) = $this->yellow->toolbox->getTextArgs($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("imageDir").$name, $width, $height); + } else { + if (empty($alt)) $alt = $this->yellow->system->get("imageAlt"); + $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)."\""; + $output .= " />"; + } + return $output; + } + + // Handle media file changes + public function onEditMediaFile($file, $action) { + if ($action=="upload" && $this->graphicsLibrary) { + $fileName = $file->fileName; + $fileType = $this->yellow->toolbox->getFileType($file->get("fileNameShort")); + list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInformation($fileName, $fileType); + $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); + if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) { + $file->error(500, "Can't write file '$fileName'!"); + } + } + if ($this->yellow->system->get("safeMode") && $fileType=="svg") { + $output = $this->sanitiseXmlData($this->yellow->toolbox->readFile($fileName)); + if (empty($output) || !$this->yellow->toolbox->createFile($fileName, $output)) { + $file->error(500, "Can't write file '$fileName'!"); + } + } + } + } + + // Handle command + public function onCommand($args) { + list($command) = $args; + switch ($command) { + case "clean": $statusCode = $this->processCommandClean($args); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Process command to clean thumbnails + public function processCommandClean($args) { + $statusCode = 0; + list($command, $path) = $args; + if ($path=="all") { + $path = $this->yellow->system->get("imageThumbnailDir"); + 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 + public function getImageInformation($fileName, $widthOutput, $heightOutput) { + $fileNameShort = substru($fileName, strlenu($this->yellow->system->get("imageDir"))); + list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInformation($fileName); + $widthOutput = $this->convertValueAndUnit($widthOutput, $widthInput); + $heightOutput = $this->convertValueAndUnit($heightOutput, $heightInput); + if (($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg") { + $src = $this->yellow->system->get("serverBase").$this->yellow->system->get("imageLocation").$fileNameShort; + $width = $widthOutput; + $height = $heightOutput; + } else { + $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("imageThumbnailDir").$fileNameThumb; + if ($this->isFileNotUpdated($fileName, $fileNameOutput)) { + $image = $this->loadImage($fileName, $type); + $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); + 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("serverBase").$this->yellow->system->get("imageThumbnailLocation").$fileNameThumb; + list($width, $height) = $this->yellow->toolbox->detectImageInformation($fileNameOutput); + } + return array($src, $width, $height); + } + + // Return image dimensions that fit, scale proportional + public function getImageDimensionsFit($widthInput, $heightInput, $widthMax, $heightMax) { + $widthOutput = $widthMax; + $heightOutput = $widthMax * ($heightInput / $widthInput); + if ($heightOutput>$heightMax) { + $widthOutput = $widthOutput * ($heightMax / $heightOutput); + $heightOutput = $heightOutput * ($heightMax / $heightOutput); + } + return array(intval($widthOutput), intval($heightOutput)); + } + + // Load image from file + public function loadImage($fileName, $type) { + $image = false; + switch ($type) { + case "gif": $image = @imagecreatefromgif($fileName); break; + case "jpg": $image = @imagecreatefromjpeg($fileName); break; + case "png": $image = @imagecreatefrompng($fileName); break; + } + return $image; + } + + // Save image to file + public function saveImage($image, $fileName, $type, $quality) { + $ok = false; + switch ($type) { + case "gif": $ok = @imagegif($image, $fileName); break; + case "jpg": $ok = @imagejpeg($image, $fileName, $quality); break; + case "png": $ok = @imagepng($image, $fileName); break; + } + return $ok; + } + + // Create image from scratch + public function createImage($width, $height) { + $image = imagecreatetruecolor($width, $height); + imagealphablending($image, false); + imagesavealpha($image, true); + return $image; + } + + // Resize image + public function resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput) { + $widthFit = $widthInput * ($heightOutput / $heightInput); + $heightFit = $heightInput * ($widthOutput / $widthInput); + $widthDiff = abs($widthOutput - $widthFit); + $heightDiff = abs($heightOutput - $heightFit); + $imageOutput = $this->createImage($widthOutput, $heightOutput); + if ($heightFit>$heightOutput) { + imagecopyresampled($imageOutput, $image, 0, $heightDiff/-2, 0, 0, $widthOutput, $heightFit, $widthInput, $heightInput); + } else { + imagecopyresampled($imageOutput, $image, $widthDiff/-2, 0, 0, 0, $widthFit, $heightOutput, $widthInput, $heightInput); + } + return $imageOutput; + } + + // Return value according to unit + public function convertValueAndUnit($text, $valueBase) { + $value = $unit = ""; + if (preg_match("/([\d\.]+)(\S*)/", $text, $matches)) { + $value = $matches[1]; + $unit = $matches[2]; + if ($unit=="%") $value = $valueBase * $value / 100; + } + return intval($value); + } + + // Return sanitised XML data + public function sanitiseXmlData($rawData) { + $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", "image", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "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", "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", "alt", "autocomplete", "background", "bgcolor", "border", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "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", "preload", "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", "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", "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", "xmlns", "y", "y1", "y2", "z", "zoomandpan"); + $attributesXml = array( + "xlink:href", "xml:id", "xml:space"); + if (!empty($rawData)) { + $entityLoader = libxml_disable_entity_loader(true); + $internalErrors = libxml_use_internal_errors(true); + $document = new DOMDocument(); + $document->recover = true; + if ($document->loadXML($rawData)) { + $elementsSafe = array_merge($elementsHtml, $elementsSvg); + $attributesSafe = array_merge($attributesHtml, $attributesSvg, $attributesXml); + $elements = $document->getElementsByTagName("*"); + for ($i=$elements->length-1; $i>=0; --$i) { + $element = $elements->item($i); + if (!in_array(strtolower($element->tagName), $elementsSafe)) { + $element->parentNode->removeChild($element); + continue; + } + for ($j=$element->attributes->length-1; $j>=0; --$j) { + $attribute = $element->attributes->item($j); + if (!in_array(strtolower($attribute->name), $attributesSafe) && !preg_match("/^(aria|data)-/i", $attribute->name)) { + $element->removeAttribute($attribute->name); + } + } + $href = $element->getAttribute("href"); + if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { + $element->setAttribute("href", "error-xss-filter"); + } + $href = $element->getAttribute("xlink:href"); + if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { + $element->setAttribute("xlink:href", "error-xss-filter"); + } + } + $output = $document->saveXML(); + if (!preg_match("/^<\?xml /", $rawData) && preg_match("/^<\?xml (.*?)>\s*(.*)$/s", $output, $matches)) $output = $matches[2]; + } + libxml_disable_entity_loader($entityLoader); + libxml_use_internal_errors($internalErrors); + } + return $output; + } + + // Check if file needs to be updated + public function isFileNotUpdated($fileNameInput, $fileNameOutput) { + return $this->yellow->toolbox->getFileModified($fileNameInput)!=$this->yellow->toolbox->getFileModified($fileNameOutput); + } + + // Check graphics library support + public function isGraphicsLibrary() { + return extension_loaded("gd") && function_exists("gd_info") && + ((imagetypes()&(IMG_GIF|IMG_JPG|IMG_PNG))==(IMG_GIF|IMG_JPG|IMG_PNG)); + } +} diff --git a/system/extensions/install-blog.zip b/system/extensions/install-blog.zip Binary files differ. diff --git a/system/extensions/install-languages.zip b/system/extensions/install-languages.zip Binary files differ. diff --git a/system/extensions/install-wiki.zip b/system/extensions/install-wiki.zip Binary files differ. diff --git a/system/extensions/install.php b/system/extensions/install.php @@ -0,0 +1,303 @@ +<?php +// Install extension, https://github.com/datenstrom/yellow +// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +class YellowInstall { + const VERSION = "0.8.2"; + const TYPE = "feature"; + const PRIORITY = "1"; + public $yellow; //access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle request + public function onRequest($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->yellow->lookup->isContentFile($fileName)) { + $server = $this->yellow->toolbox->getServerVersion(true); + $this->checkServerRewrite($scheme, $address, $base, $location, $fileName) || die("Datenstrom Yellow requires $server rewrite module!"); + $this->checkServerAccess() || die("Datenstrom Yellow requires $server read/write access!"); + $statusCode = $this->processRequestInstall($scheme, $address, $base, $location, $fileName); + } + return $statusCode; + } + + // Handle command + public function onCommand($args) { + return $this->processCommandInstall(); + } + + // Process command to install website + public function processCommandInstall() { + $statusCode = $this->updateLanguages(); + if ($statusCode==200) $statusCode = $this->updateSettings($this->getSystemData()); + if ($statusCode==200) $statusCode = $this->removeFiles(); + if ($statusCode==200) { + $statusCode = 0; + } else { + 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 website + public function processRequestInstall($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"])); + $email = trim($_REQUEST["email"]); + $password = trim($_REQUEST["password"]); + $language = trim($_REQUEST["language"]); + $extension = trim($_REQUEST["extension"]); + $status = trim($_REQUEST["status"]); + $this->yellow->content->pages["root/"] = array(); + $this->yellow->page = new YellowPage($this->yellow); + $statusCode = $this->updateLanguages(); + $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName); + $this->yellow->page->parseData($this->getRawDataInstall(), false, $statusCode, $this->yellow->page->get("pageError")); + $this->yellow->page->safeMode = false; + if ($status=="install") $status = $this->updateUser($email, $password, $name, $language)==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateExtension($extension)==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateContent($language, "Home", "/")==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateContent($language, "About", "/about/")==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateContent($language, "Footer", "/shared/footer")==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateSettings($this->getSystemData()) ? "ok" : "error"; + if ($status=="ok") $status = $this->removeFiles() ? "done" : "error"; + if ($status=="done") { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $statusCode = $this->yellow->sendPage(); + } + return $statusCode; + } + + // Update languages + public function updateLanguages() { + $statusCode = 200; + $path = $this->yellow->system->get("extensionDir")."install-languages.zip"; + if (is_file($path) && $this->yellow->extensions->isExisting("update")) { + $zip = new ZipArchive(); + if ($zip->open($path)===true) { + if (defined("DEBUG") && DEBUG>=2) echo "YellowInstall::updateLanguages file:$path<br/>\n"; + $languages = $this->detectBrowserLanguages("en, de, fr"); + $languagesFound = array(); + foreach ($languages as $language) $languagesFound[$language] = ""; + if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1]; + $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateInformationFile")); + 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], "/")) { + list($dummy, $entry) = explode("/", $matches[1], 2); + $language = array_pop(explode(",", $matches[2])); + if (preg_match("/^(.*)\.php$/", basename($entry), $tokens) && in_array($language, $languages)) { + $languagesFound[$language] = $tokens[1]; + } + } + } + $languagesFound = array_slice(array_filter($languagesFound, "strlen"), 0, 3); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (lcfirst($matches[1])=="extension") $extension = lcfirst($matches[2]); + if (lcfirst($matches[1])=="published") $modified = strtotime($matches[2]); + if (!empty($matches[1]) && !empty($matches[2]) && strposu($matches[1], "/")) { + list($dummy, $entry) = explode("/", $matches[1], 2); + list($fileName) = explode(",", $matches[2], 2); + $fileData = $zip->getFromName($pathBase.basename($entry)); + if (preg_match("/^(.*).php$/", basename($entry), $tokens) && in_array($tokens[1], $languagesFound)) { + $statusCode = $this->yellow->extensions->get("update")->updateExtensionFile($fileName, $fileData, + $modified, 0, 0, "create,update", false, $extension); + } + if (preg_match("/^(.*)-language\.txt$/", basename($entry), $tokens) && in_array($tokens[1], $languagesFound)) { + $statusCode = $this->yellow->extensions->get("update")->updateExtensionFile($fileName, $fileData, + $modified, 0, 0, "create,update", false, $extension); + } + } + } + $zip->close(); + if ($statusCode==200) { + $this->yellow->text->load($this->yellow->system->get("extensionDir").$this->yellow->system->get("languageFile"), ""); + } + } else { + $statusCode = 500; + $this->yellow->page->error(500, "Can't open file '$path'!"); + } + } + return $statusCode; + } + + // Update user + public function updateUser($email, $password, $name, $language) { + $statusCode = 200; + if (!empty($email) && !empty($password) && $this->yellow->extensions->isExisting("edit")) { + $fileNameUser = $this->yellow->system->get("settingDir").$this->yellow->system->get("editUserFile"); + $status = $this->yellow->extensions->get("edit")->users->save($fileNameUser, $email, $password, $name, $language) ? "ok" : "error"; + if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + return $statusCode; + } + + // Update extension + public function updateExtension($extension) { + $statusCode = 200; + $path = $this->yellow->system->get("extensionDir"); + if (!empty($extension) && $this->yellow->extensions->isExisting("update")) { + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { + if (preg_match("/^install-(.*?)\./", basename($entry), $matches)) { + if (strtoloweru($matches[1])==strtoloweru($extension)) { + $statusCode = $this->yellow->extensions->get("update")->updateExtensionArchive($entry); + break; + } + } + } + } + return $statusCode; + } + + // Update content + public function updateContent($language, $name, $location) { + $statusCode = 200; + if ($language!="en") { + $titleOld = "Title: ".$this->yellow->text->getText("install{$name}Title", "en"); + $titleNew = "Title: ".$this->yellow->text->getText("install{$name}Title", $language); + $textOld = strreplaceu("\\n", "\n", $this->yellow->text->getText("install{$name}Text", "en")); + $textNew = strreplaceu("\\n", "\n", $this->yellow->text->getText("install{$name}Text", $language)); + $fileName = $this->yellow->lookup->findFileFromLocation($location); + $fileData = strreplaceu("\r\n", "\n", $this->yellow->toolbox->readFile($fileName)); + $fileData = strreplaceu($titleOld, $titleNew, $fileData); + $fileData = strreplaceu($textOld, $textNew, $fileData); + if (!$this->yellow->toolbox->createFile($fileName, $fileData)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + return $statusCode; + } + + // Update settings + public function updateSettings($settings) { + $statusCode = 200; + $fileName = $this->yellow->system->get("settingDir").$this->yellow->system->get("systemFile"); + if (!$this->yellow->system->save($fileName, $settings)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Remove files used by installation + public function removeFiles() { + $statusCode = 200; + if (function_exists("opcache_reset")) opcache_reset(); + $path = $this->yellow->system->get("extensionDir"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { + if (preg_match("/^install-(.*?)\./", basename($entry), $matches)) { + if (!$this->yellow->toolbox->deleteFile($entry)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$entry'!"); + } + } + } + $path = $this->yellow->system->get("extensionDir")."install.php"; + if ($statusCode==200 && !$this->yellow->toolbox->deleteFile($path)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$path'!"); + } + if ($statusCode==200) unset($this->yellow->extensions->extensions["install"]); + return $statusCode; + } + + // Check web server rewrite + public function checkServerRewrite($scheme, $address, $base, $location, $fileName) { + $curlHandle = curl_init(); + $location = $this->yellow->system->get("resourceLocation").$this->yellow->lookup->normaliseName($this->yellow->system->get("theme")).".css"; + $url = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + curl_setopt($curlHandle, CURLOPT_URL, $url); + curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowCore/".YellowCore::VERSION).")"; + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); + $rawData = curl_exec($curlHandle); + $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); + curl_close($curlHandle); + return !empty($rawData) && $statusCode==200; + } + + // Check web server read/write access + public function checkServerAccess() { + $fileName = $this->yellow->system->get("settingDir").$this->yellow->system->get("systemFile"); + return $this->yellow->system->save($fileName, array()); + } + + // Detect web browser languages + public function detectBrowserLanguages($languagesDefault) { + $languages = array(); + if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) { + foreach (preg_split("/\s*,\s*/", $_SERVER["HTTP_ACCEPT_LANGUAGE"]) as $string) { + list($language) = explode(";", $string); + if (!empty($language)) array_push($languages, $language); + } + } + foreach (preg_split("/\s*,\s*/", $languagesDefault) as $language) { + if (!empty($language)) array_push($languages, $language); + } + return array_unique($languages); + } + + // Return system data, detect server URL + public function getSystemData() { + $data = array(); + foreach ($_REQUEST as $key=>$value) { + if (!$this->yellow->system->isExisting($key)) continue; + $data[$key] = trim($value); + } + $data["timezone"] = $this->yellow->toolbox->getTimezone(); + $data["staticUrl"] = $this->yellow->toolbox->getServerUrl(); + if ($this->yellow->isCommandLine()) $data["staticUrl"] = getenv("URL"); + return $data; + } + + // Return raw data for install page + public function getRawDataInstall() { + $language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->text->getLanguages(), $this->yellow->system->get("language")); + $this->yellow->text->setLanguage($language); + $rawData = "---\nTitle:".$this->yellow->text->get("installTitle")."\nLanguage:$language\nNavigation:navigation\n---\n"; + $rawData .= "<form class=\"install-form\" action=\"".$this->yellow->page->getLocation(true)."\" method=\"post\">\n"; + $rawData .= "<p><label for=\"name\">".$this->yellow->text->get("editSignupName")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"name\" id=\"name\" value=\"\"></p>\n"; + $rawData .= "<p><label for=\"email\">".$this->yellow->text->get("editSignupEmail")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"email\" id=\"email\" value=\"\"></p>\n"; + $rawData .= "<p><label for=\"password\">".$this->yellow->text->get("editSignupPassword")."</label><br /><input class=\"form-control\" type=\"password\" maxlength=\"64\" name=\"password\" id=\"password\" value=\"\"></p>\n"; + if (count($this->yellow->text->getLanguages())>1) { + $rawData .= "<p>"; + foreach ($this->yellow->text->getLanguages() as $language) { + $checked = $language==$this->yellow->text->language ? " checked=\"checked\"" : ""; + $rawData .= "<label for=\"$language\"><input type=\"radio\" name=\"language\" id=\"$language\" value=\"$language\"$checked> ".$this->yellow->text->getTextHtml("languageDescription", $language)."</label><br />"; + } + $rawData .= "</p>\n"; + } + if (count($this->getExtensionsInstall())>1) { + $rawData .= "<p>".$this->yellow->text->get("installExtension")."<p>"; + foreach ($this->getExtensionsInstall() as $extension) { + $checked = $extension=="website" ? " checked=\"checked\"" : ""; + $rawData .= "<label for=\"$extension\"><input type=\"radio\" name=\"extension\" id=\"$extension\" value=\"$extension\"$checked> ".ucfirst($extension)."</label><br />"; + } + $rawData .= "</p>\n"; + } + $rawData .= "<input class=\"btn\" type=\"submit\" value=\"".$this->yellow->text->get("editOkButton")."\" />\n"; + $rawData .= "<input type=\"hidden\" name=\"status\" value=\"install\" />\n"; + $rawData .= "</form>\n"; + return $rawData; + } + + // Return extensions for install page + public function getExtensionsInstall() { + $extensions = array("website"); + $path = $this->yellow->system->get("extensionDir"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false, false) as $entry) { + if (preg_match("/^install-(.*?)\./", $entry, $matches) && $matches[1]!="languages") array_push($extensions, $matches[1]); + } + return $extensions; + } +} diff --git a/system/extensions/markdown.php b/system/extensions/markdown.php @@ -0,0 +1,3869 @@ +<?php +// Markdown extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/markdown +// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +class YellowMarkdown { + const VERSION = "0.8.2"; + const TYPE = "feature"; + public $yellow; //access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle page content in raw format + public function onParseContentRaw($page, $text) { + $markdown = new YellowMarkdownExtraParser($this->yellow, $page); + return $markdown->transform($text); + } +} + +// PHP Markdown Lib +// Copyright (c) 2004-2018 Michel Fortin +// <https://michelf.ca/> +// All rights reserved. +// +// Original Markdown +// Copyright (c) 2004-2006 John Gruber +// <https://daringfireball.net/> +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name "Markdown" nor the names of its contributors may +// be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// This software is provided by the copyright holders and contributors "as +// is" and any express or implied warranties, including, but not limited +// to, the implied warranties of merchantability and fitness for a +// particular purpose are disclaimed. In no event shall the copyright owner +// or contributors be liable for any direct, indirect, incidental, special, +// exemplary, or consequential damages (including, but not limited to, +// procurement of substitute goods or services; loss of use, data, or +// profits; or business interruption) however caused and on any theory of +// liability, whether in contract, strict liability, or tort (including +// negligence or otherwise) arising in any way out of the use of this +// software, even if advised of the possibility of such damage. + +class MarkdownParser { + /** + * Define the package version + * @var string + */ + const MARKDOWNLIB_VERSION = "1.8.0"; + + /** + * Simple function interface - Initialize the parser and return the result + * of its transform method. This will work fine for derived classes too. + * + * @api + * + * @param string $text + * @return string + */ + public static function defaultTransform($text) { + // Take parser class on which this function was called. + $parser_class = \get_called_class(); + + // Try to take parser from the static parser list + static $parser_list; + $parser =& $parser_list[$parser_class]; + + // Create the parser it not already set + if (!$parser) { + $parser = new $parser_class; + } + + // Transform text using parser. + return $parser->transform($text); + } + + /** + * Configuration variables + */ + + /** + * Change to ">" for HTML output. + * @var string + */ + public $empty_element_suffix = " />"; + + /** + * The width of indentation of the output markup + * @var int + */ + public $tab_width = 4; + + /** + * Change to `true` to disallow markup or entities. + * @var boolean + */ + public $no_markup = false; + public $no_entities = false; + + + /** + * Change to `true` to enable line breaks on \n without two trailling spaces + * @var boolean + */ + public $hard_wrap = false; + + /** + * Predefined URLs and titles for reference links and images. + * @var array + */ + public $predef_urls = array(); + public $predef_titles = array(); + + /** + * Optional filter function for URLs + * @var callable + */ + public $url_filter_func = null; + + /** + * Optional header id="" generation callback function. + * @var callable + */ + public $header_id_func = null; + + /** + * Optional function for converting code block content to HTML + * @var callable + */ + public $code_block_content_func = null; + + /** + * Optional function for converting code span content to HTML. + * @var callable + */ + public $code_span_content_func = null; + + /** + * Class attribute to toggle "enhanced ordered list" behaviour + * setting this to true will allow ordered lists to start from the index + * number that is defined first. + * + * For example: + * 2. List item two + * 3. List item three + * + * Becomes: + * <ol start="2"> + * <li>List item two</li> + * <li>List item three</li> + * </ol> + * + * @var bool + */ + public $enhanced_ordered_list = false; + + /** + * Parser implementation + */ + + /** + * Regex to match balanced [brackets]. + * Needed to insert a maximum bracked depth while converting to PHP. + * @var int + */ + protected $nested_brackets_depth = 6; + protected $nested_brackets_re; + + protected $nested_url_parenthesis_depth = 4; + protected $nested_url_parenthesis_re; + + /** + * Table of hash values for escaped characters: + * @var string + */ + protected $escape_chars = '\`*_{}[]()>#+-.!'; + protected $escape_chars_re; + + /** + * Constructor function. Initialize appropriate member variables. + * @return void + */ + public function __construct() { + $this->_initDetab(); + $this->prepareItalicsAndBold(); + + $this->nested_brackets_re = + str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). + str_repeat('\])*', $this->nested_brackets_depth); + + $this->nested_url_parenthesis_re = + str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). + str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + + $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + + // Sort document, block, and span gamut in ascendent priority order. + asort($this->document_gamut); + asort($this->block_gamut); + asort($this->span_gamut); + } + + + /** + * Internal hashes used during transformation. + * @var array + */ + protected $urls = array(); + protected $titles = array(); + protected $html_hashes = array(); + + /** + * Status flag to avoid invalid nesting. + * @var boolean + */ + protected $in_anchor = false; + + /** + * Status flag to avoid invalid nesting. + * @var boolean + */ + protected $in_emphasis_processing = false; + + /** + * Called before the transformation process starts to setup parser states. + * @return void + */ + protected function setup() { + // Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); + $this->in_anchor = false; + $this->in_emphasis_processing = false; + } + + /** + * Called after the transformation process to clear any variable which may + * be taking up memory unnecessarly. + * @return void + */ + protected function teardown() { + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } + + /** + * Main function. Performs some preprocessing on the input text and pass + * it through the document gamut. + * + * @api + * + * @param string $text + * @return string + */ + public function transform($text) { + $this->setup(); + + # Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + + # Standardize line endings: + # DOS to Unix and Mac to Unix + $text = preg_replace('{\r\n?}', "\n", $text); + + # Make sure $text ends with a couple of newlines: + $text .= "\n\n"; + + # Convert all tabs to spaces. + $text = $this->detab($text); + + # Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks($text); + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ ]*\n+/ . + $text = preg_replace('/^[ ]+$/m', '', $text); + + # Run document gamut methods. + foreach ($this->document_gamut as $method => $priority) { + $text = $this->$method($text); + } + + $this->teardown(); + + return $text . "\n"; + } + + /** + * Define the document gamut + * @var array + */ + protected $document_gamut = array( + // Strip link definitions, store in hashes. + "stripLinkDefinitions" => 20, + "runBasicBlockGamut" => 30, + ); + + /** + * Strips link definitions from text, stores the URLs and titles in + * hash references + * @param string $text + * @return string + */ + protected function stripLinkDefinitions($text) { + + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:\n+|\Z) + }xm', + array($this, '_stripLinkDefinitions_callback'), + $text + ); + return $text; + } + + /** + * The callback to strip link definitions + * @param array $matches + * @return string + */ + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + return ''; // String that will replace the block + } + + /** + * Hashify HTML blocks + * @param string $text + * @return string + */ + protected function hashHTMLBlocks($text) { + if ($this->no_markup) { + return $text; + } + + $less_than_tab = $this->tab_width - 1; + + /** + * Hashify HTML blocks: + * + * We only want to do this for block-level HTML tags, such as headers, + * lists, and tables. That's because we still want to wrap <p>s around + * "paragraphs" that are wrapped in non-block-level tags, such as + * anchors, phrase emphasis, and spans. The list of tags we're looking + * for is hard-coded: + * + * * List "a" is made of tags which can be both inline or block-level. + * These will be treated block-level when the start tag is alone on + * its line, otherwise they're not matched here and will be taken as + * inline later. + * * List "b" is made of tags which are always block-level; + */ + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. + 'script|noscript|style|form|fieldset|iframe|math|svg|'. + 'article|section|nav|aside|hgroup|header|footer|'. + 'figure'; + + // Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' + (?> # optional tag attributes + \s # starts with whitespace + (?> + [^>"/]+ # text outside quotes + | + /+(?!>) # slash not followed by ">" + | + "[^"]*" # text inside double quotes (tolerate ">") + | + \'[^\']*\' # text inside single quotes (tolerate ">") + )* + )? + '; + $content = + str_repeat(' + (?> + [^<]+ # content without tag + | + <\2 # nested opening tag + '.$attr.' # attributes + (?> + /> + | + >', $nested_tags_level). // end of opening tag + '.*?'. // last level nested tag content + str_repeat(' + </\2\s*> # closing nested tag + ) + | + <(?!/\2\s*> # other tags with a different name + ) + )*', + $nested_tags_level); + $content2 = str_replace('\2', '\3', $content); + + /** + * First, look for nested blocks, e.g.: + * <div> + * <div> + * tags for inner block must be indented. + * </div> + * </div> + * + * The outermost tags must start at the left margin for this to match, + * and the inner nested divs must be indented. + * We need to do this before the next, more liberal match, because the + * next match will start at the first `<div>` and stop at the + * first `</div>`. + */ + $text = preg_replace_callback('{(?> + (?> + (?<=\n) # Starting on its own line + | # or + \A\n? # the at beginning of the doc + ) + ( # save in $1 + + # Match from `\n<tag>` to `</tag>\n`, handling nested tags + # in between. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_b_re.')# start tag = $2 + '.$attr.'> # attributes followed by > and \n + '.$content.' # content, support nesting + </\2> # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special version for tags of group a. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_a_re.')# start tag = $3 + '.$attr.'>[ ]*\n # attributes followed by > + '.$content2.' # content, support nesting + </\3> # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for <hr />. It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,'.$less_than_tab.'} + <(hr) # start tag = $2 + '.$attr.' # attributes + /?> # the matching end tag + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # Special case for standalone HTML comments: + + [ ]{0,'.$less_than_tab.'} + (?s: + <!-- .*? --> + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # PHP and ASP-style processor instructions (<? and <%) + + [ ]{0,'.$less_than_tab.'} + (?s: + <([?%]) # $2 + .*? + \2> + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + ) + )}Sxmi', + array($this, '_hashHTMLBlocks_callback'), + $text + ); + + return $text; + } + + /** + * The callback for hashing HTML blocks + * @param string $matches + * @return string + */ + protected function _hashHTMLBlocks_callback($matches) { + $text = $matches[1]; + $key = $this->hashBlock($text); + return "\n\n$key\n\n"; + } + + /** + * Called whenever a tag must be hashed when a function insert an atomic + * element in the text stream. Passing $text to through this function gives + * a unique text-token which will be reverted back when calling unhash. + * + * The $boundary argument specify what character should be used to surround + * the token. By convension, "B" is used for block elements that needs not + * to be wrapped into paragraph tags at the end, ":" is used for elements + * that are word separators and "X" is used in the general case. + * + * @param string $text + * @param string $boundary + * @return string + */ + protected function hashPart($text, $boundary = 'X') { + // Swap back any tag hash found in $text so we do not have to `unhash` + // multiple times at the end. + $text = $this->unhash($text); + + // Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++$i . $boundary; + $this->html_hashes[$key] = $text; + return $key; // String that will replace the tag. + } + + /** + * Shortcut function for hashPart with block-level boundaries. + * @param string $text + * @return string + */ + protected function hashBlock($text) { + return $this->hashPart($text, 'B'); + } + + /** + * Define the block gamut - these are all the transformations that form + * block-level tags like paragraphs, headers, and list items. + * @var array + */ + protected $block_gamut = array( + "doHeaders" => 10, + "doHorizontalRules" => 20, + "doLists" => 40, + "doCodeBlocks" => 50, + "doBlockQuotes" => 60, + ); + + /** + * Run block gamut tranformations. + * + * We need to escape raw HTML in Markdown source before doing anything + * else. This need to be done for each block, and not only at the + * begining in the Markdown function since hashed blocks can be part of + * list items and could have been indented. Indented blocks would have + * been seen as a code block in a previous pass of hashHTMLBlocks. + * + * @param string $text + * @return string + */ + protected function runBlockGamut($text) { + $text = $this->hashHTMLBlocks($text); + return $this->runBasicBlockGamut($text); + } + + /** + * Run block gamut tranformations, without hashing HTML blocks. This is + * useful when HTML blocks are known to be already hashed, like in the first + * whole-document pass. + * + * @param string $text + * @return string + */ + protected function runBasicBlockGamut($text) { + + foreach ($this->block_gamut as $method => $priority) { + $text = $this->$method($text); + } + + // Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs($text); + + return $text; + } + + /** + * Convert horizontal rules + * @param string $text + * @return string + */ + protected function doHorizontalRules($text) { + return preg_replace( + '{ + ^[ ]{0,3} # Leading space + ([-*_]) # $1: First marker + (?> # Repeated marker group + [ ]{0,2} # Zero, one, or two spaces. + \1 # Marker character + ){2,} # Group repeated at least twice + [ ]* # Tailing spaces + $ # End of line. + }mx', + "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n", + $text + ); + } + + /** + * These are all the transformations that occur *within* block-level + * tags like paragraphs, headers, and list items. + * @var array + */ + protected $span_gamut = array( + // Process character escapes, code spans, and inline HTML + // in one shot. + "parseSpan" => -30, + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + "doImages" => 10, + "doAnchors" => 20, + // Make links out of things like `<https://example.com/>` + // Must come after doAnchors, because you can use < and > + // delimiters in inline links like [this](<url>). + "doAutoLinks" => 30, + "encodeAmpsAndAngles" => 40, + "doItalicsAndBold" => 50, + "doHardBreaks" => 60, + ); + + /** + * Run span gamut transformations + * @param string $text + * @return string + */ + protected function runSpanGamut($text) { + foreach ($this->span_gamut as $method => $priority) { + $text = $this->$method($text); + } + + return $text; + } + + /** + * Do hard breaks + * @param string $text + * @return string + */ + protected function doHardBreaks($text) { + if ($this->hard_wrap) { + return preg_replace_callback('/ *\n/', + array($this, '_doHardBreaks_callback'), $text); + } else { + return preg_replace_callback('/ {2,}\n/', + array($this, '_doHardBreaks_callback'), $text); + } + } + + /** + * Trigger part hashing for the hard break (callback method) + * @param array $matches + * @return string + */ + protected function _doHardBreaks_callback($matches) { + return $this->hashPart("<br$this->empty_element_suffix\n"); + } + + /** + * Turn Markdown link shortcuts into XHTML <a> tags. + * @param string $text + * @return string + */ + protected function doAnchors($text) { + if ($this->in_anchor) { + return $text; + } + $this->in_anchor = true; + + // First, handle reference-style links: [link text] [id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + // Next, inline-style links: [link text](url "optional title") + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + ) + }xs', + array($this, '_doAnchors_inline_callback'), $text); + + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link text][1] + // or [link text](/foo) + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + + /** + * Callback method to parse referenced anchors + * @param string $matches + * @return string + */ + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + // for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + // lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if ( isset( $this->titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + $result = $this->hashPart($result); + } else { + $result = $whole_match; + } + return $result; + } + + /** + * Callback method to parse inline anchors + * @param string $matches + * @return string + */ + protected function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + // If the URL was of the form <s p a c e s> it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using + // the URL. + $unhashed = $this->unhash($url); + if ($unhashed != $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + /** + * Turn Markdown image shortcuts into <img> tags. + * @param string $text + * @return string + */ + protected function doImages($text) { + // First, handle reference-style labeled images: ![alt text][id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array($this, '_doImages_reference_callback'), $text); + + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + ) + }xs', + array($this, '_doImages_inline_callback'), $text); + + return $text; + } + + /** + * Callback to parse references image tags + * @param array $matches + * @return string + */ + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); // for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeURLAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } else { + // If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + + /** + * Callback to parse inline image tags + * @param array $matches + * @return string + */ + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeURLAttribute($url); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; // $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + /** + * Parse Markdown heading elements to HTML + * @param string $text + * @return string + */ + protected function doHeaders($text) { + /** + * Setext-style headers: + * Header 1 + * ======== + * + * Header 2 + * -------- + */ + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array($this, '_doHeaders_callback_setext'), $text); + + /** + * atx-style headers: + * # Header 1 + * ## Header 2 + * ## Header 2 with closing hashes ## + * ... + * ###### Header 6 + */ + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array($this, '_doHeaders_callback_atx'), $text); + + return $text; + } + + /** + * Setext header parsing callback + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_setext($matches) { + // Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) { + return $matches[0]; + } + + $level = $matches[2]{0} == '=' ? 1 : 2; + + // ID attribute generation + $idAtt = $this->_generateIdFromHeaderValue($matches[1]); + + $block = "<h$level$idAtt>".$this->runSpanGamut($matches[1])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * ATX header parsing callback + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_atx($matches) { + // ID attribute generation + $idAtt = $this->_generateIdFromHeaderValue($matches[2]); + + $level = strlen($matches[1]); + $block = "<h$level$idAtt>".$this->runSpanGamut($matches[2])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * If a header_id_func property is set, we can use it to automatically + * generate an id attribute. + * + * This method returns a string in the form id="foo", or an empty string + * otherwise. + * @param string $headerValue + * @return string + */ + protected function _generateIdFromHeaderValue($headerValue) { + if (!is_callable($this->header_id_func)) { + return ""; + } + + $idValue = call_user_func($this->header_id_func, $headerValue); + if (!$idValue) { + return ""; + } + + return ' id="' . $this->encodeAttribute($idValue) . '"'; + } + + /** + * Form HTML ordered (numbered) and unordered (bulleted) lists. + * @param string $text + * @return string + */ + protected function doLists($text) { + $less_than_tab = $this->tab_width - 1; + + // Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + + $markers_relist = array( + $marker_ul_re => $marker_ol_re, + $marker_ol_re => $marker_ul_re, + ); + + foreach ($markers_relist as $marker_re => $other_marker_re) { + // Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces + ('.$marker_re.') # $4 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $5 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + | + (?= # Lookahead for another kind of list + \n + \3 # Must have the same indentation + '.$other_marker_re.'[ ]+ + ) + ) + ) + '; // mx + + // We use a different prefix before nested lists than top-level lists. + //See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array($this, '_doLists_callback'), $text); + } else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array($this, '_doLists_callback'), $text); + } + } + + return $text; + } + + /** + * List parsing callback + * @param array $matches + * @return string + */ + protected function _doLists_callback($matches) { + // Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + $marker_ol_start_re = '[0-9]+'; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $ol_start = 1; + if ($this->enhanced_ordered_list) { + // Get the start number for ordered list. + if ($list_type == 'ol') { + $ol_start_array = array(); + $ol_start_check = preg_match("/$marker_ol_start_re/", $matches[4], $ol_start_array); + if ($ol_start_check){ + $ol_start = $ol_start_array[0]; + } + } + } + + if ($ol_start > 1 && $list_type == 'ol'){ + $result = $this->hashBlock("<$list_type start=\"$ol_start\">\n" . $result . "</$list_type>"); + } else { + $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>"); + } + return "\n". $result ."\n\n"; + } + + /** + * Nesting tracker for list levels + * @var integer + */ + protected $list_level = 0; + + /** + * Process the contents of a single ordered or unordered list, splitting it + * into individual list items. + * @param string $list_str + * @param string $marker_any_re + * @return string + */ + protected function processListItems($list_str, $marker_any_re) { + /** + * The $this->list_level global keeps track of when we're inside a list. + * Each time we enter a list, we increment it; when we leave a list, + * we decrement. If it's zero, we're not in a list anymore. + * + * We do this because when we're not inside a list, we want to treat + * something like this: + * + * I recommend upgrading to version + * 8. Oops, now this line is treated + * as a sub-list. + * + * As a single paragraph, despite the fact that the second line starts + * with a digit-period-space sequence. + * + * Whereas when we're inside a list (or sub-list), that line will be + * treated as the start of a sub-list. What a kludge, huh? This is + * an aspect of Markdown's syntax that's hard to parse perfectly + * without resorting to mind-reading. Perhaps the solution is to + * change the syntax rules such that sub-lists must start with a + * starting cardinal number; e.g. "1." or "a.". + */ + $this->list_level++; + + // Trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array($this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + + /** + * List item parsing callback + * @param array $matches + * @return string + */ + protected function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + // Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } else { + // Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = $this->formParagraphs($item, false); + } + + return "<li>" . $item . "</li>\n"; + } + + /** + * Process Markdown `<pre><code>` blocks. + * @param string $text + * @return string + */ + protected function doCodeBlocks($text) { + $text = preg_replace_callback('{ + (?:\n\n|\A\n?) + ( # $1 = the code block -- one or more lines, starting with a space/tab + (?> + [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc + }xm', + array($this, '_doCodeBlocks_callback'), $text); + + return $text; + } + + /** + * Code block parsing callback + * @param array $matches + * @return string + */ + protected function _doCodeBlocks_callback($matches) { + $codeblock = $matches[1]; + + $codeblock = $this->outdent($codeblock); + if ($this->code_block_content_func) { + $codeblock = call_user_func($this->code_block_content_func, $codeblock, ""); + } else { + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + } + + # trim leading newlines and trailing newlines + $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock); + + $codeblock = "<pre><code>$codeblock\n</code></pre>"; + return "\n\n" . $this->hashBlock($codeblock) . "\n\n"; + } + + /** + * Create a code span markup for $code. Called from handleSpanToken. + * @param string $code + * @return string + */ + protected function makeCodeSpan($code) { + if ($this->code_span_content_func) { + $code = call_user_func($this->code_span_content_func, $code); + } else { + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + } + return $this->hashPart("<code>$code</code>"); + } + + /** + * Define the emphasis operators with their regex matches + * @var array + */ + protected $em_relist = array( + '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?![\.,:;]?\s)', + '*' => '(?<![\s*])\*(?!\*)', + '_' => '(?<![\s_])_(?!_)', + ); + + /** + * Define the strong operators with their regex matches + * @var array + */ + protected $strong_relist = array( + '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?![\.,:;]?\s)', + '**' => '(?<![\s*])\*\*(?!\*)', + '__' => '(?<![\s_])__(?!_)', + ); + + /** + * Define the emphasis + strong operators with their regex matches + * @var array + */ + protected $em_strong_relist = array( + '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?![\.,:;]?\s)', + '***' => '(?<![\s*])\*\*\*(?!\*)', + '___' => '(?<![\s_])___(?!_)', + ); + + /** + * Container for prepared regular expressions + * @var array + */ + protected $em_strong_prepared_relist; + + /** + * Prepare regular expressions for searching emphasis tokens in any + * context. + * @return void + */ + protected function prepareItalicsAndBold() { + foreach ($this->em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + // Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + // Construct master expression from list. + $token_re = '{(' . implode('|', $token_relist) . ')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + /** + * Convert Markdown italics (emphasis) and bold (strong) to HTML + * @param string $text + * @return string + */ + protected function doItalicsAndBold($text) { + if ($this->in_emphasis_processing) { + return $text; // avoid reentrency + } + $this->in_emphasis_processing = true; + + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + // Get prepared regular expression for seraching emphasis tokens + // in current context. + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + // Each loop iteration search for the next emphasis token. + // Each token is then passed to handleSpanToken. + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + // Reached end of text span: empty stack without emitting. + // any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + // Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + // Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong><em>$span</em></strong>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + // Other closing marker: close one em or strong and + // change current token state to match the other + $token_stack[0] = str_repeat($token{0}, 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; // $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + // Reached closing marker for both em and strong. + // Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; // $$tag stands for $em or $strong + } + } else { + // Reached opening three-char emphasis marker. Push on token + // stack; will be handled by the special condition above. + $em = $token{0}; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + // Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + $em = ''; + } + // Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong>$span</strong>"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + // Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + // Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<em>$span</em>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + $this->in_emphasis_processing = false; + return $text_stack[0]; + } + + /** + * Parse Markdown blockquotes to HTML + * @param string $text + * @return string + */ + protected function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array($this, '_doBlockQuotes_callback'), $text); + + return $text; + } + + /** + * Blockquote parsing callback + * @param array $matches + * @return string + */ + protected function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + // trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); // recurse + + $bq = preg_replace('/^/m', " ", $bq); + // These leading spaces cause problem with <pre> content, + // so we need to fix that: + $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx', + array($this, '_doBlockQuotes_callback2'), $bq); + + return "\n" . $this->hashBlock("<blockquote>\n$bq\n</blockquote>") . "\n\n"; + } + + /** + * Blockquote parsing callback + * @param array $matches + * @return string + */ + protected function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + /** + * Parse paragraphs + * + * @param string $text String to process in paragraphs + * @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags + * @return string + */ + protected function formParagraphs($text, $wrap_in_p = true) { + // Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + // Wrap <p> tags and unhashify HTML blocks + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + // Is a paragraph. + $value = $this->runSpanGamut($value); + if ($wrap_in_p) { + $value = preg_replace('/^([ ]*)/', "<p>", $value); + $value .= "</p>"; + } + $grafs[$key] = $this->unhash($value); + } else { + // Is a block. + // Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 = <div> tag +// <div \s+ +// [^>]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (</div>) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// // We can't call Markdown(), because that resets the hash; +// // that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// // Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + /** + * Encode text for a double-quoted HTML attribute. This function + * is *not* suitable for attributes enclosed in single quotes. + * @param string $text + * @return string + */ + protected function encodeAttribute($text) { + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '&quot;', $text); + return $text; + } + + /** + * Encode text for a double-quoted HTML attribute containing a URL, + * applying the URL filter if set. Also generates the textual + * representation for the URL (removing mailto: or tel:) storing it in $text. + * This function is *not* suitable for attributes enclosed in single quotes. + * + * @param string $url + * @param string &$text Passed by reference + * @return string URL + */ + protected function encodeURLAttribute($url, &$text = null) { + if ($this->url_filter_func) { + $url = call_user_func($this->url_filter_func, $url); + } + + if (preg_match('{^mailto:}i', $url)) { + $url = $this->encodeEntityObfuscatedAttribute($url, $text, 7); + } else if (preg_match('{^tel:}i', $url)) { + $url = $this->encodeAttribute($url); + $text = substr($url, 4); + } else { + $url = $this->encodeAttribute($url); + $text = $url; + } + + return $url; + } + + /** + * Smart processing for ampersands and angle brackets that need to + * be encoded. Valid character entities are left alone unless the + * no-entities mode is set. + * @param string $text + * @return string + */ + protected function encodeAmpsAndAngles($text) { + if ($this->no_entities) { + $text = str_replace('&', '&amp;', $text); + } else { + // Ampersand-encoding based entirely on Nat Irons's Amputator + // MT plugin: <http://bumppo.net/projects/amputator/> + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&amp;', $text); + } + // Encode remaining <'s + $text = str_replace('<', '&lt;', $text); + + return $text; + } + + /** + * Parse Markdown automatic links to anchor HTML tags + * @param string $text + * @return string + */ + protected function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i', + array($this, '_doAutoLinks_url_callback'), $text); + + // Email addresses: <address@domain.foo> + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + (?: + [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ + | + ".*?" + ) + \@ + (?: + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + | + \[[\d.a-fA-F:]+\] # IPv4 & IPv6 + ) + ) + > + }xi', + array($this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + + /** + * Parse URL callback + * @param array $matches + * @return string + */ + protected function _doAutoLinks_url_callback($matches) { + $url = $this->encodeURLAttribute($matches[1], $text); + $link = "<a href=\"$url\">$text</a>"; + return $this->hashPart($link); + } + + /** + * Parse email address callback + * @param array $matches + * @return string + */ + protected function _doAutoLinks_email_callback($matches) { + $addr = $matches[1]; + $url = $this->encodeURLAttribute("mailto:$addr", $text); + $link = "<a href=\"$url\">$text</a>"; + return $this->hashPart($link); + } + + /** + * Input: some text to obfuscate, e.g. "mailto:foo@example.com" + * + * Output: the same text but with most characters encoded as either a + * decimal or hex entity, in the hopes of foiling most address + * harvesting spam bots. E.g.: + * + * &#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111; + * &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111; + * &#x6d; + * + * Note: the additional output $tail is assigned the same value as the + * ouput, minus the number of characters specified by $head_length. + * + * Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + * With some optimizations by Milian Wolff. Forced encoding of HTML + * attribute special characters by Allan Odgaard. + * + * @param string $text + * @param string &$tail + * @param integer $head_length + * @return string + */ + protected function encodeEntityObfuscatedAttribute($text, &$tail = null, $head_length = 0) { + if ($text == "") { + return $tail = ""; + } + + $chars = preg_split('/(?<!^)(?!$)/', $text); + $seed = (int)abs(crc32($text) / strlen($text)); // Deterministic seed. + + foreach ($chars as $key => $char) { + $ord = ord($char); + // Ignore non-ascii chars. + if ($ord < 128) { + $r = ($seed * (1 + $key)) % 100; // Pseudo-random function. + // roughly 10% raw, 45% hex, 45% dec + // '@' *must* be encoded. I insist. + // '"' and '>' have to be encoded inside the attribute + if ($r > 90 && strpos('@"&>', $char) === false) { + /* do nothing */ + } else if ($r < 45) { + $chars[$key] = '&#x'.dechex($ord).';'; + } else { + $chars[$key] = '&#'.$ord.';'; + } + } + } + + $text = implode('', $chars); + $tail = $head_length ? implode('', array_slice($chars, $head_length)) : $text; + + return $text; + } + + /** + * Take the string $str and parse it into tokens, hashing embeded HTML, + * escaped characters and handling code spans. + * @param string $str + * @return string + */ + protected function parseSpan($str) { + $output = ''; + + $span_re = '{ + ( + \\\\'.$this->escape_chars_re.' + | + (?<![`\\\\]) + `+ # code span marker + '.( $this->no_markup ? '' : ' + | + <!-- .*? --> # comment + | + <\?.*?\?> | <%.*?%> # processing instruction + | + <[!$]?[-a-zA-Z0-9:_]+ # regular tags + (?> + \s + (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* + )? + > + | + <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag + | + </[-a-zA-Z0-9:_]+\s*> # closing tag + ').' + ) + }xs'; + + while (1) { + // Each loop iteration seach for either the next tag, the next + // openning code span marker, or the next escaped character. + // Each token is then passed to handleSpanToken. + $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); + + // Create token from text preceding tag. + if ($parts[0] != "") { + $output .= $parts[0]; + } + + // Check if we reach the end. + if (isset($parts[1])) { + $output .= $this->handleSpanToken($parts[1], $parts[2]); + $str = $parts[2]; + } else { + break; + } + } + + return $output; + } + + /** + * Handle $token provided by parseSpan by determining its nature and + * returning the corresponding value that should replace it. + * @param string $token + * @param string &$str + * @return string + */ + protected function handleSpanToken($token, &$str) { + switch ($token{0}) { + case "\\": + return $this->hashPart("&#". ord($token{1}). ";"); + case "`": + // Search for end marker in remaining text. + if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', + $str, $matches)) + { + $str = $matches[2]; + $codespan = $this->makeCodeSpan($matches[1]); + return $this->hashPart($codespan); + } + return $token; // Return as text since no ending marker found. + default: + return $this->hashPart($token); + } + } + + /** + * Remove one level of line-leading tabs or spaces + * @param string $text + * @return string + */ + protected function outdent($text) { + return preg_replace('/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text); + } + + + /** + * String length function for detab. `_initDetab` will create a function to + * handle UTF-8 if the default function does not exist. + * @var string + */ + protected $utf8_strlen = 'mb_strlen'; + + /** + * Replace tabs with the appropriate amount of spaces. + * + * For each line we separate the line in blocks delemited by tab characters. + * Then we reconstruct every line by adding the appropriate number of space + * between each blocks. + * + * @param string $text + * @return string + */ + protected function detab($text) { + $text = preg_replace_callback('/^.*\t.*$/m', + array($this, '_detab_callback'), $text); + + return $text; + } + + /** + * Replace tabs callback + * @param string $matches + * @return string + */ + protected function _detab_callback($matches) { + $line = $matches[0]; + $strlen = $this->utf8_strlen; // strlen function for UTF-8. + + // Split in blocks. + $blocks = explode("\t", $line); + // Add each blocks to the line. + $line = $blocks[0]; + unset($blocks[0]); // Do not add first block twice. + foreach ($blocks as $block) { + // Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - + $strlen($line, 'UTF-8') % $this->tab_width; + $line .= str_repeat(" ", $amount) . $block; + } + return $line; + } + + /** + * Check for the availability of the function in the `utf8_strlen` property + * (initially `mb_strlen`). If the function is not available, create a + * function that will loosely count the number of UTF-8 characters with a + * regular expression. + * @return void + */ + protected function _initDetab() { + + if (function_exists($this->utf8_strlen)) { + return; + } + + $this->utf8_strlen = function($text) { + return preg_match_all('/[\x00-\xBF]|[\xC0-\xFF][\x80-\xBF]*/', $text, $m); + }; + } + + /** + * Swap back in all the tags hashed by _HashHTMLBlocks. + * @param string $text + * @return string + */ + protected function unhash($text) { + return preg_replace_callback('/(.)\x1A[0-9]+\1/', + array($this, '_unhash_callback'), $text); + } + + /** + * Unhashing callback + * @param array $matches + * @return string + */ + protected function _unhash_callback($matches) { + return $this->html_hashes[$matches[0]]; + } +} + +class MarkdownExtraParser extends MarkdownParser { + /** + * Configuration variables + */ + + /** + * Prefix for footnote ids. + * @var string + */ + public $fn_id_prefix = ""; + + /** + * Optional title attribute for footnote links and backlinks. + * @var string + */ + public $fn_link_title = ""; + public $fn_backlink_title = ""; + + /** + * Optional class attribute for footnote links and backlinks. + * @var string + */ + public $fn_link_class = "footnote-ref"; + public $fn_backlink_class = "footnote-backref"; + + /** + * Content to be displayed within footnote backlinks. The default is '↩'; + * the U+FE0E on the end is a Unicode variant selector used to prevent iOS + * from displaying the arrow character as an emoji. + * @var string + */ + public $fn_backlink_html = '&#8617;&#xFE0E;'; + + /** + * Class name for table cell alignment (%% replaced left/center/right) + * For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center' + * If empty, the align attribute is used instead of a class name. + * @var string + */ + public $table_align_class_tmpl = ''; + + /** + * Optional class prefix for fenced code block. + * @var string + */ + public $code_class_prefix = ""; + + /** + * Class attribute for code blocks goes on the `code` tag; + * setting this to true will put attributes on the `pre` tag instead. + * @var boolean + */ + public $code_attr_on_pre = false; + + /** + * Predefined abbreviations. + * @var array + */ + public $predef_abbr = array(); + + /** + * Only convert atx-style headers if there's a space between the header and # + * @var boolean + */ + public $hashtag_protection = false; + + /** + * Parser implementation + */ + + /** + * Constructor function. Initialize the parser object. + * @return void + */ + public function __construct() { + // Add extra escapable characters before parent constructor + // initialize the table. + $this->escape_chars .= ':|'; + + // Insert extra document, block, and span transformations. + // Parent constructor will do the sorting. + $this->document_gamut += array( + "doFencedCodeBlocks" => 5, + "stripFootnotes" => 15, + "stripAbbreviations" => 25, + "appendFootnotes" => 50, + ); + $this->block_gamut += array( + "doFencedCodeBlocks" => 5, + "doTables" => 15, + "doDefLists" => 45, + ); + $this->span_gamut += array( + "doFootnotes" => 5, + "doAbbreviations" => 70, + ); + + $this->enhanced_ordered_list = true; + parent::__construct(); + } + + + /** + * Extra variables used during extra transformations. + * @var array + */ + protected $footnotes = array(); + protected $footnotes_ordered = array(); + protected $footnotes_ref_count = array(); + protected $footnotes_numbers = array(); + protected $abbr_desciptions = array(); + /** @var string */ + protected $abbr_word_re = ''; + + /** + * Give the current footnote number. + * @var integer + */ + protected $footnote_counter = 1; + + /** + * Setting up Extra-specific variables. + */ + protected function setup() { + parent::setup(); + + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + $this->footnote_counter = 1; + + foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + } + } + + /** + * Clearing Extra-specific variables. + */ + protected function teardown() { + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + + parent::teardown(); + } + + + /** + * Extra attribute parser + */ + + /** + * Expression to use to catch attributes (includes the braces) + * @var string + */ + protected $id_class_attr_catch_re = '\{((?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}'; + + /** + * Expression to use when parsing in a context when no capture is desired + * @var string + */ + protected $id_class_attr_nocatch_re = '\{(?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}'; + + /** + * Parse attributes caught by the $this->id_class_attr_catch_re expression + * and return the HTML-formatted list of attributes. + * + * Currently supported attributes are .class and #id. + * + * In addition, this method also supports supplying a default Id value, + * which will be used to populate the id attribute in case it was not + * overridden. + * @param string $tag_name + * @param string $attr + * @param mixed $defaultIdValue + * @param array $classes + * @return string + */ + protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = array()) { + if (empty($attr) && !$defaultIdValue && empty($classes)) return ""; + + // Split on components + preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches); + $elements = $matches[0]; + + // Handle classes and IDs (only first ID taken into account) + $attributes = array(); + $id = false; + foreach ($elements as $element) { + if ($element{0} == '.') { + $classes[] = substr($element, 1); + } else if ($element{0} == '#') { + if ($id === false) $id = substr($element, 1); + } else if (strpos($element, '=') > 0) { + $parts = explode('=', $element, 2); + $attributes[] = $parts[0] . '="' . $parts[1] . '"'; + } + } + + if (!$id) $id = $defaultIdValue; + + // Compose attributes as string + $attr_str = ""; + if (!empty($id)) { + $attr_str .= ' id="'.$this->encodeAttribute($id) .'"'; + } + if (!empty($classes)) { + $attr_str .= ' class="'. implode(" ", $classes) . '"'; + } + if (!$this->no_markup && !empty($attributes)) { + $attr_str .= ' '.implode(" ", $attributes); + } + return $attr_str; + } + + /** + * Strips link definitions from text, stores the URLs and titles in + * hash references. + * @param string $text + * @return string + */ + protected function stripLinkDefinitions($text) { + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr + (?:\n+|\Z) + }xm', + array($this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + + /** + * Strip link definition callback + * @param array $matches + * @return string + */ + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]); + return ''; // String that will replace the block + } + + + /** + * HTML block parser + */ + + /** + * Tags that are always treated as block tags + * @var string + */ + protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure'; + + /** + * Tags treated as block tags only if the opening tag is alone on its line + * @var string + */ + protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; + + /** + * Tags where markdown="1" default to span mode: + * @var string + */ + protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; + + /** + * Tags which must not have their contents modified, no matter where + * they appear + * @var string + */ + protected $clean_tags_re = 'script|style|math|svg'; + + /** + * Tags that do not need to be closed. + * @var string + */ + protected $auto_close_tags_re = 'hr|img|param|source|track'; + + /** + * Hashify HTML Blocks and "clean tags". + * + * We only want to do this for block-level HTML tags, such as headers, + * lists, and tables. That's because we still want to wrap <p>s around + * "paragraphs" that are wrapped in non-block-level tags, such as anchors, + * phrase emphasis, and spans. The list of tags we're looking for is + * hard-coded. + * + * This works by calling _HashHTMLBlocks_InMarkdown, which then calls + * _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" + * attribute is found within a tag, _HashHTMLBlocks_InHTML calls back + * _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. + * These two functions are calling each other. It's recursive! + * @param string $text + * @return string + */ + protected function hashHTMLBlocks($text) { + if ($this->no_markup) { + return $text; + } + + // Call the HTML-in-Markdown hasher. + list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); + + return $text; + } + + /** + * Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. + * + * * $indent is the number of space to be ignored when checking for code + * blocks. This is important because if we don't take the indent into + * account, something like this (which looks right) won't work as expected: + * + * <div> + * <div markdown="1"> + * Hello World. <-- Is this a Markdown code block or text? + * </div> <-- Is this a Markdown code block or a real tag? + * <div> + * + * If you don't like this, just don't indent the tag on which + * you apply the markdown="1" attribute. + * + * * If $enclosing_tag_re is not empty, stops at the first unmatched closing + * tag with that name. Nested tags supported. + * + * * If $span is true, text inside must treated as span. So any double + * newline will be replaced by a single newline so that it does not create + * paragraphs. + * + * Returns an array of that form: ( processed text , remaining text ) + * + * @param string $text + * @param integer $indent + * @param string $enclosing_tag_re + * @param boolean $span + * @return array + */ + protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0, + $enclosing_tag_re = '', $span = false) + { + + if ($text === '') return array('', ''); + + // Regex to check for the presense of newlines around a block tag. + $newline_before_re = '/(?:^\n?|\n\n)*$/'; + $newline_after_re = + '{ + ^ # Start of text following the tag. + (?>[ ]*<!--.*?-->)? # Optional comment. + [ ]*\n # Must be followed by newline. + }xs'; + + // Regex to match any tag. + $block_tag_re = + '{ + ( # $2: Capture whole tag. + </? # Any opening or closing tag. + (?> # Tag name. + ' . $this->block_tags_re . ' | + ' . $this->context_block_tags_re . ' | + ' . $this->clean_tags_re . ' | + (?!\s)'.$enclosing_tag_re . ' + ) + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + <!-- .*? --> # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + <!\[CDATA\[.*?\]\]> # CData Block + ' . ( !$span ? ' # If not in span. + | + # Indented code block + (?: ^[ ]*\n | ^ | \n[ ]*\n ) + [ ]{' . ($indent + 4) . '}[^\n]* \n + (?> + (?: [ ]{' . ($indent + 4) . '}[^\n]* | [ ]* ) \n + )* + | + # Fenced code block marker + (?<= ^ | \n ) + [ ]{0,' . ($indent + 3) . '}(?:~{3,}|`{3,}) + [ ]* + (?: \.?[-_:a-zA-Z0-9]+ )? # standalone class name + [ ]* + (?: ' . $this->id_class_attr_nocatch_re . ' )? # extra attributes + [ ]* + (?= \n ) + ' : '' ) . ' # End (if not is span). + | + # Code span marker + # Note, this regex needs to go after backtick fenced + # code blocks but it should also be kept outside of the + # "if not in span" condition adding backticks to the parser + `+ + ) + }xs'; + + + $depth = 0; // Current depth inside the tag tree. + $parsed = ""; // Parsed text that will be returned. + + // Loop through every tag until we find the closing tag of the parent + // or loop until reaching the end of text if no parent tag specified. + do { + // Split the text using the first $tag_match pattern found. + // Text before pattern will be first in the array, text after + // pattern will be at the end, and between will be any catches made + // by the pattern. + $parts = preg_split($block_tag_re, $text, 2, + PREG_SPLIT_DELIM_CAPTURE); + + // If in Markdown span mode, add a empty-string span-level hash + // after each newline to prevent triggering any block element. + if ($span) { + $void = $this->hashPart("", ':'); + $newline = "\n$void"; + $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; + } + + $parsed .= $parts[0]; // Text before current tag. + + // If end of $text has been reached. Stop loop. + if (count($parts) < 3) { + $text = ""; + break; + } + + $tag = $parts[1]; // Tag to handle. + $text = $parts[2]; // Remaining text after current tag. + $tag_re = preg_quote($tag); // For use in a regular expression. + + // Check for: Fenced code block marker. + // Note: need to recheck the whole tag to disambiguate backtick + // fences from code spans + if (preg_match('{^\n?([ ]{0,' . ($indent + 3) . '})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+)?[ ]*(?:' . $this->id_class_attr_nocatch_re . ')?[ ]*\n?$}', $tag, $capture)) { + // Fenced code block marker: find matching end marker. + $fence_indent = strlen($capture[1]); // use captured indent in re + $fence_re = $capture[2]; // use captured fence in re + if (preg_match('{^(?>.*\n)*?[ ]{' . ($fence_indent) . '}' . $fence_re . '[ ]*(?:\n|$)}', $text, + $matches)) + { + // End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + // No end marker: just skip it. + $parsed .= $tag; + } + } + // Check for: Indented code block. + else if ($tag{0} == "\n" || $tag{0} == " ") { + // Indented code block: pass it unchanged, will be handled + // later. + $parsed .= $tag; + } + // Check for: Code span marker + // Note: need to check this after backtick fenced code blocks + else if ($tag{0} == "`") { + // Find corresponding end marker. + $tag_re = preg_quote($tag); + if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)' . $tag_re . '(?!`)}', + $text, $matches)) + { + // End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + // Unmatched marker: just skip it. + $parsed .= $tag; + } + } + // Check for: Opening Block level tag or + // Opening Context Block tag (like ins and del) + // used as a block tag (tag is alone on it's line). + else if (preg_match('{^<(?:' . $this->block_tags_re . ')\b}', $tag) || + ( preg_match('{^<(?:' . $this->context_block_tags_re . ')\b}', $tag) && + preg_match($newline_before_re, $parsed) && + preg_match($newline_after_re, $text) ) + ) + { + // Need to parse tag and following text using the HTML parser. + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); + + // Make sure it stays outside of any paragraph by adding newlines. + $parsed .= "\n\n$block_text\n\n"; + } + // Check for: Clean tag (like script, math) + // HTML Comments, processing instructions. + else if (preg_match('{^<(?:' . $this->clean_tags_re . ')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + // Need to parse tag and following text using the HTML parser. + // (don't check for markdown attribute) + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); + + $parsed .= $block_text; + } + // Check for: Tag with same name as enclosing tag. + else if ($enclosing_tag_re !== '' && + // Same name as enclosing tag. + preg_match('{^</?(?:' . $enclosing_tag_re . ')\b}', $tag)) + { + // Increase/decrease nested tag count. + if ($tag{1} == '/') $depth--; + else if ($tag{strlen($tag)-2} != '/') $depth++; + + if ($depth < 0) { + // Going out of parent element. Clean up and break so we + // return to the calling function. + $text = $tag . $text; + break; + } + + $parsed .= $tag; + } + else { + $parsed .= $tag; + } + } while ($depth >= 0); + + return array($parsed, $text); + } + + /** + * Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. + * + * * Calls $hash_method to convert any blocks. + * * Stops when the first opening tag closes. + * * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. + * (it is not inside clean tags) + * + * Returns an array of that form: ( processed text , remaining text ) + * @param string $text + * @param string $hash_method + * @param string $md_attr + * @return array + */ + protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { + if ($text === '') return array('', ''); + + // Regex to match `markdown` attribute inside of a tag. + $markdown_attr_re = ' + { + \s* # Eat whitespace before the `markdown` attribute + markdown + \s*=\s* + (?> + (["\']) # $1: quote delimiter + (.*?) # $2: attribute value + \1 # matching delimiter + | + ([^\s>]*) # $3: unquoted attribute value + ) + () # $4: make $3 always defined (avoid warnings) + }xs'; + + // Regex to match any tag. + $tag_re = '{ + ( # $2: Capture whole tag. + </? # Any opening or closing tag. + [\w:$]+ # Tag name. + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + <!-- .*? --> # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + <!\[CDATA\[.*?\]\]> # CData Block + ) + }xs'; + + $original_text = $text; // Save original text in case of faliure. + + $depth = 0; // Current depth inside the tag tree. + $block_text = ""; // Temporary text holder for current text. + $parsed = ""; // Parsed text that will be returned. + + // Get the name of the starting tag. + // (This pattern makes $base_tag_name_re safe without quoting.) + if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) + $base_tag_name_re = $matches[1]; + + // Loop through every tag until we find the corresponding closing tag. + do { + // Split the text using the first $tag_match pattern found. + // Text before pattern will be first in the array, text after + // pattern will be at the end, and between will be any catches made + // by the pattern. + $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (count($parts) < 3) { + // End of $text reached with unbalenced tag(s). + // In that case, we return original text unchanged and pass the + // first character as filtered to prevent an infinite loop in the + // parent function. + return array($original_text{0}, substr($original_text, 1)); + } + + $block_text .= $parts[0]; // Text before current tag. + $tag = $parts[1]; // Tag to handle. + $text = $parts[2]; // Remaining text after current tag. + + // Check for: Auto-close tag (like <hr/>) + // Comments and Processing Instructions. + if (preg_match('{^</?(?:' . $this->auto_close_tags_re . ')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + // Just add the tag to the block as if it was text. + $block_text .= $tag; + } + else { + // Increase/decrease nested tag count. Only do so if + // the tag's name match base tag's. + if (preg_match('{^</?' . $base_tag_name_re . '\b}', $tag)) { + if ($tag{1} == '/') $depth--; + else if ($tag{strlen($tag)-2} != '/') $depth++; + } + + // Check for `markdown="1"` attribute and handle it. + if ($md_attr && + preg_match($markdown_attr_re, $tag, $attr_m) && + preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3])) + { + // Remove `markdown` attribute from opening tag. + $tag = preg_replace($markdown_attr_re, '', $tag); + + // Check if text inside this tag must be parsed in span mode. + $this->mode = $attr_m[2] . $attr_m[3]; + $span_mode = $this->mode == 'span' || $this->mode != 'block' && + preg_match('{^<(?:' . $this->contain_span_tags_re . ')\b}', $tag); + + // Calculate indent before tag. + if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { + $strlen = $this->utf8_strlen; + $indent = $strlen($matches[1], 'UTF-8'); + } else { + $indent = 0; + } + + // End preceding block with this tag. + $block_text .= $tag; + $parsed .= $this->$hash_method($block_text); + + // Get enclosing tag name for the ParseMarkdown function. + // (This pattern makes $tag_name_re safe without quoting.) + preg_match('/^<([\w:$]*)\b/', $tag, $matches); + $tag_name_re = $matches[1]; + + // Parse the content using the HTML-in-Markdown parser. + list ($block_text, $text) + = $this->_hashHTMLBlocks_inMarkdown($text, $indent, + $tag_name_re, $span_mode); + + // Outdent markdown text. + if ($indent > 0) { + $block_text = preg_replace("/^[ ]{1,$indent}/m", "", + $block_text); + } + + // Append tag content to parsed text. + if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; + else $parsed .= "$block_text"; + + // Start over with a new block. + $block_text = ""; + } + else $block_text .= $tag; + } + + } while ($depth > 0); + + // Hash last block text that wasn't processed inside the loop. + $parsed .= $this->$hash_method($block_text); + + return array($parsed, $text); + } + + /** + * Called whenever a tag must be hashed when a function inserts a "clean" tag + * in $text, it passes through this function and is automaticaly escaped, + * blocking invalid nested overlap. + * @param string $text + * @return string + */ + protected function hashClean($text) { + return $this->hashPart($text, 'C'); + } + + /** + * Turn Markdown link shortcuts into XHTML <a> tags. + * @param string $text + * @return string + */ + protected function doAnchors($text) { + if ($this->in_anchor) { + return $text; + } + $this->in_anchor = true; + + // First, handle reference-style links: [link text] [id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + (' . $this->nested_brackets_re . ') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + // Next, inline-style links: [link text](url "optional title") + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + (' . $this->nested_brackets_re . ') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + (' . $this->nested_url_parenthesis_re . ') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes + ) + }xs', + array($this, '_doAnchors_inline_callback'), $text); + + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link text][1] + // or [link text](/foo) + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array($this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + + /** + * Callback for reference anchors + * @param array $matches + * @return string + */ + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + // for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + // lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if ( isset( $this->titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + + /** + * Callback for inline anchors + * @param array $matches + * @return string + */ + protected function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + + // if the URL was of the form <s p a c e s> it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using the URL. + $unhashed = $this->unhash($url); + if ($unhashed != $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $attr; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + /** + * Turn Markdown image shortcuts into <img> tags. + * @param string $text + * @return string + */ + protected function doImages($text) { + // First, handle reference-style labeled images: ![alt text][id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + (' . $this->nested_brackets_re . ') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array($this, '_doImages_reference_callback'), $text); + + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + (' . $this->nested_brackets_re . ') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + (' . $this->nested_url_parenthesis_re . ') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes + ) + }xs', + array($this, '_doImages_inline_callback'), $text); + + return $text; + } + + /** + * Callback for referenced images + * @param array $matches + * @return string + */ + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); // for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeURLAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + // If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + + /** + * Callback for inline images + * @param array $matches + * @return string + */ + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeURLAttribute($url); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; // $title already quoted + } + $result .= $attr; + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + /** + * Process markdown headers. Redefined to add ID and class attribute support. + * @param string $text + * @return string + */ + protected function doHeaders($text) { + // Setext-style headers: + // Header 1 {#header1} + // ======== + // + // Header 2 {#header2 .class1 .class2} + // -------- + // + $text = preg_replace_callback( + '{ + (^.+?) # $1: Header text + (?:[ ]+ ' . $this->id_class_attr_catch_re . ' )? # $3 = id/class attributes + [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer + }mx', + array($this, '_doHeaders_callback_setext'), $text); + + // atx-style headers: + // # Header 1 {#header1} + // ## Header 2 {#header2} + // ## Header 2 with closing hashes ## {#header3.class1.class2} + // ... + // ###### Header 6 {.class2} + // + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]'.($this->hashtag_protection ? '+' : '*').' + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + (?:[ ]+ ' . $this->id_class_attr_catch_re . ' )? # $3 = id/class attributes + [ ]* + \n+ + }xm', + array($this, '_doHeaders_callback_atx'), $text); + + return $text; + } + + /** + * Callback for setext headers + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_setext($matches) { + if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) { + return $matches[0]; + } + + $level = $matches[3]{0} == '=' ? 1 : 2; + + $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null; + + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId); + $block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * Callback for atx headers + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + + $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null; + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId); + $block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * Form HTML tables. + * @param string $text + * @return string + */ + protected function doTables($text) { + $less_than_tab = $this->tab_width - 1; + // Find tables with leading pipe. + // + // | Header 1 | Header 2 + // | -------- | -------- + // | Cell 1 | Cell 2 + // | Cell 3 | Cell 4 + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + [|] # Optional leading pipe (present) + (.+) \n # $1: Header row (at least one pipe) + + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + [ ]* # Allowed whitespace. + [|] .* \n # Row content. + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array($this, '_doTable_leadingPipe_callback'), $text); + + // Find tables without leading pipe. + // + // Header 1 | Header 2 + // -------- | -------- + // Cell 1 | Cell 2 + // Cell 3 | Cell 4 + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + (\S.*[|].*) \n # $1: Header row (at least one pipe) + + [ ]{0,' . $less_than_tab . '} # Allowed whitespace. + ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + .* [|] .* \n # Row content + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array($this, '_DoTable_callback'), $text); + + return $text; + } + + /** + * Callback for removing the leading pipe for each row + * @param array $matches + * @return string + */ + protected function _doTable_leadingPipe_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + $content = preg_replace('/^ *[|]/m', '', $content); + + return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); + } + + /** + * Make the align attribute in a table + * @param string $alignname + * @return string + */ + protected function _doTable_makeAlignAttr($alignname) + { + if (empty($this->table_align_class_tmpl)) { + return " align=\"$alignname\""; + } + + $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl); + return " class=\"$classname\""; + } + + /** + * Calback for processing tables + * @param array $matches + * @return string + */ + protected function _doTable_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + // Remove any tailing pipes for each line. + $head = preg_replace('/[|] *$/m', '', $head); + $underline = preg_replace('/[|] *$/m', '', $underline); + $content = preg_replace('/[|] *$/m', '', $content); + + // Reading alignement from header underline. + $separators = preg_split('/ *[|] */', $underline); + foreach ($separators as $n => $s) { + if (preg_match('/^ *-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('right'); + else if (preg_match('/^ *:-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('center'); + else if (preg_match('/^ *:-+ *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('left'); + else + $attr[$n] = ''; + } + + // Parsing span elements, including code spans, character escapes, + // and inline HTML tags, so that pipes inside those gets ignored. + $head = $this->parseSpan($head); + $headers = preg_split('/ *[|] */', $head); + $col_count = count($headers); + $attr = array_pad($attr, $col_count, ''); + + // Write column headers. + $text = "<table>\n"; + $text .= "<thead>\n"; + $text .= "<tr>\n"; + foreach ($headers as $n => $header) + $text .= " <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n"; + $text .= "</tr>\n"; + $text .= "</thead>\n"; + + // Split content by row. + $rows = explode("\n", trim($content, "\n")); + + $text .= "<tbody>\n"; + foreach ($rows as $row) { + // Parsing span elements, including code spans, character escapes, + // and inline HTML tags, so that pipes inside those gets ignored. + $row = $this->parseSpan($row); + + // Split row by cell. + $row_cells = preg_split('/ *[|] */', $row, $col_count); + $row_cells = array_pad($row_cells, $col_count, ''); + + $text .= "<tr>\n"; + foreach ($row_cells as $n => $cell) + $text .= " <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n"; + $text .= "</tr>\n"; + } + $text .= "</tbody>\n"; + $text .= "</table>"; + + return $this->hashBlock($text) . "\n"; + } + + /** + * Form HTML definition lists. + * @param string $text + * @return string + */ + protected function doDefLists($text) { + $less_than_tab = $this->tab_width - 1; + + // Re-usable pattern to match any entire dl list: + $whole_list_re = '(?> + ( # $1 = whole list + ( # $2 + [ ]{0,' . $less_than_tab . '} + ((?>.*\S.*\n)+) # $3 = defined term + \n? + [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another term + [ ]{0,' . $less_than_tab . '} + (?: \S.*\n )+? # defined term + \n? + [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition + ) + (?! # Negative lookahead for another definition + [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition + ) + ) + ) + )'; // mx + + $text = preg_replace_callback('{ + (?>\A\n?|(?<=\n\n)) + ' . $whole_list_re . ' + }mx', + array($this, '_doDefLists_callback'), $text); + + return $text; + } + + /** + * Callback for processing definition lists + * @param array $matches + * @return string + */ + protected function _doDefLists_callback($matches) { + // Re-usable patterns to match list item bullets and number markers: + $list = $matches[1]; + + // Turn double returns into triple returns, so that we can make a + // paragraph for the last item in a list, if necessary: + $result = trim($this->processDefListItems($list)); + $result = "<dl>\n" . $result . "\n</dl>"; + return $this->hashBlock($result) . "\n\n"; + } + + /** + * Process the contents of a single definition list, splitting it + * into individual term and definition list items. + * @param string $list_str + * @return string + */ + protected function processDefListItems($list_str) { + + $less_than_tab = $this->tab_width - 1; + + // Trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + // Process definition terms. + $list_str = preg_replace_callback('{ + (?>\A\n?|\n\n+) # leading line + ( # definition terms = $1 + [ ]{0,' . $less_than_tab . '} # leading whitespace + (?!\:[ ]|[ ]) # negative lookahead for a definition + # mark (colon) or more whitespace. + (?> \S.* \n)+? # actual term (not whitespace). + ) + (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed + # with a definition mark. + }xm', + array($this, '_processDefListItems_callback_dt'), $list_str); + + // Process actual definitions. + $list_str = preg_replace_callback('{ + \n(\n+)? # leading line = $1 + ( # marker space = $2 + [ ]{0,' . $less_than_tab . '} # whitespace before colon + \:[ ]+ # definition mark (colon) + ) + ((?s:.+?)) # definition text = $3 + (?= \n+ # stop at next definition mark, + (?: # next term or end of text + [ ]{0,' . $less_than_tab . '} \:[ ] | + <dt> | \z + ) + ) + }xm', + array($this, '_processDefListItems_callback_dd'), $list_str); + + return $list_str; + } + + /** + * Callback for <dt> elements in definition lists + * @param array $matches + * @return string + */ + protected function _processDefListItems_callback_dt($matches) { + $terms = explode("\n", trim($matches[1])); + $text = ''; + foreach ($terms as $term) { + $term = $this->runSpanGamut(trim($term)); + $text .= "\n<dt>" . $term . "</dt>"; + } + return $text . "\n"; + } + + /** + * Callback for <dd> elements in definition lists + * @param array $matches + * @return string + */ + protected function _processDefListItems_callback_dd($matches) { + $leading_line = $matches[1]; + $marker_space = $matches[2]; + $def = $matches[3]; + + if ($leading_line || preg_match('/\n{2,}/', $def)) { + // Replace marker with the appropriate whitespace indentation + $def = str_repeat(' ', strlen($marker_space)) . $def; + $def = $this->runBlockGamut($this->outdent($def . "\n\n")); + $def = "\n". $def ."\n"; + } + else { + $def = rtrim($def); + $def = $this->runSpanGamut($this->outdent($def)); + } + + return "\n<dd>" . $def . "</dd>\n"; + } + + /** + * Adding the fenced code block syntax to regular Markdown: + * + * ~~~ + * Code block + * ~~~ + * + * @param string $text + * @return string + */ + protected function doFencedCodeBlocks($text) { + + $less_than_tab = $this->tab_width; + + $text = preg_replace_callback('{ + (?:\n|\A) + # 1: Opening marker + ( + (?:~{3,}|`{3,}) # 3 or more tildes/backticks. + ) + [ ]* + (?: + \.?([-_:a-zA-Z0-9]+) # 2: standalone class name + )? + [ ]* + (?: + ' . $this->id_class_attr_catch_re . ' # 3: Extra attributes + )? + [ ]* \n # Whitespace and newline following marker. + + # 4: Content + ( + (?> + (?!\1 [ ]* \n) # Not a closing marker. + .*\n+ + )+ + ) + + # Closing marker. + \1 [ ]* (?= \n ) + }xm', + array($this, '_doFencedCodeBlocks_callback'), $text); + + return $text; + } + + /** + * Callback to process fenced code blocks + * @param array $matches + * @return string + */ + protected function _doFencedCodeBlocks_callback($matches) { + $classname =& $matches[2]; + $attrs =& $matches[3]; + $codeblock = $matches[4]; + + if ($this->code_block_content_func) { + $codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname); + } else { + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + } + + $codeblock = preg_replace_callback('/^\n+/', + array($this, '_doFencedCodeBlocks_newlines'), $codeblock); + + $classes = array(); + if ($classname != "") { + if ($classname{0} == '.') + $classname = substr($classname, 1); + $classes[] = $this->code_class_prefix . $classname; + } + $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes); + $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; + $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; + $codeblock = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>"; + + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + + /** + * Replace new lines in fenced code blocks + * @param array $matches + * @return string + */ + protected function _doFencedCodeBlocks_newlines($matches) { + return str_repeat("<br$this->empty_element_suffix", + strlen($matches[0])); + } + + /** + * Redefining emphasis markers so that emphasis by underscore does not + * work in the middle of a word. + * @var array + */ + protected $em_relist = array( + '' => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)', + '*' => '(?<![\s*])\*(?!\*)', + '_' => '(?<![\s_])_(?![a-zA-Z0-9_])', + ); + protected $strong_relist = array( + '' => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)', + '**' => '(?<![\s*])\*\*(?!\*)', + '__' => '(?<![\s_])__(?![a-zA-Z0-9_])', + ); + protected $em_strong_relist = array( + '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)', + '***' => '(?<![\s*])\*\*\*(?!\*)', + '___' => '(?<![\s_])___(?![a-zA-Z0-9_])', + ); + + /** + * Parse text into paragraphs + * @param string $text String to process in paragraphs + * @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags + * @return string HTML output + */ + protected function formParagraphs($text, $wrap_in_p = true) { + // Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + // Wrap <p> tags and unhashify HTML blocks + foreach ($grafs as $key => $value) { + $value = trim($this->runSpanGamut($value)); + + // Check if this should be enclosed in a paragraph. + // Clean tag hashes & block tag hashes are left alone. + $is_p = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); + + if ($is_p) { + $value = "<p>$value</p>"; + } + $grafs[$key] = $value; + } + + // Join grafs in one text, then unhash HTML tags. + $text = implode("\n\n", $grafs); + + // Finish by removing any tag hashes still present in $text. + $text = $this->unhash($text); + + return $text; + } + + + /** + * Footnotes - Strips link definitions from text, stores the URLs and + * titles in hash references. + * @param string $text + * @return string + */ + protected function stripFootnotes($text) { + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: [^id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,' . $less_than_tab . '}\[\^(.+?)\][ ]?: # note_id = $1 + [ ]* + \n? # maybe *one* newline + ( # text = $2 (no blank lines allowed) + (?: + .+ # actual text + | + \n # newlines but + (?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker. + (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed + # by non-indented content + )* + ) + }xm', + array($this, '_stripFootnotes_callback'), + $text); + return $text; + } + + /** + * Callback for stripping footnotes + * @param array $matches + * @return string + */ + protected function _stripFootnotes_callback($matches) { + $note_id = $this->fn_id_prefix . $matches[1]; + $this->footnotes[$note_id] = $this->outdent($matches[2]); + return ''; // String that will replace the block + } + + /** + * Replace footnote references in $text [^id] with a special text-token + * which will be replaced by the actual footnote marker in appendFootnotes. + * @param string $text + * @return string + */ + protected function doFootnotes($text) { + if (!$this->in_anchor) { + $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); + } + return $text; + } + + /** + * Append footnote list to text + * @param string $text + * @return string + */ + protected function appendFootnotes($text) { + $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array($this, '_appendFootnotes_callback'), $text); + + if (!empty($this->footnotes_ordered)) { + $text .= "\n\n"; + $text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n"; + $text .= "<hr" . $this->empty_element_suffix . "\n"; + $text .= "<ol>\n\n"; + + $attr = ""; + if ($this->fn_backlink_class != "") { + $class = $this->fn_backlink_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_backlink_title != "") { + $title = $this->fn_backlink_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + $attr .= " aria-label=\"$title\""; + } + $attr .= " role=\"doc-backlink\""; + $backlink_text = $this->fn_backlink_html; + $num = 0; + + while (!empty($this->footnotes_ordered)) { + $footnote = reset($this->footnotes_ordered); + $note_id = key($this->footnotes_ordered); + unset($this->footnotes_ordered[$note_id]); + $ref_count = $this->footnotes_ref_count[$note_id]; + unset($this->footnotes_ref_count[$note_id]); + unset($this->footnotes[$note_id]); + + $footnote .= "\n"; // Need to append newline before parsing. + $footnote = $this->runBlockGamut("$footnote\n"); + $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array($this, '_appendFootnotes_callback'), $footnote); + + $attr = str_replace("%%", ++$num, $attr); + $note_id = $this->encodeAttribute($note_id); + + // Prepare backlink, multiple backlinks if multiple references + $backlink = "<a href=\"#fnref:$note_id\"$attr>$backlink_text</a>"; + for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) { + $backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>$backlink_text</a>"; + } + // Add backlink to last paragraph; create new paragraph if needed. + if (preg_match('{</p>$}', $footnote)) { + $footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>"; + } else { + $footnote .= "\n\n<p>$backlink</p>"; + } + + $text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n"; + $text .= $footnote . "\n"; + $text .= "</li>\n\n"; + } + + $text .= "</ol>\n"; + $text .= "</div>"; + } + return $text; + } + + /** + * Callback for appending footnotes + * @param array $matches + * @return string + */ + protected function _appendFootnotes_callback($matches) { + $node_id = $this->fn_id_prefix . $matches[1]; + + // Create footnote marker only if it has a corresponding footnote *and* + // the footnote hasn't been used by another marker. + if (isset($this->footnotes[$node_id])) { + $num =& $this->footnotes_numbers[$node_id]; + if (!isset($num)) { + // Transfer footnote content to the ordered list and give it its + // number + $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; + $this->footnotes_ref_count[$node_id] = 1; + $num = $this->footnote_counter++; + $ref_count_mark = ''; + } else { + $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1; + } + + $attr = ""; + if ($this->fn_link_class != "") { + $class = $this->fn_link_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_link_title != "") { + $title = $this->fn_link_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + $attr .= " role=\"doc-noteref\""; + + $attr = str_replace("%%", $num, $attr); + $node_id = $this->encodeAttribute($node_id); + + return + "<sup id=\"fnref$ref_count_mark:$node_id\">". + "<a href=\"#fn:$node_id\"$attr>$num</a>". + "</sup>"; + } + + return "[^" . $matches[1] . "]"; + } + + + /** + * Abbreviations - strips abbreviations from text, stores titles in hash + * references. + * @param string $text + * @return string + */ + protected function stripAbbreviations($text) { + $less_than_tab = $this->tab_width - 1; + + // Link defs are in the form: [id]*: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,' . $less_than_tab . '}\*\[(.+?)\][ ]?: # abbr_id = $1 + (.*) # text = $2 (no blank lines allowed) + }xm', + array($this, '_stripAbbreviations_callback'), + $text); + return $text; + } + + /** + * Callback for stripping abbreviations + * @param array $matches + * @return string + */ + protected function _stripAbbreviations_callback($matches) { + $abbr_word = $matches[1]; + $abbr_desc = $matches[2]; + if ($this->abbr_word_re) { + $this->abbr_word_re .= '|'; + } + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + return ''; // String that will replace the block + } + + /** + * Find defined abbreviations in text and wrap them in <abbr> elements. + * @param string $text + * @return string + */ + protected function doAbbreviations($text) { + if ($this->abbr_word_re) { + // cannot use the /x modifier because abbr_word_re may + // contain significant spaces: + $text = preg_replace_callback('{' . + '(?<![\w\x1A])' . + '(?:' . $this->abbr_word_re . ')' . + '(?![\w\x1A])' . + '}', + array($this, '_doAbbreviations_callback'), $text); + } + return $text; + } + + /** + * Callback for processing abbreviations + * @param array $matches + * @return string + */ + protected function _doAbbreviations_callback($matches) { + $abbr = $matches[0]; + if (isset($this->abbr_desciptions[$abbr])) { + $desc = $this->abbr_desciptions[$abbr]; + if (empty($desc)) { + return $this->hashPart("<abbr>$abbr</abbr>"); + } else { + $desc = $this->encodeAttribute($desc); + return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>"); + } + } else { + return $matches[0]; + } + } +} + +// Yellow Markdown extra extension +// Copyright (c) 2013-2019 Datenstrom + +class YellowMarkdownExtraParser extends MarkdownExtraParser { + public $yellow; //access to API + public $page; //access to page + public $idAttributes; //id attributes + + public function __construct($yellow, $page) { + $this->yellow = $yellow; + $this->page = $page; + $this->idAttributes = array(); + $this->no_markup = $page->safeMode; + $this->url_filter_func = function($url) use ($yellow, $page) { + return $yellow->lookup->normaliseLocation($url, $page->location, + $page->safeMode && $page->statusCode==200); + }; + parent::__construct(); + } + + // Handle links + 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("/\[(\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); + $text = preg_replace_callback("/((http|https|ftp):\/\/\S+[^\'\"\,\.\;\:\s]+)/", array(&$this, "_doAutoLinks_url_callback"), $text); + $text = preg_replace_callback("/([\w\+\-\.]+@[\w\-\.]+\.[\w]{2,4})/", array(&$this, "_doAutoLinks_email_callback"), $text); + return $text; + } + + // Handle shortcuts, block style + public function _doAutoLinks_shortcutBlock_callback($matches) { + $output = $this->page->parseContentShortcut($matches[1], trim($matches[2]), "block"); + return is_null($output) ? $matches[0] : $this->hashBlock($output); + } + + // Handle shortcuts, inline style + public function _doAutoLinks_shortcutInline_callback($matches) { + $output = $this->page->parseContentShortcut($matches[1], trim($matches[2]), "inline"); + return is_null($output) ? $matches[0] : $this->hashPart($output); + } + + // Handle shortcuts, comment style + public function _doAutoLinks_shortcutComment_callback($matches) { + $output = "<!--".htmlspecialchars($matches[1], ENT_NOQUOTES)."-->"; + return $this->hashBlock($output); + } + + // Handle shortcuts, symbol style + public function _doAutoLinks_shortcutSymbol_callback($matches) { + $output = $this->page->parseContentShortcut("", $matches[1], "symbol"); + return is_null($output) ? $matches[0] : $this->hashPart($output); + } + + // Handle fenced code blocks + public function _doFencedCodeBlocks_callback($matches) { + $text = $matches[4]; + $name = empty($matches[2]) ? "" : "$matches[2] $matches[3]"; + $output = $this->page->parseContentShortcut($name, $text, "code"); + if (is_null($output)) { + $attr = $this->doExtraAttributes("pre", ".$matches[2] $matches[3]"); + $output = "<pre$attr><code>".htmlspecialchars($text, ENT_NOQUOTES)."</code></pre>"; + } + return "\n\n".$this->hashBlock($output)."\n\n"; + } + + // Handle headers, text style + public function _doHeaders_callback_setext($matches) { + if ($matches[3]=="-" && preg_match('{^- }', $matches[1])) return $matches[0]; + $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); + $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; + return "\n".$this->hashBlock($output)."\n\n"; + } + + // Handle headers, atx style + public function _doHeaders_callback_atx($matches) { + $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); + $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; + return "\n".$this->hashBlock($output)."\n\n"; + } + + // Handle inline links + public function _doAnchors_inline_callback($matches) { + $url = $matches[3]=="" ? $matches[4] : $matches[3]; + $text = $matches[2]; + $title = $matches[7]; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + $output = "<a href=\"".$this->encodeURLAttribute($url)."\""; + if (!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; + $output .= $attr; + $output .= ">".$this->runSpanGamut($text)."</a>"; + return $this->hashPart($output); + } + + // Handle inline images + public function _doImages_inline_callback($matches) { + $width = $height = 0; + $src = $matches[3]=="" ? $matches[4] : $matches[3]; + if (!preg_match("/^\w+:/", $src)) { + list($width, $height) = $this->yellow->toolbox->detectImageInformation($this->yellow->system->get("imageDir").$src); + $src = $this->yellow->system->get("serverBase").$this->yellow->system->get("imageLocation").$src; + } + $alt = $matches[2]; + $title = $matches[7]=="" ? $matches[2] : $matches[7]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + $output = "<img src=\"".$this->encodeURLAttribute($src)."\""; + if ($width && $height) $output .= " width=\"$width\" height=\"$height\""; + if (!empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\""; + if (!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; + $output .= $attr; + $output .= $this->empty_element_suffix; + return $this->hashPart($output); + } + + // Return unique id attribute + public function getIdAttribute($text) { + $text = $this->yellow->lookup->normaliseName($text, true, false, true); + $text = trim(preg_replace("/-+/", "-", $text), "-"); + if (is_null($this->idAttributes[$text])) { + $this->idAttributes[$text] = $text; + $attr = " id=\"$text\""; + } + return $attr; + } +} diff --git a/system/extensions/update.php b/system/extensions/update.php @@ -0,0 +1,750 @@ +<?php +// Update extension, https://github.com/datenstrom/yellow-extensions/tree/master/features/update +// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se +// This file may be used and distributed under the terms of the public license. + +class YellowUpdate { + const VERSION = "0.8.2"; + const TYPE = "feature"; + const PRIORITY = "2"; + public $yellow; //access to API + public $updates; //number of updates + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("updateExtensionUrl", "https://github.com/datenstrom/yellow-extensions"); + $this->yellow->system->setDefault("updateInformationFile", "update.ini"); + $this->yellow->system->setDefault("updateVersionFile", "version.ini"); + $this->yellow->system->setDefault("updateWaffleFile", "waffle.ini"); + } + + // Handle startup + public function onStartup($update) { + if ($update) { //TODO: remove later, converts old API in layouts + $fileNameError = $this->yellow->system->get("settingDir")."system-error.log"; + if ($this->yellow->system->isExisting("templateDir")) { + $path = $this->yellow->system->get("layoutDir"); + foreach ($this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/^.*\.html$/", true, false) as $entry) { + $fileData = $fileDataNew = $this->yellow->toolbox->readFile($entry); + $fileDataNew = str_replace("\$yellow->", "\$this->yellow->", $fileDataNew); + $fileDataNew = str_replace("yellow->config->", "yellow->system->", $fileDataNew); + $fileDataNew = str_replace("yellow->pages->", "yellow->content->", $fileDataNew); + $fileDataNew = str_replace("yellow->files->", "yellow->media->", $fileDataNew); + $fileDataNew = str_replace("yellow->plugins->", "yellow->extensions->", $fileDataNew); + $fileDataNew = str_replace("yellow->themes->", "yellow->extensions->", $fileDataNew); + $fileDataNew = str_replace("yellow->snippet(", "yellow->layout(", $fileDataNew); + $fileDataNew = str_replace("yellow->getSnippetArgs(", "yellow->getLayoutArgs(", $fileDataNew); + $fileDataNew = str_replace("\"template\"", "\"layout\"", $fileDataNew); + $fileDataNew = str_replace("\"page template-\"", "\"page layout-\"", $fileDataNew); + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($entry, $fileDataNew)) { + $fileDataError .= "ERROR writing file '$entry'!\n"; + } + } + if (!empty($fileDataError)) $this->yellow->toolbox->createFile($fileNameError, $fileDataError); + } + } + if ($update) { //TODO: remove later, converts old website icon + $fileNameError = $this->yellow->system->get("settingDir")."system-error.log"; + if ($this->yellow->system->isExisting("siteicon")) { + $theme = $this->yellow->system->get("theme"); + $fileName = $this->yellow->system->get("resourceDir")."icon.png"; + $fileNameNew = $this->yellow->system->get("resourceDir").$this->yellow->lookup->normaliseName($theme)."-icon.png"; + if (is_file($fileName) && !$this->yellow->toolbox->renameFile($fileName, $fileNameNew)) { + $fileDataError .= "ERROR renaming file '$fileName'!\n"; + } + if (!empty($fileDataError)) $this->yellow->toolbox->createFile($fileNameError, $fileDataError); + } + } + if ($update) { //TODO: remove later, converts old language files + $fileNameError = $this->yellow->system->get("settingDir")."system-error.log"; + if ($this->yellow->system->isExisting("languageFile")) { + $path = $this->yellow->system->get("extensionDir"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.txt$/", true, false) as $entry) { + preg_match("/^language-(.*)\.txt$/", basename($entry), $matches); + $languageName = $this->getLanguageName($matches[1]); + if (!empty($languageName)) { + $entryNew = $path.$languageName."-language.txt"; + if (!$this->yellow->toolbox->renameFile($entry, $entryNew)) { + $fileDataError .= "ERROR renaming file '$entry'!\n"; + } + $fileNameNew = $path.$languageName.".php"; + $fileDataNew = "<?php\n\nclass Yellow".ucfirst($languageName)." {\nconst VERSION = \"0.8.2\";\nconst TYPE = \"language\";\n}\n"; + if (!$this->yellow->toolbox->createFile($fileNameNew, $fileDataNew)) { + $fileDataError .= "ERROR writing file '$fileNameNew'!\n"; + } + } + } + $fileName = $this->yellow->system->get("extensionDir")."language.php"; + if (is_file($fileName) && !$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("trashDir"))) { + $fileDataError .= "ERROR deleting file '$fileName'!\n"; + } + if (!empty($fileDataError)) $this->yellow->toolbox->createFile($fileNameError, $fileDataError); + } + } + if ($update) { //TODO: remove later, converts old Markdown files + $fileNameError = $this->yellow->system->get("settingDir")."system-error.log"; + if ($this->yellow->system->get("contentDefaultFile")=="page.txt") { + $settings = array("contentDefaultFile" => "page.md", "contentExtension" => ".md", "editNewFile" => "page-new-(.*).md"); + $fileName = $this->yellow->system->get("settingDir").$this->yellow->system->get("systemFile"); + if (!$this->yellow->system->save($fileName, $settings)) { + $fileDataError .= "ERROR writing file '$fileName'!\n"; + } + $path = $this->yellow->system->get("contentDir"); + foreach ($this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/^.*\.txt$/", true, false) as $entry) { + if (!$this->yellow->toolbox->renameFile($entry, str_replace(".txt", ".md", $entry))) { + $fileDataError .= "ERROR renaming file '$entry'!\n"; + } + } + $path = $this->yellow->system->get("settingDir"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.txt$/", true, false) as $entry) { + if (!$this->yellow->toolbox->renameFile($entry, str_replace(".txt", ".md", $entry))) { + $fileDataError .= "ERROR renaming file '$entry!'\n"; + } + } + if (!empty($fileDataError)) $this->yellow->toolbox->createFile($fileNameError, $fileDataError); + $_GET["clean-url"] = "system-updated"; + } + } + if ($update) { //TODO: remove later, converts old template setting + $fileNameError = $this->yellow->system->get("settingDir")."system-error.log"; + if ($this->yellow->system->isExisting("template")) { + $path = $this->yellow->system->get("contentDir"); + foreach ($this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/^.*\.md$/", true, false) as $entry) { + $fileData = $fileDataNew = $this->yellow->toolbox->readFile($entry); + $fileDataNew = preg_replace("/Template:/i", "Layout:", $fileDataNew); + $fileDataNew = preg_replace("/TemplateNew:/i", "LayoutNew:", $fileDataNew); + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($entry, $fileDataNew)) { + $fileDataError .= "ERROR writing file '$entry'!\n"; + } + } + if (!empty($fileDataError)) $this->yellow->toolbox->createFile($fileNameError, $fileDataError); + $_GET["clean-url"] = "system-updated"; + } + } + if ($update) { //TODO: remove later, updates shared pages + $fileNameError = $this->yellow->system->get("settingDir")."system-error.log"; + $pathSetting = $this->yellow->system->get("settingDir"); + $pathShared = $this->yellow->system->get("contentDir").$this->yellow->system->get("contentSharedDir"); + if (count($this->yellow->toolbox->getDirectoryEntries($pathSetting, "/.*/", false, false))>3) { + $regex = "/^page-error-(.*)\.md$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($pathSetting, $regex, true, false) as $entry) { + if (!$this->yellow->toolbox->deleteFile($entry, $this->yellow->system->get("trashDir"))) { + $fileDataError .= "ERROR deleting file '$entry'!\n"; + } + } + $regex = "/^page-new-(.*)\.md$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($pathSetting, $regex, true, false) as $entry) { + if (!$this->yellow->toolbox->renameFile($entry, str_replace($pathSetting, $pathShared, $entry), true)) { + $fileDataError .= "ERROR moving file '$entry'!\n"; + } + } + $fileNameHeader = $pathShared."header.md"; + if (!is_file($fileNameHeader) && $this->yellow->system->isExisting("tagline")) { + $fileDataHeader = "---\nTitle: Header\nStatus: hidden\n---\n".$this->yellow->system->get("tagline"); + if (!$this->yellow->toolbox->createFile($fileNameHeader, $fileDataHeader, true)) { + $fileDataError .= "ERROR writing file '$fileNameHeader'!\n"; + } + } + $fileNameFooter = $pathShared."footer.md"; + if (!is_file($fileNameFooter)) { + $fileDataFooter = "---\nTitle: Footer\nStatus: hidden\n---\n"; + $fileDataFooter .= $this->yellow->text->getText("InstallFooterText", $this->yellow->system->get("language")); + if (!$this->yellow->toolbox->createFile($fileNameFooter, $fileDataFooter, true)) { + $fileDataError .= "ERROR writing file '$fileNameFooter'!\n"; + } + } + $this->updateContentMultiLanguage("shared-pages"); + if (!empty($fileDataError)) $this->yellow->toolbox->createFile($fileNameError, $fileDataError); + } + } + if ($update) { + $fileName = $this->yellow->system->get("settingDir").$this->yellow->system->get("systemFile"); + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileDataNew = ""; + $settingsDefaults = new YellowDataCollection(); + $settingsDefaults->exchangeArray($this->yellow->system->settingsDefaults->getArrayCopy()); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (!empty($matches[1]) && !is_null($settingsDefaults[$matches[1]])) unset($settingsDefaults[$matches[1]]); + if (!empty($matches[1]) && $matches[1][0]!="#" && is_null($this->yellow->system->settingsDefaults[$matches[1]])) { + $fileDataNew .= "# $line"; + } else { + $fileDataNew .= $line; + } + } + unset($settingsDefaults["systemFile"]); + foreach ($settingsDefaults as $key=>$value) { + $fileDataNew .= ucfirst($key).": $value\n"; + } + if ($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileName, $fileDataNew); + } + } + + // 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; + } + + // Handle command + public function onCommand($args) { + $statusCode = 0; + if ($this->isExtensionPending()) $statusCode = $this->processCommandPending(); + if ($statusCode==0) { + list($command) = $args; + switch ($command) { + case "clean": $statusCode = $this->processCommandClean($args); break; + case "install": $statusCode = $this->processCommandInstall($args); break; + case "uninstall": $statusCode = $this->processCommandUninstall($args); break; + case "update": $statusCode = $this->processCommandUpdate($args); break; + default: $statusCode = 0; break; + } + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + $help .= "install [extension]\n"; + $help .= "uninstall [extension]\n"; + $help .= "update [extension]\n"; + return $help; + } + + // Process command to clean downloads + public function processCommandClean($args) { + $statusCode = 0; + list($command, $path) = $args; + if ($path=="all") { + $path = $this->yellow->system->get("extensionDir"); + $regex = "/^.*\\".$this->yellow->system->get("downloadExtension")."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, false) as $entry) { + if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + } + 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($args) { + list($command, $extensions) = $this->getExtensionInformation($args); + if (!empty($extensions)) { + $this->updates = 0; + list($statusCode, $data) = $this->getInstallInformation($extensions); + if ($statusCode==200) $statusCode = $this->downloadExtensions($data); + if ($statusCode==200) $statusCode = $this->updateExtensions(); + if ($statusCode>=400) echo "ERROR installing files: ".$this->yellow->page->get("pageError")."\n"; + echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; + echo ", $this->updates extension".($this->updates!=1 ? "s" : "")." installed\n"; + } else { + $statusCode = $this->showExtensions(); + } + return $statusCode; + } + + // Process command to uninstall extensions + public function processCommandUninstall($args) { + list($command, $extensions) = $this->getExtensionInformation($args); + 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"; + echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; + echo ", $this->updates extension".($this->updates!=1 ? "s" : "")." uninstalled\n"; + } else { + $statusCode = $this->showExtensions(); + } + return $statusCode; + } + + // Process command to update website + public function processCommandUpdate($args) { + list($command, $extensions, $force) = $this->getExtensionInformation($args); + 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($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"; + } else { + echo "Your website is up to date\n"; + } + return $statusCode; + } + + // Process command to update website with pending extension + public function processCommandPending() { + $statusCode = $this->updateExtensions(); + 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 update website with pending extension + public function processRequestPending($scheme, $address, $base, $location, $fileName) { + $statusCode = $this->updateExtensions(); + if ($statusCode==200) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } + return $statusCode; + } + + // Return extension information + public function getExtensionInformation($args) { + $command = array_shift($args); + $extensions = array_unique(array_filter($args, "strlen")); + foreach ($extensions as $key=>$value) { + if ($value=="force") { + $force = true; + unset($extensions[$key]); + } + } + return array($command, $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) && !is_null($dataLatest[$key]) && !is_null($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) { + list($version) = explode(",", $dataLatest[$key]); + if (strnatcasecmp($dataCurrent[$key], $version)<0) $data[$key] = $dataLatest[$key]; + if (!is_null($dataModified[$key]) && !empty($version) && $force) $data[$key] = $dataLatest[$key]; + } + } else { + foreach ($extensions as $extension) { + $found = false; + foreach ($dataCurrent as $key=>$value) { + list($version) = explode(",", $dataLatest[$key]); + 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) = explode(",", $value); + if (is_null($dataModified[$key]) || $force) { + echo ucfirst($key)." $version\n"; + } else { + echo ucfirst($key)." $version has been modified - Force update\n"; + } + } + } + return array($statusCode, $data); + } + + // Show extensions + public function showExtensions() { + list($statusCode, $dataLatest) = $this->getExtensionsVersion(true, true); + foreach ($dataLatest as $key=>$value) { + list($version, $url, $description) = explode(",", $value, 3); + echo ucfirst($key).": $description\n"; + } + if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->get("pageError")."\n"; + return $statusCode; + } + + // Download extensions + public function downloadExtensions($data) { + $statusCode = 200; + $path = $this->yellow->system->get("extensionDir"); + $fileExtension = $this->yellow->system->get("downloadExtension"); + foreach ($data as $key=>$value) { + $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; + list($version, $url) = explode(",", $value); + list($statusCode, $fileData) = $this->getExtensionFile($url); + if (empty($fileData) || !$this->yellow->toolbox->createFile($fileName.$fileExtension, $fileData)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + break; + } + } + if ($statusCode==200) { + foreach ($data as $key=>$value) { + $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; + if (!$this->yellow->toolbox->renameFile($fileName.$fileExtension, $fileName)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + } + return $statusCode; + } + + // Update extensions + public function updateExtensions($force = false) { + $statusCode = 200; + if (function_exists("opcache_reset")) opcache_reset(); + $path = $this->yellow->system->get("extensionDir"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { + $statusCode = max($statusCode, $this->updateExtensionArchive($entry, $force)); + if (!$this->yellow->toolbox->deleteFile($entry)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$entry'!"); + } + } + return $statusCode; + } + + // Update extension from archive + public function updateExtensionArchive($path, $force = false) { + $statusCode = 200; + $zip = new ZipArchive(); + if ($zip->open($path)===true) { + if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateExtensionArchive file:$path<br/>\n"; + if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1]; + $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateInformationFile")); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (!empty($matches[1]) && !empty($matches[2])) { + list($fileName) = explode(",", $matches[2], 2); + if (is_file($fileName)) { + $lastPublished = filemtime($fileName); + break; + } + } + } + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (lcfirst($matches[1])=="extension") $extension = lcfirst($matches[2]); + if (lcfirst($matches[1])=="plugin") $extension = lcfirst(substru($matches[2], 6)); //TODO: remove later, for backwards compatibility + if (lcfirst($matches[1])=="theme") $extension = lcfirst(substru($matches[2], 11)); //TODO: remove later, for backwards compatibility + if (lcfirst($matches[1])=="published") $modified = strtotime($matches[2]); + if (!empty($matches[1]) && !empty($matches[2]) && strposu($matches[1], "/")) { + list($dummy, $entry) = explode("/", $matches[1], 2); + list($fileName, $flags) = explode(",", $matches[2], 2); + $fileData = $zip->getFromName($pathBase.basename($entry)); + $lastModified = $this->yellow->toolbox->getFileModified($fileName); + $statusCode = $this->updateExtensionFile($fileName, $fileData, $modified, $lastModified, $lastPublished, $flags, $force, $extension); + if ($statusCode!=200) break; + } + } + $zip->close(); + if ($statusCode==200) $statusCode = $this->updateContentMultiLanguage($extension); + if ($statusCode==200) $statusCode = $this->updateStartupNotification($extension); + ++$this->updates; + } else { + $statusCode = 500; + $this->yellow->page->error(500, "Can't open file '$path'!"); + } + return $statusCode; + } + + // Update extension from file + public function updateExtensionFile($fileName, $fileData, $modified, $lastModified, $lastPublished, $flags, $force, $extension) { + $statusCode = 200; + $fileName = $this->yellow->toolbox->normaliseTokens($fileName); + if ($this->yellow->lookup->isValidFile($fileName) && !empty($extension)) { + $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("/delete/i", $flags) && is_file($fileName)) $delete = true; + if (preg_match("/careful/i", $flags) && is_file($fileName) && $lastModified!=$lastPublished && !$force) $update = false; + if (preg_match("/optional/i", $flags) && $this->yellow->extensions->isExisting($extension)) $create = $update = $delete = false; + if ($create) { + if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || + !$this->yellow->toolbox->modifyFile($fileName, $modified)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + if ($update) { + if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("trashDir")) || + !$this->yellow->toolbox->createFile($fileName, $fileData) || + !$this->yellow->toolbox->modifyFile($fileName, $modified)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + if ($delete) { + if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("trashDir"))) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); + } + } + if (defined("DEBUG") && DEBUG>=2) { + $debug = "action:".($create ? "create" : "").($update ? "update" : "").($delete ? "delete" : ""); + if (!$create && !$update && !$delete) $debug = "action:none"; + echo "YellowUpdate::updateExtensionFile file:$fileName $debug<br/>\n"; + } + } + return $statusCode; + } + + // Update content for multi language mode + public function updateContentMultiLanguage($extension) { + $statusCode = 200; + if ($this->yellow->system->get("multiLanguageMode") && !$this->yellow->extensions->isExisting($extension)) { + $pathsSource = $pathsTarget = array(); + $pathBase = $this->yellow->system->get("contentDir"); + $fileExtension = $this->yellow->system->get("contentExtension"); + $fileRegex = "/^.*\\".$fileExtension."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true) as $entry) { + if (count($this->yellow->toolbox->getDirectoryEntries($entry, $fileRegex, false, false))) { + array_push($pathsSource, $entry."/"); + } elseif (count($this->yellow->toolbox->getDirectoryEntries($entry, "/.*/", false, true))) { + array_push($pathsTarget, $entry."/"); + } + } + if (count($pathsSource) && count($pathsTarget)) { + foreach ($pathsSource as $pathSource) { + foreach ($pathsTarget as $pathTarget) { + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($pathSource, "/.*/", false, false); + foreach ($fileNames as $fileName) { + $modified = $this->yellow->toolbox->getFileModified($fileName); + $fileNameTarget = $pathTarget.substru($fileName, strlenu($pathBase)); + if (!is_file($fileNameTarget)) { + if (!$this->yellow->toolbox->copyFile($fileName, $fileNameTarget, true) || + !$this->yellow->toolbox->modifyFile($fileNameTarget, $modified)) { + $statusCode = 500; + $this->yellow->page->error(500, "Can't write file '$fileNameTarget'!"); + } + } + if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateContentMultiLanguage file:$fileNameTarget<br/>\n"; + } + } + if (!$this->yellow->toolbox->deleteDirectory($pathSource)) { + $statusCode = 500; + $this->yellow->page->error(500, "Can't delete path '$pathSource'!"); + } + } + } + } + return $statusCode; + } + + // Update notification for next startup + public function updateStartupNotification($extension) { + $statusCode = 200; + $startupUpdate = $this->yellow->system->get("startupUpdate"); + if ($startupUpdate=="none") $startupUpdate = "update"; + if ($extension!="update") $startupUpdate .= ",$extension"; + $fileName = $this->yellow->system->get("settingDir").$this->yellow->system->get("systemFile"); + if (!$this->yellow->system->save($fileName, array("startupUpdate" => $startupUpdate))) { + $statusCode = 500; + $this->yellow->page->error(500, "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Remove extensions + public function removeExtensions($data) { + $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)); + } + ++$this->updates; + } + if ($statusCode==200) $statusCode = $this->updateStartupNotification("update"); + return $statusCode; + } + + // Remove extensions file + public function removeExtensionsFile($fileName, $extension) { + $statusCode = 200; + $fileName = $this->yellow->toolbox->normaliseTokens($fileName); + if ($this->yellow->lookup->isValidFile($fileName) && !empty($extension)) { + if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("trashDir"))) { + $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"; + } + } + return $statusCode; + } + + // 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) = explode(",", $matches[2]); + $data[$extension] = $rawFormat ? $matches[2] : $version; + } + } + } + } else { + $statusCode = 200; + $data = $this->yellow->extensions->getData(); + } + return array($statusCode, $data); + } + + // 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])) { + list($extension) = explode("/", lcfirst($matches[1])); + list($fileName) = explode(",", $matches[2], 2); + if (!is_null($data[$extension])) $data[$extension] .= ","; + $data[$extension] .= $fileName; + } + } + } + return array($statusCode, $data); + } + + // 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) { + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); + if (!empty($matches[1]) && !empty($matches[2])) { + list($extensionNew) = explode("/", lcfirst($matches[1])); + list($fileName, $flags) = explode(",", $matches[2], 2); + if ($extension!=$extensionNew) { + $extension = $extensionNew; + $lastPublished = $this->yellow->toolbox->getFileModified($fileName); + } + if (!is_null($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"; + } + } + } + } + } + } + return array($statusCode, $data); + } + + // 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_RETURNTRANSFER, 1); + curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); + $rawData = curl_exec($curlHandle); + $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); + 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 { + $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"; + return array($statusCode, $fileData); + } + + // Return human readable language name, TODO: remove later, for backwards compatibility + public function getLanguageName($language) { + $languageName = ""; + $languageNames = array("bn" => "bengali", "cs" => "czech", "da" => "danish", "de" => "german", + "en" => "english", "es" => "spanish", "fr" => "french", "hu" => "hungarian", "id" => "indonesian", + "it" => "italian", "ja" => "japanese", "ko" => "korean", "nl" => "dutch", "pl" => "polish", + "pt" => "portuguese", "ru" => "russian", "sk" => "slovenian", "sv" => "swedish", "zh-CN" => "chinese"); + if (array_key_exists($language, $languageNames)) { + $languageName = $languageNames[$language]; + } + return $languageName; + } + + // Check if extension pending + public function isExtensionPending() { + $path = $this->yellow->system->get("extensionDir"); + return count($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))>0; + } +} diff --git a/system/layouts/default.html b/system/layouts/default.html @@ -0,0 +1,9 @@ +<?php $this->yellow->layout("header") ?> +<div class="content"> +<?php $this->yellow->layout("sidebar") ?> +<div class="main" role="main"> +<h1><?php echo $this->yellow->page->getHtml("titleContent") ?></h1> +<?php echo $this->yellow->page->getContent() ?> +</div> +</div> +<?php $this->yellow->layout("footer") ?> diff --git a/system/layouts/error.html b/system/layouts/error.html @@ -0,0 +1,8 @@ +<?php $this->yellow->layout("header") ?> +<div class="content"> +<div class="main" role="main"> +<h1><?php echo $this->yellow->page->getHtml("titleContent") ?></h1> +<?php echo $this->yellow->page->getContent() ?> +</div> +</div> +<?php $this->yellow->layout("footer") ?> diff --git a/system/layouts/footer.html b/system/layouts/footer.html @@ -0,0 +1,10 @@ +<div class="footer" role="contentinfo"> +<div class="siteinfo"> +<?php if ($this->yellow->page->isPage("footer")) echo $this->yellow->page->getPage("footer")->getContent() ?> +<div class="siteinfo-banner"></div> +</div> +</div> +</div> +<?php echo $this->yellow->page->getExtra("footer") ?> +</body> +</html> diff --git a/system/layouts/header.html b/system/layouts/header.html @@ -0,0 +1,27 @@ +<!DOCTYPE html><html lang="<?php echo $this->yellow->page->getHtml("language") ?>"> +<head> +<title><?php echo $this->yellow->page->getHtml("titleHeader") ?></title> +<meta charset="utf-8" /> +<meta name="description" content="<?php echo $this->yellow->page->getHtml("description") ?>" /> +<meta name="keywords" content="<?php echo $this->yellow->page->getHtml("keywords") ?>" /> +<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 echo $this->yellow->page->getExtra("header") ?> +</head> +<body> +<?php if ($page = $this->yellow->content->shared($this->yellow->page->location, false, $this->yellow->page->get("header"))) $this->yellow->page->setPage("header", $page) ?> +<?php if ($page = $this->yellow->content->shared($this->yellow->page->location, false, $this->yellow->page->get("footer"))) $this->yellow->page->setPage("footer", $page) ?> +<?php if ($page = $this->yellow->content->shared($this->yellow->page->location, false, $this->yellow->page->get("sidebar"))) $this->yellow->page->setPage("sidebar", $page) ?> +<?php if ($this->yellow->page->get("navigation")=="navigation-sidebar") $this->yellow->page->setPage("navigation-sidebar", $this->yellow->page->getParentTop(true)) ?> +<?php $this->yellow->page->set("pageClass", "page layout-".$this->yellow->page->get("layout")) ?> +<?php if (!$this->yellow->page->isError() && ($this->yellow->page->isPage("sidebar") || $this->yellow->page->isPage("navigation-sidebar"))) $this->yellow->page->set("pageClass", $this->yellow->page->get("pageClass")." with-sidebar") ?> +<div class="<?php echo $this->yellow->page->getHtml("pageClass") ?>"> +<div class="header" role="banner"> +<div class="sitename"> +<h1><a href="<?php echo $this->yellow->page->getBase(true)."/" ?>"><i class="sitename-logo"></i><?php echo $this->yellow->page->getHtml("sitename") ?></a></h1> +<?php if ($this->yellow->page->isPage("header")) echo $this->yellow->page->getPage("header")->getContent() ?> +</div> +<div class="sitename-banner"></div> +<?php $this->yellow->layout($this->yellow->page->get("navigation")) ?> +</div> diff --git a/system/layouts/navigation-sidebar.html b/system/layouts/navigation-sidebar.html @@ -0,0 +1,10 @@ +<?php $pages = $this->yellow->content->top() ?> +<?php $this->yellow->page->setLastModified($pages->getModified()) ?> +<div class="navigation" role="navigation"> +<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> +<?php endforeach ?> +</ul> +</div> +<div class="navigation-banner"></div> diff --git a/system/layouts/navigation-tree.html b/system/layouts/navigation-tree.html @@ -0,0 +1,16 @@ +<?php list($name, $pages, $level) = $this->yellow->getLayoutArgs() ?> +<?php if (!$pages) $pages = $this->yellow->content->top() ?> +<?php $this->yellow->page->setLastModified($pages->getModified()) ?> +<?php if (!$level): ?> +<div class="navigation-tree" role="navigation"> +<?php endif ?> +<ul> +<?php foreach ($pages as $page): ?> +<?php $children = $page->getChildren() ?> +<li><a<?php echo $page->isActive() ? " class=\"active\" aria-current=\"page\"" : "" ?> href="<?php echo $page->getLocation(true) ?>"><?php echo $page->getHtml("titleNavigation") ?></a><?php if ($children->count()) { echo "\n"; $this->yellow->layout($name, $children, $level+1); } ?></li> +<?php endforeach ?> +</ul> +<?php if (!$level): ?> +</div> +<div class="navigation-banner"></div> +<?php endif ?> diff --git a/system/layouts/navigation.html b/system/layouts/navigation.html @@ -0,0 +1,10 @@ +<?php $pages = $this->yellow->content->top() ?> +<?php $this->yellow->page->setLastModified($pages->getModified()) ?> +<div class="navigation" role="navigation"> +<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> +<?php endforeach ?> +</ul> +</div> +<div class="navigation-banner"></div> diff --git a/system/layouts/pagination.html b/system/layouts/pagination.html @@ -0,0 +1,11 @@ +<?php list($name, $pages) = $this->yellow->getLayoutArgs() ?> +<?php if ($pages->isPagination()): ?> +<div class="pagination" role="navigation"> +<?php if ($pages->getPaginationPrevious()): ?> +<a class="previous" href="<?php echo $pages->getPaginationPrevious() ?>"><?php echo $this->yellow->text->getHtml("paginationPrevious") ?></a> +<?php endif ?> +<?php if ($pages->getPaginationNext()): ?> +<a class="next" href="<?php echo $pages->getPaginationNext() ?>"><?php echo $this->yellow->text->getHtml("paginationNext") ?></a> +<?php endif ?> +</div> +<?php endif ?> diff --git a/system/layouts/sidebar.html b/system/layouts/sidebar.html @@ -0,0 +1,21 @@ +<?php if ($this->yellow->page->isPage("sidebar")): ?> +<div class="sidebar" role="complementary"> +<?php $page = $this->yellow->page->getPage("sidebar") ?> +<?php $page->setPage("main", $this->yellow->page) ?> +<?php echo $page->getContent() ?> +</div> +<?php elseif ($this->yellow->page->isPage("navigation-sidebar")): ?> +<div class="sidebar" role="complementary"> +<div class="navigation-sidebar"> +<?php $page = $this->yellow->page->getPage("navigation-sidebar") ?> +<?php $pages = $page->getChildren(!$page->isVisible()) ?> +<?php $this->yellow->page->setLastModified($pages->getModified()) ?> +<p><?php echo $page->getHtml("titleNavigation") ?></p> +<ul> +<?php foreach ($pages as $page): ?> +<li><a<?php echo $page->isActive() ? " class=\"active\"" : "" ?> href="<?php echo $page->getLocation(true) ?>"><?php echo $page->getHtml("titleNavigation") ?></a></li> +<?php endforeach ?> +</ul> +</div> +</div> +<?php endif ?> diff --git a/system/plugins/bundle.php b/system/plugins/bundle.php @@ -1,1938 +0,0 @@ -<?php -// Bundle plugin, https://github.com/datenstrom/yellow-plugins/tree/master/bundle -// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowBundle { - const VERSION = "0.8.1"; - public $yellow; //access to API - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - $this->yellow->config->setDefault("bundleAndMinify", "1"); - } - - // 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($args) { - list($command) = $args; - switch ($command) { - case "clean": $statusCode = $this->processCommandClean($args); break; - default: $statusCode = 0; - } - return $statusCode; - } - - // Process command to clean bundles - public function processCommandClean($args) { - $statusCode = 0; - list($command, $path) = $args; - if ($path=="all") { - $path = $this->yellow->config->get("assetDir"); - 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 = $dataScript = $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 (is_null($dataCss[$matches[2]])) $dataCss[$matches[2]] = $line; - } else { - if (is_null($dataLink[$matches[2]])) $dataLink[$matches[2]] = $line; - } - } elseif (preg_match("/^<script (.*?)src=\"([^\"]+)\"(.*?)><\/script>$/i", $line, $matches)) { - if (preg_match("/\"defer\"/i", $line)) { - if (is_null($dataScript[$matches[2]])) $dataScript[$matches[2]] = $line; - } else { - array_push($dataOther, $line); - } - } else { - array_push($dataOther, $line); - } - } - if ($this->yellow->config->get("bundleAndMinify")) { - $dataCss = $this->processBundle($dataCss, "css"); - $dataScript = $this->processBundle($dataScript, "js"); - } - $output = implode($dataMeta).implode($dataLink).implode($dataCss).implode($dataScript).implode($dataOther); - return $output; - } - - // Process bundle, create file on demand - public function processBundle($data, $type) { - $fileNames = array(); - $scheme = $this->yellow->config->get("serverScheme"); - $address = $this->yellow->config->get("serverAddress"); - $base = $this->yellow->config->get("serverBase"); - foreach ($data as $key=>$value) { - if (preg_match("/^\w+:/", $key)) continue; - if (preg_match("/data-bundle=\"none\"/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)) { - $this->yellow->toolbox->timerStart($time); - $id = substru(md5(implode($fileNames).$base), 0, 10); - $fileNameBundle = $this->yellow->config->get("assetDir")."bundle-$id.min.$type";; - $locationBundle = $base.$this->yellow->config->get("assetLocation")."bundle-$id.min.$type"; - if ($type=="css") { - $data[$locationBundle] = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"".htmlspecialchars($locationBundle)."\" />\n"; - } else { - $data[$locationBundle] = "<script type=\"text/javascript\" defer=\"defer\" src=\"".htmlspecialchars($locationBundle)."\"></script>\n"; - } - if ($this->yellow->toolbox->getFileModified($fileNameBundle)!=$modified) { - 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 (!empty($fileDataNew)) $fileDataNew .= "\n\n"; - $fileDataNew .= "/* ".basename($fileName)." */\n"; - $fileDataNew .= $fileData; - } - if (defined("DEBUG") && DEBUG>=2) { - if (!empty($fileDataNew)) $fileDataNew .= "\n\n"; - $fileDataNew .= "/* YellowBundle::processBundle file:$fileNameBundle <- ".$this->yellow->page->fileName." */"; - } - if (is_file($fileNameBundle)) $this->yellow->toolbox->deleteFile($fileNameBundle); - if (!$this->yellow->toolbox->createFile($fileNameBundle, $fileDataNew) || - !$this->yellow->toolbox->modifyFile($fileNameBundle, $modified)) { - $this->yellow->page->error(500, "Can't write file '$fileNameBundle'!"); - } - } - $this->yellow->toolbox->timerStop($time); - if (defined("DEBUG") && DEBUG>=2) { - $data["debug"] = "YellowBundle::processBundle file:$fileNameBundle time:$time ms<br/>\n"; - } - } - return $data; - } - - // Process bundle, convert URLs - public function processBundleConvert($scheme, $address, $base, $fileData, $fileName, $type) { - if ($type=="css") { - $pluginDirLength = strlenu($this->yellow->config->get("pluginDir")); - if (substru($fileName, 0, $pluginDirLength) == $this->yellow->config->get("pluginDir")) { - $base .= $this->yellow->config->get("pluginLocation"); - } else { - $base .= $this->yellow->config->get("assetLocation"); - } - $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 (!isset($positions[$i])) { - 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(); - $css = $this->replace($css); - - $css = $this->stripWhitespace($css); - $css = $this->shortenHex($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 shortenHex($content) - { - $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?=[; }])/i', '#$1$2$3', $content); - - // we can shorten some even more by replacing them with their color name - $colors = array( - '#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', - ); - - 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 - // best to just leave `calc()`s alone, even if they could be optimized - // (which is a whole other undertaking, where units & order of - // operations all need to be considered...) - $calcs = $this->findCalcs($content); - $content = str_replace($calcs, array_keys($calcs), $content); - - // 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); - - // restore `calc()` expressions - $content = str_replace(array_keys($calcs), $calcs, $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() - { - $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); - } - - /** - * Find all `calc()` occurrences. - * - * @param string $content The CSS content to find `calc()`s in. - * - * @return string[] - */ - protected function findCalcs($content) - { - $results = array(); - preg_match_all('/calc(\(.+?)(?=$|;|calc\()/', $content, $matches, PREG_SET_ORDER); - - foreach ($matches as $match) { - $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; - } - } - - $results['calc('.count($results).')'] = 'calc'.$expr; - } - - return $results; - } - - /** - * 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() - { - // single-line comments - $this->registerPattern('/\/\/.*$/m', ''); - - // multi-line comments - $this->registerPattern('/\/\*.*?\*\//s', ''); - } - - /** - * 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-2018 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/plugins/command.php b/system/plugins/command.php @@ -1,621 +0,0 @@ -<?php -// Command plugin, https://github.com/datenstrom/yellow-plugins/tree/master/command -// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowCommand { - const VERSION = "0.8.1"; - public $yellow; //access to API - public $files; //number of files - public $links; //number of links - public $errors; //number of errors - public $locationsArgs; //locations with location arguments detected - public $locationsArgsPagination; //locations with pagination arguments detected - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - } - - // Handle command - public function onCommand($args) { - list($command) = $args; - switch ($command) { - case "": $statusCode = $this->processCommandHelp(); break; - case "build": $statusCode = $this->processCommandBuild($args); break; - case "check": $statusCode = $this->processCommandCheck($args); break; - case "clean": $statusCode = $this->processCommandClean($args); break; - case "serve": $statusCode = $this->processCommandServe($args); break; - case "version": $statusCode = $this->processCommandVersion($args); 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 [url]\n"; - $help .= "version\n"; - return $help; - } - - // Process command to show available commands - public function processCommandHelp() { - echo "Datenstrom Yellow is for people who make 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($args) { - $statusCode = 0; - list($command, $path, $location) = $args; - if (empty($location) || $location[0]=="/") { - if ($this->checkStaticConfig()) { - $statusCode = $this->buildStaticFiles($path, $location); - } else { - $statusCode = 500; - $this->files = 0; - $this->errors = 1; - $fileName = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile"); - echo "ERROR building files: Please configure StaticUrl 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->config->get("staticDir") : $path, "/"); - $this->files = $this->errors = 0; - $this->locationsArgs = $this->locationsArgsPagination = array(); - $statusCode = empty($locationFilter) ? $this->cleanStaticFiles($path, $locationFilter) : 200; - $staticUrl = $this->yellow->config->get("staticUrl"); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - foreach ($this->getContentLocations() as $location) { - if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; - $statusCode = max($statusCode, $this->buildStaticFile($path, $location, true)); - } - foreach ($this->locationsArgs as $location) { - if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; - $statusCode = max($statusCode, $this->buildStaticFile($path, $location, true)); - } - foreach ($this->locationsArgsPagination as $location) { - if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; - if (substru($location, -1)!=$this->yellow->toolbox->getLocationArgsSeparator()) { - $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() as $location) { - $statusCode = max($statusCode, $this->buildStaticFile($path, $location)); - } - $statusCode = max($statusCode, $this->buildStaticFile($path, "/error/", false, false, true)); - } - return $statusCode; - } - - // Build static file - public function buildStaticFile($path, $location, $analyse = false, $probe = false, $error = false) { - $this->yellow->pages = new YellowPages($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->config->get("staticUrl"); - 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 "ERROR 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) = explode(":", $address); - if (is_null($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) { - $pagination = $this->yellow->config->get("contentPagination"); - 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->isLocationArgs($location)) continue; - if (!$this->yellow->toolbox->isLocationArgsPagination($location, $pagination)) { - $location = rtrim($location, "/")."/"; - if (is_null($this->locationsArgs[$location])) { - $this->locationsArgs[$location] = $location; - if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLocations detected location:$location<br/>\n"; - } - } else { - $location = rtrim($location, "0..9"); - if (is_null($this->locationsArgsPagination[$location])) { - $this->locationsArgsPagination[$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($args) { - $statusCode = 0; - list($command, $path, $location) = $args; - if (empty($location) || $location[0]=="/") { - if ($this->checkStaticConfig()) { - $statusCode = $this->checkStaticFiles($path, $location); - } else { - $statusCode = 500; - $this->files = $this->links = 0; - $fileName = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile"); - echo "ERROR checking files: Please configure StaticUrl in file '$fileName'!\n"; - } - echo "Yellow $command: $this->files file".($this->files!=1 ? "s" : ""); - echo ", $this->links link".($this->links!=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->config->get("staticDir") : $path, "/"); - $this->files = $this->links = 0; - $regex = "/^[^.]+$|".$this->yellow->config->get("staticDefaultFile")."$/"; - $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->config->get("staticUrl"); - 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 (!is_null($links[$url])) $links[$url] .= ","; - $links[$url] .= $locationSource; - if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLinks detected url:$url<br/>\n"; - } elseif ($location[0]=="/") { - $url = "$scheme://$address$location"; - if (!is_null($links[$url])) $links[$url] .= ","; - $links[$url] .= $locationSource; - if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseLinks detected url:$url<br/>\n"; - } - } - ++$this->files; - } else { - $statusCode = 500; - echo "ERROR reading files: Can't read file '$fileName'!\n"; - } - } - $this->links = count($links); - } else { - $statusCode = 500; - 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; - $broken = $redirect = $data = array(); - $staticUrl = $this->yellow->config->get("staticUrl"); - $staticUrlLength = strlenu(rtrim($staticUrl, "/")); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - $staticLocations = $this->getContentLocations(true); - uksort($links, "strnatcasecmp"); - foreach ($links as $url=>$value) { - if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::analyseStatus url:$url\n"; - 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)) { - $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; - } - } - } - 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($args) { - $statusCode = 0; - list($command, $path, $location) = $args; - if (empty($location) || $location[0]=="/") { - $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->config->get("staticDir") : $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 plugins - public function broadcastCommand($args) { - $statusCode = 0; - foreach ($this->yellow->plugins->plugins as $key=>$value) { - if ($key=="command") continue; - if (method_exists($value["obj"], "onCommand")) { - $statusCode = $value["obj"]->onCommand(func_get_args()); - if ($statusCode!=0) break; - } - } - return $statusCode; - } - - // Process command to start built-in web server - public function processCommandServe($args) { - list($command, $url) = $args; - 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"; - system("php -S $address yellow.php", $returnStatus); - $statusCode = $returnStatus!=0 ? 500 : 200; - } else { - $statusCode = 400; - echo "Yellow $command: Invalid arguments\n"; - } - return $statusCode; - } - - // Process command to show software version and updates - public function processCommandVersion($args) { - $serverVersion = $this->yellow->toolbox->getServerVersion(); - echo "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion\n"; - list($statusCode, $dataCurrent) = $this->getSoftwareVersion(); - list($statusCode, $dataLatest) = $this->getSoftwareVersion(true); - foreach ($dataCurrent as $key=>$value) { - if (strnatcasecmp($dataCurrent[$key], $dataLatest[$key])>=0) { - echo "$key $value\n"; - } else { - echo "$key $value - Update available\n"; - } - } - if ($statusCode!=200) echo "ERROR checking updates: ".$this->yellow->page->get("pageError")."\n"; - return $statusCode; - } - - // Check static configuration - public function checkStaticConfig() { - $staticUrl = $this->yellow->config->get("staticUrl"); - return !empty($staticUrl); - } - - // Check static directory - public function checkStaticDirectory($path) { - $ok = false; - if (!empty($path)) { - if ($path==rtrim($this->yellow->config->get("staticDir"), "/")) $ok = true; - if ($path==rtrim($this->yellow->config->get("trashDir"), "/")) $ok = true; - if (is_file("$path/".$this->yellow->config->get("staticDefaultFile"))) $ok = true; - if (is_file("$path/yellow.php")) $ok = false; - } - return $ok; - } - - // Return static file - public function getStaticFile($path, $location, $statusCode) { - if ($statusCode<400) { - $fileName = $path.$location; - if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->config->get("staticDefaultFile"); - } elseif ($statusCode==404) { - $fileName = $path."/".$this->yellow->config->get("staticErrorFile"); - } - return $fileName; - } - - // Return static location - public function getStaticLocation($path, $fileName) { - $location = substru($fileName, strlenu($path)); - if (basename($location)==$this->yellow->config->get("staticDefaultFile")) { - $defaultFileLength = strlenu($this->yellow->config->get("staticDefaultFile")); - $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 human readable status - public function getStatusFormatted($statusCode) { - return $this->yellow->toolbox->getHttpStatusFormatted($statusCode, true); - } - - // Return content locations - public function getContentLocations($includeAll = false) { - $locations = array(); - $staticUrl = $this->yellow->config->get("staticUrl"); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - $this->yellow->page->setRequestInformation($scheme, $address, $base, "", ""); - foreach ($this->yellow->pages->index(true, true) as $page) { - if (($page->get("status")!="ignore" && $page->get("status")!="draft") || $includeAll) { - array_push($locations, $page->location); - } - } - if (!$this->yellow->pages->find("/") && $this->yellow->config->get("multiLanguageMode")) array_unshift($locations, "/"); - return $locations; - } - - // Return media locations - public function getMediaLocations() { - $locations = array(); - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->config->get("mediaDir"), "/.*/", 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)$/"; - $pluginDirLength = strlenu($this->yellow->config->get("pluginDir")); - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->config->get("pluginDir"), $regex, false, false); - foreach ($fileNames as $fileName) { - array_push($locations, $this->yellow->config->get("pluginLocation").substru($fileName, $pluginDirLength)); - } - $themeDirLength = strlenu($this->yellow->config->get("themeDir")); - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->config->get("themeDir"), $regex, false, false); - foreach ($fileNames as $fileName) { - array_push($locations, $this->yellow->config->get("themeLocation").substru($fileName, $themeDirLength)); - } - return $locations; - } - - // Return extra locations - public function getExtraLocations() { - $locations = array(); - $pathIgnore = "(".$this->yellow->config->get("staticDir")."|". - $this->yellow->config->get("cacheDir")."|". - $this->yellow->config->get("contentDir")."|". - $this->yellow->config->get("mediaDir")."|". - $this->yellow->config->get("systemDir").")"; - $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 command help - public function getCommandHelp() { - $data = array(); - foreach ($this->yellow->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onCommandHelp")) { - foreach (preg_split("/[\r\n]+/", $value["obj"]->onCommandHelp()) as $line) { - list($command) = explode(" ", $line); - if (!empty($command) && is_null($data[$command])) $data[$command] = $line; - } - } - } - uksort($data, "strnatcasecmp"); - return $data; - } - - // Return software version - public function getSoftwareVersion($latest = false) { - $data = array(); - if ($this->yellow->plugins->isExisting("update")) { - list($statusCode, $data) = $this->yellow->plugins->get("update")->getSoftwareVersion($latest); - } else { - $statusCode = 200; - $data = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData()); - } - return array($statusCode, $data); - } - - // 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/plugins/core.php b/system/plugins/core.php @@ -1,3122 +0,0 @@ -<?php -// Core plugin, https://github.com/datenstrom/yellow-plugins/tree/master/core -// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowCore { - const VERSION = "0.8.1"; - public $page; //current page - public $pages; //pages from file system - public $files; //files from file system - public $plugins; //plugins - public $themes; //themes - public $config; //configuration - public $text; //text - public $lookup; //location and file lookup - public $toolbox; //toolbox with helpers - - public function __construct() { - $this->page = new YellowPage($this); - $this->pages = new YellowPages($this); - $this->files = new YellowFiles($this); - $this->plugins = new YellowPlugins($this); - $this->themes = new YellowThemes($this); - $this->config = new YellowConfig($this); - $this->text = new YellowText($this); - $this->lookup = new YellowLookup($this); - $this->toolbox = new YellowToolbox(); - $this->config->setDefault("sitename", "Yellow"); - $this->config->setDefault("author", "Yellow"); - $this->config->setDefault("email", "webmaster"); - $this->config->setDefault("language", "en"); - $this->config->setDefault("timezone", "UTC"); - $this->config->setDefault("theme", "default"); - $this->config->setDefault("staticUrl", ""); - $this->config->setDefault("staticDefaultFile", "index.html"); - $this->config->setDefault("staticErrorFile", "404.html"); - $this->config->setDefault("staticDir", "public/"); - $this->config->setDefault("cacheDir", "cache/"); - $this->config->setDefault("mediaLocation", "/media/"); - $this->config->setDefault("downloadLocation", "/media/downloads/"); - $this->config->setDefault("imageLocation", "/media/images/"); - $this->config->setDefault("pluginLocation", "/media/plugins/"); - $this->config->setDefault("themeLocation", "/media/themes/"); - $this->config->setDefault("assetLocation", "/media/themes/assets/"); - $this->config->setDefault("mediaDir", "media/"); - $this->config->setDefault("downloadDir", "media/downloads/"); - $this->config->setDefault("imageDir", "media/images/"); - $this->config->setDefault("systemDir", "system/"); - $this->config->setDefault("configDir", "system/config/"); - $this->config->setDefault("pluginDir", "system/plugins/"); - $this->config->setDefault("themeDir", "system/themes/"); - $this->config->setDefault("assetDir", "system/themes/assets/"); - $this->config->setDefault("snippetDir", "system/themes/snippets/"); - $this->config->setDefault("templateDir", "system/themes/templates/"); - $this->config->setDefault("trashDir", "system/trash/"); - $this->config->setDefault("contentDir", "content/"); - $this->config->setDefault("contentRootDir", "default/"); - $this->config->setDefault("contentHomeDir", "home/"); - $this->config->setDefault("contentSharedDir", "shared/"); - $this->config->setDefault("contentPagination", "page"); - $this->config->setDefault("contentDefaultFile", "page.md"); - $this->config->setDefault("contentExtension", ".md"); - $this->config->setDefault("configExtension", ".ini"); - $this->config->setDefault("downloadExtension", ".download"); - $this->config->setDefault("configFile", "config.ini"); - $this->config->setDefault("textFile", "text.ini"); - $this->config->setDefault("newFile", "page-new-(.*).md"); - $this->config->setDefault("languageFile", "language-(.*).txt"); - $this->config->setDefault("serverUrl", ""); - $this->config->setDefault("startupUpdate", "none"); - $this->config->setDefault("template", "default"); - $this->config->setDefault("navigation", "navigation"); - $this->config->setDefault("header", "header"); - $this->config->setDefault("footer", "footer"); - $this->config->setDefault("sidebar", "sidebar"); - $this->config->setDefault("siteicon", "icon"); - $this->config->setDefault("parser", "markdown"); - $this->config->setDefault("multiLanguageMode", "0"); - $this->config->setDefault("safeMode", "0"); - } - - public function __destruct() { - $this->shutdown(); - } - - // Handle initialisation - public function load() { - if (defined("DEBUG") && DEBUG>=2) { - $serverVersion = $this->toolbox->getServerVersion(); - echo "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion<br/>\n"; - } - $this->toolbox->timerStart($time); - $this->config->load($this->config->get("configDir").$this->config->get("configFile")); - $this->lookup->load(); - $this->themes->load(); - $this->plugins->load(); - $this->text->load($this->config->get("pluginDir").$this->config->get("languageFile"), ""); - $this->text->load($this->config->get("configDir").$this->config->get("textFile"), $this->config->get("language")); - $this->toolbox->timerStop($time); - $this->startup(); - if (defined("DEBUG") && DEBUG>=2) { - $plugins = count($this->plugins->plugins); - $themes = count($this->themes->themes); - $languages = count($this->text->text); - echo "YellowCore::load plugins:$plugins themes:$themes languages:$languages time:$time ms<br/>\n"; - } - } - - // Handle request - public function request() { - ob_start(); - $statusCode = 0; - $this->toolbox->timerStart($time); - list($scheme, $address, $base, $location, $fileName) = $this->getRequestInformation(); - $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName); - foreach ($this->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onRequest")) { - $this->lookup->requestHandler = $key; - $statusCode = $value["obj"]->onRequest($scheme, $address, $base, $location, $fileName); - if ($statusCode!=0) break; - } - } - if ($statusCode==0) { - $this->lookup->requestHandler = "core"; - $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName, true); - } - if ($this->page->isExisting("pageError")) $statusCode = $this->processRequestError(); - $this->toolbox->timerStop($time); - ob_end_flush(); - if (defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName)) { - $handler = $this->getRequestHandler(); - echo "YellowCore::request status:$statusCode handler:$handler time:$time ms<br/>\n"; - } - return $statusCode; - } - - // Process request - public function processRequest($scheme, $address, $base, $location, $fileName, $cacheable) { - $statusCode = 0; - if (is_readable($fileName)) { - if ($this->toolbox->isRequestCleanUrl($location)) { - $location = $location.$this->getRequestLocationArgsClean(); - $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->sendStatus(303, $location); - } - } else { - if ($this->lookup->isRedirectLocation($location)) { - $location = $this->lookup->isFileLocation($location) ? "$location/" : "/".$this->getRequestLanguage()."/"; - $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->sendStatus(301, $location); - } - } - 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 (defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName)) { - echo "YellowCore::processRequest file:$fileName<br/>\n"; - } - return $statusCode; - } - - // 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"; - return $statusCode; - } - - // Read page - public function readPage($scheme, $address, $base, $location, $fileName, $cacheable, $statusCode, $pageError) { - if ($statusCode>=400) { - $language = $this->lookup->findLanguageFromFile($fileName, $this->config->get("language")); - if ($this->text->isExisting("error${statusCode}Title", $language)) { - $rawData = "---\nTitle:".$this->text->getText("error${statusCode}Title", $language)."\n"; - $rawData .= "Template:error\nLanguage:$language\n---\n".$this->text->getText("error${statusCode}Text", $language); - } else { - $rawData = "---\nTitle:".$this->toolbox->getHttpStatusFormatted($statusCode, true)."\n"; - $rawData .= "Template:error\nLanguage:en\n---\n[yellow error]"; - } - $cacheable = false; - } else { - $rawData = $this->toolbox->readFile($fileName); - } - $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; - } - - // Send page response - public function sendPage() { - $this->page->parsePage(); - $statusCode = $this->page->statusCode; - $lastModifiedFormatted = $this->page->getHeader("Last-Modified"); - if ($statusCode==200 && $this->page->isCacheable() && $this->toolbox->isRequestNotModified($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) { - foreach ($this->page->headerData as $key=>$value) { - echo "YellowCore::sendPage $key: $value<br/>\n"; - } - $theme = $this->page->get("theme"); - $template = $this->page->get("template"); - $parser = $this->page->get("parser"); - echo "YellowCore::sendPage theme:$theme template:$template parser:$parser<br/>\n"; - } - return $statusCode; - } - - // Send file response - public function sendFile($statusCode, $fileName, $cacheable) { - $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted($this->toolbox->getFileModified($fileName)); - if ($statusCode==200 && $cacheable && $this->toolbox->isRequestNotModified($lastModifiedFormatted)) { - $statusCode = 304; - @header($this->toolbox->getHttpStatusFormatted($statusCode)); - } else { - @header($this->toolbox->getHttpStatusFormatted($statusCode)); - if (!$cacheable) @header("Cache-Control: no-cache, must-revalidate"); - @header("Content-Type: ".$this->toolbox->getMimeContentType($fileName)); - @header("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, must-revalidate"); - @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)); - 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"; - } - } - return $statusCode; - } - - // Handle command - public function command($args = null) { - $statusCode = 0; - $this->toolbox->timerStart($time); - foreach ($this->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onCommand")) { - $this->lookup->commandHandler = $key; - $statusCode = $value["obj"]->onCommand(func_get_args()); - if ($statusCode!=0) break; - } - } - if ($statusCode==0) { - $this->lookup->commandHandler = "core"; - $statusCode = 400; - list($command) = func_get_args(); - echo "Yellow $command: Command not found\n"; - } - $this->toolbox->timerStop($time); - if (defined("DEBUG") && DEBUG>=1) { - $handler = $this->getCommandHandler(); - echo "YellowCore::command status:$statusCode handler:$handler time:$time ms<br/>\n"; - } - return $statusCode; - } - - // Handle startup - public function startup() { - $tokens = explode(",", $this->config->get("startupUpdate")); - foreach ($this->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onStartup")) $value["obj"]->onStartup(in_array($value["plugin"], $tokens)); - } - foreach ($this->themes->themes as $key=>$value) { - if (method_exists($value["obj"], "onStartup")) $value["obj"]->onStartup(in_array($value["theme"], $tokens)); - } - if ($this->config->get("startupUpdate")!="none") { - $fileNameConfig = $this->config->get("configDir").$this->config->get("configFile"); - $this->config->save($fileNameConfig, array("startupUpdate" => "none")); - } - } - - // Handle shutdown - public function shutdown() { - foreach ($this->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onShutdown")) $value["obj"]->onShutdown(); - } - foreach ($this->themes->themes as $key=>$value) { - if (method_exists($value["obj"], "onShutdown")) $value["obj"]->onShutdown(); - } - } - - // Include snippet - public function snippet($name, $args = null) { - $this->lookup->snippetArgs = func_get_args(); - $this->page->includePageSnippet($name); - } - - // Return snippet arguments - public function getSnippetArgs() { - return $this->lookup->snippetArgs; - } - - // Return request information - public function getRequestInformation($scheme = "", $address = "", $base = "") { - if (empty($scheme) && empty($address) && empty($base)) { - $url = $this->config->get("serverUrl"); - if (empty($url) || $this->isCommandLine()) $url = $this->toolbox->getServerUrl(); - list($scheme, $address, $base) = $this->lookup->getUrlInformation($url); - $this->config->set("serverScheme", $scheme); - $this->config->set("serverAddress", $address); - $this->config->set("serverBase", $base); - if (defined("DEBUG") && DEBUG>=3) echo "YellowCore::getRequestInformation $scheme://$address$base<br/>\n"; - } - $location = substru($this->toolbox->getLocation(), 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 request location - public function getRequestLocationArgsClean() { - return $this->toolbox->getLocationArgsClean($this->config->get("contentPagination")); - } - - // Return request language - public function getRequestLanguage() { - return $this->toolbox->detectBrowserLanguage($this->pages->getLanguages(), $this->config->get("language")); - } - - // 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 !empty($this->lookup->commandHandler); - } -} - -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 $safeMode; //page is parsed in safe mode? (boolean) - 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 - - 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; - } - - // Parse page data - public function parseData($rawData, $cacheable, $statusCode, $pageError = "") { - $this->rawData = $rawData; - $this->parser = null; - $this->parserData = ""; - $this->safeMode = intval($this->yellow->config->get("safeMode")); - $this->available = $this->yellow->lookup->isAvailableLocation($this->location, $this->fileName); - $this->visible = $this->yellow->lookup->isVisibleLocation($this->location, $this->fileName); - $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location); - $this->cacheable = $cacheable; - $this->lastModified = 0; - $this->statusCode = $statusCode; - $this->parseMeta($pageError); - } - - // Parse page data update - public function parseDataUpdate() { - if ($this->statusCode==0) { - $this->rawData = $this->yellow->toolbox->readFile($this->fileName); - $this->statusCode = 200; - $this->parseMeta(); - } - } - - // 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->config->get("language"))); - $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); - $this->parseMetaRaw(array("theme", "template", "sitename", "siteicon", "author", "navigation", "header", "footer", "sidebar", "parser")); - $titleHeader = ($this->location==$this->yellow->pages->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")=="hidden") $this->available = false; - $this->set("pageRead", $this->yellow->lookup->normaliseUrl( - $this->yellow->config->get("serverScheme"), - $this->yellow->config->get("serverAddress"), - $this->yellow->config->get("serverBase"), - $this->location)); - $this->set("pageEdit", $this->yellow->lookup->normaliseUrl( - $this->yellow->config->get("serverScheme"), - $this->yellow->config->get("serverAddress"), - $this->yellow->config->get("serverBase"), - rtrim($this->yellow->config->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->config->get("mediaDir"))); - $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->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onParseMeta")) $value["obj"]->onParseMeta($this); - } - } - - // Parse page meta data from raw data - public function parseMetaRaw($defaultKeys) { - foreach ($defaultKeys as $key) { - $value = $this->yellow->config->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) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !strempty($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]); - } - } - - // Parse page content on demand - public function parseContent($sizeMax = 0) { - if (!is_object($this->parser)) { - if ($this->yellow->plugins->isExisting($this->get("parser"))) { - $plugin = $this->yellow->plugins->plugins[$this->get("parser")]; - if (method_exists($plugin["obj"], "onParseContentRaw")) { - $this->parser = $plugin["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); - foreach ($this->yellow->plugins->plugins 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")) { - $this->set("description", $this->yellow->toolbox->createTextDescription($this->parserData, 150)); - } - if (!$this->isExisting("keywords")) { - $this->set("keywords", $this->yellow->toolbox->createTextKeywords($this->get("title"), 10)); - } - if (defined("DEBUG") && DEBUG>=3) echo "YellowPage::parseContent location:".$this->location."<br/>\n"; - } - } - - // Parse page content shortcut - public function parseContentShortcut($name, $text, $type) { - $output = null; - foreach ($this->yellow->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onParseContentShortcut")) { - $output = $value["obj"]->onParseContentShortcut($this, $name, $text, $type); - if (!is_null($output)) break; - } else if (method_exists($value["obj"], "onParseContentBlock")) { //TODO: remove later, old event handler - $output = $value["obj"]->onParseContentBlock($this, $name, $text, true); - if (!is_null($output)) break; - } - } - if (is_null($output)) { - if ($name=="yellow" && $type=="inline") { - $output = "Datenstrom Yellow ".YellowCore::VERSION; - if ($text=="error") $output = $this->get("pageError"); - if ($text=="version") { - $output = "<span class=\"".htmlspecialchars($name)."\">\n"; - $serverVersion = $this->yellow->toolbox->getServerVersion(); - $output .= "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion<br />\n"; - foreach (array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData()) as $key=>$value) { - $output .= htmlspecialchars("$key $value")."<br />\n"; - } - $output .= "</span>\n"; - if ($this->safeMode) $this->error(500, "Yellow '$text' is not available in safe mode!"); - } - } - } - if (defined("DEBUG") && DEBUG>=3 && !empty($name)) echo "YellowPage::parseContentShortcut name:$name type:$type<br/>\n"; - return $output; - } - - // Parse page - public function parsePage() { - $this->parsePageTemplate($this->get("template")); - if (!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, must-revalidate"); - if (!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8"); - if (!$this->isHeader("Page-Modified")) $this->setHeader("Page-Modified", $this->getModified(true)); - if (!$this->isHeader("Last-Modified")) $this->setHeader("Last-Modified", $this->getLastModified(true)); - if (!$this->yellow->text->isLanguage($this->get("language"))) { - $this->error(500, "Language '".$this->get("language")."' does not exist!"); - } - if (!$this->yellow->themes->isExisting($this->get("theme"))) { - $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->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->yellow->toolbox->isRequestSelf()) { - $this->error(404); - } - if ($this->isExisting("pageClean")) $this->outputData = null; - foreach ($this->yellow->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onParsePageOutput")) { - $output = $value["obj"]->onParsePageOutput($this, $this->outputData); - if (!is_null($output)) $this->outputData = $output; - } - } - } - - // Parse page template - public function parsePageTemplate($name) { - $this->outputData = null; - if (!$this->isError()) { - foreach ($this->yellow->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onParsePageTemplate")) { - $value["obj"]->onParsePageTemplate($this, $name); - } elseif (method_exists($value["obj"], "onParsePage")) { //TODO: remove later, old event handler - $value["obj"]->onParsePage(); - } - } - } - if (is_null($this->outputData)) { - ob_start(); - $this->includePageTemplate($name); - $this->outputData = ob_get_contents(); - ob_end_clean(); - } - } - - // Include page template - public function includePageTemplate($name) { - $fileNameTemplate = $this->yellow->config->get("templateDir").$this->yellow->lookup->normaliseName($name).".html"; - if (is_file($fileNameTemplate)) { - $this->setLastModified(filemtime($fileNameTemplate)); - global $yellow; - require($fileNameTemplate); - } else { - $this->error(500, "Template '$name' does not exist!"); - echo "Template error<br/>\n"; - } - } - - // Include page snippet - public function includePageSnippet($name) { - $fileNameSnippet = $this->yellow->config->get("snippetDir").$this->yellow->lookup->normaliseName($name).".php"; - if (is_file($fileNameSnippet)) { - $this->setLastModified(filemtime($fileNameSnippet)); - global $yellow; - require($fileNameSnippet); - } else { - $this->error(500, "Snippet '$name' does not exist!"); - echo "Snippet error<br/>\n"; - } - } - - // Set page meta data - public function set($key, $value) { - $this->metaData[$key] = $value; - } - - // Return page meta data - public function get($key) { - return $this->isExisting($key) ? $this->metaData[$key] : ""; - } - - // Return page meta data, HTML encoded - public function getHtml($key) { - return htmlspecialchars($this->get($key)); - } - - // Return page meta data as language specific date - public function getDate($key, $format = "") { - if (!empty($format)) { - $format = $this->yellow->text->get($format); - } else { - $format = $this->yellow->text->get("dateFormatMedium"); - } - return $this->yellow->text->getDateFormatted(strtotime($this->get($key)), $format); - } - - // Return page meta data as language specific date, HTML encoded - public function getDateHtml($key, $format = "") { - return htmlspecialchars($this->getDate($key, $format)); - } - - // Return page meta data as language specific date and relative to today - public function getDateRelative($key, $format = "", $daysLimit = 0) { - if (!empty($format)) { - $format = $this->yellow->text->get($format); - } else { - $format = $this->yellow->text->get("dateFormatMedium"); - } - return $this->yellow->text->getDateRelative(strtotime($this->get($key)), $format, $daysLimit); - } - - // Return page meta data as language specific date and relative to today, HTML encoded - public function getDateRelativeHtml($key, $format = "", $daysLimit = 0) { - return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit)); - } - - // Return page meta data as custom date - public function getDateFormatted($key, $format) { - return $this->yellow->text->getDateFormatted(strtotime($this->get($key)), $format); - } - - // Return page meta data as custom date, HTML encoded - public function getDateFormattedHtml($key, $format) { - return htmlspecialchars($this->getDateFormatted($key, $format)); - } - - // 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 $sizeMax ? substrb($text, 0, $sizeMax) : $text; - } - - // Return parent page, null if none - public function getParent() { - $parentLocation = $this->yellow->pages->getParentLocation($this->location); - return $this->yellow->pages->find($parentLocation); - } - - // Return top-level parent page, null if none - public function getParentTop($homeFallback = false) { - $parentTopLocation = $this->yellow->pages->getParentTopLocation($this->location); - if (!$this->yellow->pages->find($parentTopLocation) && $homeFallback) { - $parentTopLocation = $this->yellow->pages->getHomeLocation($this->location); - } - return $this->yellow->pages->find($parentTopLocation); - } - - // Return page collection with pages on the same level - public function getSiblings($showInvisible = false) { - $parentLocation = $this->yellow->pages->getParentLocation($this->location); - return $this->yellow->pages->getChildren($parentLocation, $showInvisible); - } - - // Return page collection with child pages - public function getChildren($showInvisible = false) { - return $this->yellow->pages->getChildren($this->location, $showInvisible); - } - - // Return page collection with sub pages - public function getChildrenRecursive($showInvisible = false, $levelMax = 0) { - return $this->yellow->pages->getChildrenRecursive($this->location, $showInvisible, $levelMax); - } - - // Set page collection with additional pages - public function setPages($pages) { - $this->pageCollection = $pages; - } - - // Return page collection with additional pages - public function getPages() { - return $this->pageCollection; - } - - // Set related page - public function setPage($key, $page) { - $this->pageRelations[$key] = $page; - } - - // Return related page - public function getPage($key) { - return !is_null($this->pageRelations[$key]) ? $this->pageRelations[$key] : $this; - } - - // Return page base - public function getBase($multiLanguage = false) { - return $multiLanguage ? rtrim($this->base.$this->yellow->pages->getHomeLocation($this->location), "/") : $this->base; - } - - // Return page location - public function getLocation($absoluteLocation = false) { - return $absoluteLocation ? $this->base.$this->location : $this->location; - } - - // Return page URL - public function getUrl() { - return $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, $this->base, $this->location); - } - - // Return page extra data - public function getExtra($name) { - $output = ""; - foreach ($this->yellow->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onParsePageExtra")) { - $outputPlugin = $value["obj"]->onParsePageExtra($this, $name); - if (!is_null($outputPlugin)) $output .= $outputPlugin; - } elseif (method_exists($value["obj"], "onExtra")) { //TODO: remove later, old event handler - $outputPlugin = $value["obj"]->onExtra($name); - if (!is_null($outputPlugin)) $output .= $outputPlugin; - } - } - if ($name=="header") { - if (is_file($this->yellow->config->get("assetDir").$this->get("theme").".css")) { - $location = $this->yellow->config->get("serverBase"). - $this->yellow->config->get("assetLocation").$this->get("theme").".css"; - $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"".htmlspecialchars($location)."\" />\n"; - } - if (is_file($this->yellow->config->get("assetDir").$this->get("theme").".js")) { - $location = $this->yellow->config->get("serverBase"). - $this->yellow->config->get("assetLocation").$this->get("theme").".js"; - $output .= "<script type=\"text/javascript\" src=\"".htmlspecialchars($location)."\"></script>\n"; - } - if (is_file($this->yellow->config->get("assetDir").$this->get("siteicon").".png")) { - $location = $this->yellow->config->get("serverBase"). - $this->yellow->config->get("assetLocation").$this->get("siteicon").".png"; - $contentType = $this->yellow->toolbox->getMimeContentType($location); - $output .= "<link rel=\"icon\" type=\"$contentType\" href=\"".htmlspecialchars($location)."\" />\n"; - $output .= "<link rel=\"apple-touch-icon\" type=\"$contentType\" href=\"".htmlspecialchars($location)."\" />\n"; - } - } - return $output; - } - - // Set page response output - public function setOutput($output) { - $this->outputData = $output; - } - - // 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] : ""; - } - - // 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->pageCollection->getModified(), - $this->yellow->config->getModified(), $this->yellow->text->getModified(), $this->yellow->plugins->getModified()); - foreach ($this->pageRelations as $page) $lastModified = max($lastModified, $page->getModified()); - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($lastModified) : $lastModified; - } - - // 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 $statusCode; - } - - // Respond with error page - public function error($statusCode, $pageError = "") { - if (!$this->isExisting("pageError") && $statusCode>0) { - $this->statusCode = $statusCode; - $this->set("pageError", empty($pageError) ? "Template/snippet error!" : $pageError); - } - } - - // 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, must-revalidate"); - } - $this->set("pageClean", (string)$statusCode); - } - } - - // 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 response header exists - public function isHeader($key) { - return !is_null($this->headerData[$key]); - } - - // Check if page meta data exists - public function isExisting($key) { - return !is_null($this->metaData[$key]); - } - - // Check if related page exists - public function isPage($key) { - return !is_null($this->pageRelations[$key]); - } -} - -class YellowDataCollection extends ArrayObject { - public function __construct() { - parent::__construct(array()); - } - - // Return array element - public function offsetGet($key) { - if (is_string($key)) $key = lcfirst($key); - return parent::offsetGet($key); - } - - // Set array element - public function offsetSet($key, $value) { - if (is_string($key)) $key = lcfirst($key); - parent::offsetSet($key, $value); - } - - // Remove array element - public function offsetUnset($key) { - if (is_string($key)) $key = lcfirst($key); - parent::offsetUnset($key); - } - - // Check if array element exists - public function offsetExists($key) { - if (is_string($key)) $key = lcfirst($key); - return parent::offsetExists($key); - } -} - -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; - } - - // Filter page collection by meta data - 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; - } - } - } - } - $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 meta data - public function sort($key, $ascendingOrder = true) { - $array = $this->getArrayCopy(); - foreach ($array as $page) { - $page->set("sortindex", ++$i); - } - $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; - } - - // Sort page collection by meta data similarity - public function similar($page, $ascendingOrder = false) { - $location = $page->location; - $keywords = $this->yellow->toolbox->createTextKeywords($page->get("title")); - $keywords .= ",".$page->get("tag").",".$page->get("author"); - $tokens = array_unique(array_filter(preg_split("/\s*,\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); - } - return $this; - } - - // Merge page collection - public function merge($input) { - $this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input)); - return $this; - } - - // Append to end of page collection - public function append($page) { - parent::append($page); - return $this; - } - - // Prepend to start of page collection - public function prepend($page) { - $array = $this->getArrayCopy(); - array_unshift($array, $page); - $this->exchangeArray($array); - return $this; - } - - // Limit the number of pages in page collection - public function limit($pagesMax) { - $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax)); - return $this; - } - - // Reverse page collection - public function reverse() { - $this->exchangeArray(array_reverse($this->getArrayCopy())); - return $this; - } - - // Randomize page collection - public function shuffle() { - $array = $this->getArrayCopy(); - shuffle($array); - $this->exchangeArray($array); - return $this; - } - - // Paginate page collection - public function pagination($limit, $reverse = true) { - $this->paginationNumber = 1; - $this->paginationCount = ceil($this->count() / $limit); - $pagination = $this->yellow->config->get("contentPagination"); - if (isset($_REQUEST[$pagination])) $this->paginationNumber = intval($_REQUEST[$pagination]); - 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 $this; - } - - // Return current page number in pagination - public function getPaginationNumber() { - return $this->paginationNumber; - } - - // Return highest page number in pagination - public function getPaginationCount() { - return $this->paginationCount; - } - - // Return location for a page in pagination - public function getPaginationLocation($absoluteLocation = true, $pageNumber = 1) { - if ($pageNumber>=1 && $pageNumber<=$this->paginationCount) { - $pagination = $this->yellow->config->get("contentPagination"); - $location = $this->yellow->page->getLocation($absoluteLocation); - $locationArgs = $this->yellow->toolbox->getLocationArgsNew( - $pageNumber>1 ? "$pagination:$pageNumber" : "$pagination:", $pagination); - } - return $location.$locationArgs; - } - - // Return location for previous page in pagination - public function getPaginationPrevious($absoluteLocation = true) { - $pageNumber = $this->paginationNumber-1; - return $this->getPaginationLocation($absoluteLocation, $pageNumber); - } - - // Return location for next page in pagination - public function getPaginationNext($absoluteLocation = true) { - $pageNumber = $this->paginationNumber+1; - return $this->getPaginationLocation($absoluteLocation, $pageNumber); - } - - // 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 $pageNumber; - } - - // Return page in collection, null if none - public function getPage($pageNumber = 1) { - return ($pageNumber>=1 && $pageNumber<=$this->count()) ? $this->offsetGet($pageNumber-1) : null; - } - - // 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 current page filter - public function getFilter() { - return $this->filterValue; - } - - // 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 there is a pagination - public function isPagination() { - return $this->paginationCount>1; - } -} - -class YellowPages { - public $yellow; //access to API - public $pages; //scanned pages - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->pages = array(); - } - - // Scan file system on demand - public function scanLocation($location) { - if (is_null($this->pages[$location])) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowPages::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) = explode(" ", $rootLocation, 2); - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $rootLocation, $fileName); - $page->parseData("", false, 0); - array_push($this->pages[$location], $page); - } - } 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); - } - } - } - return $this->pages[$location]; - } - - // Return page from file system, null if not found - public function find($location, $absoluteLocation = 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; - } - - // 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 page collection with top-level navigation - public function top($showInvisible = false) { - $rootLocation = $this->getRootLocation($this->yellow->page->location); - return $this->getChildren($rootLocation, $showInvisible); - } - - // 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); - } - return $pages; - } - - // Return page with shared content, null if not found - public function shared($location, $absoluteLocation = false, $name = "shared") { - if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); - $locationShared = $this->yellow->lookup->getDirectoryLocation($location); - $page = $this->find($locationShared.$name); - if ($page==null) { - $locationShared = $this->getHomeLocation($location).$this->yellow->config->get("contentSharedDir"); - $page = $this->find($locationShared.$name); - } - return $page; - } - - // 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); - } - } - } - 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(); - foreach ($this->scanLocation("") as $page) { - if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) array_push($languages, $page->get("language")); - } - return $languages; - } - - // 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; - } - - // 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 root location - public function getRootLocation($location) { - $rootLocation = "root/"; - if ($this->yellow->config->get("multiLanguageMode")) { - 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 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 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; - } -} - -class YellowFiles { - 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 (is_null($this->files[$location])) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowFiles::scanLocation location:$location<br/>\n"; - $this->files[$location] = array(); - $scheme = $this->yellow->page->scheme; - $address = $this->yellow->page->address; - $base = $this->yellow->config->get("serverBase"); - if (empty($location)) { - $fileNames = array($this->yellow->config->get("mediaDir")); - } 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); - } - } - 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); - } - } - return $this->files[$location]; - } - - // Return page with media file information, null if not found - public function find($location, $absoluteLocation = false) { - if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->config->get("serverBase"))); - 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 collection with all media files - public function index($showInvisible = false, $multiPass = false, $levelMax = 0) { - return $this->getChildrenRecursive("", $showInvisible, $levelMax); - } - - // Return page collection that's empty - public function clean() { - return new YellowPageCollection($this->yellow); - } - - // 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 $files; - } - - // 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; - } - - // Return home location - public function getHomeLocation($location) { - return $this->yellow->config->get("mediaLocation"); - } - - // Return parent location - public function getParentLocation($location) { - $token = rtrim($this->yellow->config->get("mediaLocation"), "/"); - if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { - if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; - } - if (empty($parentLocation)) $parentLocation = ""; - return $parentLocation; - } - - // Return top-level location - public function getParentTopLocation($location) { - $token = rtrim($this->yellow->config->get("mediaLocation"), "/"); - if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; - if (empty($parentTopLocation)) $parentTopLocation = "$token/"; - return $parentTopLocation; - } -} - -class YellowPlugins { - public $yellow; //access to API - public $modified; //plugin modification date - public $plugins; //registered plugins - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->modified = 0; - $this->plugins = array(); - } - - // Load plugins - public function load($path = "") { - $path = empty($path) ? $this->yellow->config->get("pluginDir") : $path; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) { - if (defined("DEBUG") && DEBUG>=3) echo "YellowPlugins::load file:$entry<br/>\n"; - $this->modified = max($this->modified, filemtime($entry)); - global $yellow; - require_once($entry); - $name = $this->yellow->lookup->normaliseName(basename($entry), true, true, true); - $this->register($name, "Yellow".ucfirst($name)); - } - $callback = function ($a, $b) { - return $a["priority"] - $b["priority"]; - }; - uasort($this->plugins, $callback); - foreach ($this->plugins as $key=>$value) { - if (method_exists($this->plugins[$key]["obj"], "onLoad")) $this->plugins[$key]["obj"]->onLoad($this->yellow); - } - } - - // Register plugin - public function register($name, $plugin, $obsoleteVersion = 0, $obsoletePriority = 0) { //TODO: remove obsolete arguments later - if (!$this->isExisting($name) && class_exists($plugin)) { - $this->plugins[$name] = array(); - $this->plugins[$name]["obj"] = new $plugin; - $this->plugins[$name]["plugin"] = $plugin; - $this->plugins[$name]["version"] = defined("$plugin::VERSION") ? $plugin::VERSION : 0; - $this->plugins[$name]["priority"] = defined("$plugin::PRIORITY") ? $plugin::PRIORITY : count($this->plugins) + 10; - } - } - - // Return plugin - public function get($name) { - return $this->plugins[$name]["obj"]; - } - - // Return plugin version - public function getData() { - $data = array(); - $data["YellowCore"] = YellowCore::VERSION; - foreach ($this->plugins as $key=>$value) { - if (empty($value["plugin"]) || empty($value["version"])) continue; - $data[$value["plugin"]] = $value["version"]; - } - uksort($data, "strnatcasecmp"); - return $data; - } - - // Return plugin modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; - } - - // Check if plugin exists - public function isExisting($name) { - return !is_null($this->plugins[$name]); - } -} - -class YellowThemes { - public $yellow; //access to API - public $modified; //theme modification date - public $themes; //themes - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->modified = 0; - $this->themes = array(); - } - - // Load themes - public function load($path = "") { - $path = empty($path) ? $this->yellow->config->get("assetDir") : $path; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) { - if (defined("DEBUG") && DEBUG>=3) echo "YellowThemes::load file:$entry<br/>\n"; - $this->modified = max($this->modified, filemtime($entry)); - global $yellow; - require_once($entry); - $name = $this->yellow->lookup->normaliseName(basename($entry), true, true, true); - $this->register($name, "YellowTheme".ucfirst($name)); - } - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false) as $entry) { - if (defined("DEBUG") && DEBUG>=3) echo "YellowThemes::load file:$entry<br/>\n"; - $this->modified = max($this->modified, filemtime($entry)); - $name = $this->yellow->lookup->normaliseName(basename($entry), true, true, true); - if (substru($name, 0, 7)!="bundle-") $this->register($name, "stdClass"); - } - $callback = function ($a, $b) { - return $a["priority"] - $b["priority"]; - }; - uasort($this->themes, $callback); - foreach ($this->themes as $key=>$value) { - if (method_exists($this->themes[$key]["obj"], "onLoad")) $this->themes[$key]["obj"]->onLoad($this->yellow); - } - } - - // Register theme - public function register($name, $theme, $obsoleteVersion = 0, $obsoletePriority = 0) { //TODO: remove obsolete arguments later - if (!$this->isExisting($name) && class_exists($theme)) { - $this->themes[$name] = array(); - $this->themes[$name]["obj"] = new $theme; - $this->themes[$name]["theme"] = $theme; - $this->themes[$name]["version"] = defined("$theme::VERSION") ? $theme::VERSION : 0; - $this->themes[$name]["priority"] = defined("$theme::PRIORITY") ? $theme::PRIORITY : count($this->themes) + 10; - } - } - - // Return theme - public function get($name) { - return $this->theme[$name]["obj"]; - } - - // Return theme version - public function getData() { - $data = array(); - foreach ($this->themes as $key=>$value) { - if (empty($value["theme"]) || empty($value["version"])) continue; - $data[$value["theme"]] = $value["version"]; - } - uksort($data, "strnatcasecmp"); - return $data; - } - - // Return theme modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; - } - - // Check if theme exists - public function isExisting($name) { - return !is_null($this->themes[$name]); - } -} - -class YellowConfig { - public $yellow; //access to API - public $modified; //configuration modification date - public $config; //configuration - public $configDefaults; //configuration defaults - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->modified = 0; - $this->config = new YellowDataCollection(); - $this->configDefaults = new YellowDataCollection(); - } - - // Load configuration from file - public function load($fileName) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowConfig::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; - 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 "YellowConfig::load $matches[1]:$matches[2]<br/>\n"; - } - } - } - - // Save configuration to file - public function save($fileName, $config) { - $configNew = new YellowDataCollection(); - foreach ($config as $key=>$value) { - if (!empty($key) && !strempty($value)) { - $this->set($key, $value); - $configNew[$key] = $value; - } - } - $this->modified = time(); - $fileData = $this->yellow->toolbox->readFile($fileName); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !is_null($configNew[$matches[1]])) { - $fileDataNew .= "$matches[1]: ".$configNew[$matches[1]]."\n"; - unset($configNew[$matches[1]]); - } else { - $fileDataNew .= $line; - } - } - foreach ($configNew as $key=>$value) { - $fileDataNew .= ucfirst($key).": $value\n"; - } - return $this->yellow->toolbox->createFile($fileName, $fileDataNew); - } - - // Set default configuration - public function setDefault($key, $value) { - $this->configDefaults[$key] = $value; - } - - // Set configuration - public function set($key, $value) { - $this->config[$key] = $value; - } - - // Return configuration - public function get($key) { - if (!is_null($this->config[$key])) { - $value = $this->config[$key]; - } else { - $value = !is_null($this->configDefaults[$key]) ? $this->configDefaults[$key] : ""; - } - return $value; - } - - // Return configuration, HTML encoded - public function getHtml($key) { - return htmlspecialchars($this->get($key)); - } - - // Return configuration strings - public function getData($filterStart = "", $filterEnd = "") { - $config = array(); - if (empty($filterStart) && empty($filterEnd)) { - $config = array_merge($this->configDefaults->getArrayCopy(), $this->config->getArrayCopy()); - } else { - foreach (array_merge($this->configDefaults->getArrayCopy(), $this->config->getArrayCopy()) as $key=>$value) { - if (!empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $config[$key] = $value; - if (!empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $config[$key] = $value; - } - } - return $config; - } - - // Return configuration modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; - } - - // Check if configuration exists - public function isExisting($key) { - return !is_null($this->config[$key]); - } -} - -class YellowText { - public $yellow; //access to API - public $modified; //text modification date - public $text; //text - public $language; //current language - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->modified = 0; - $this->text = new YellowDataCollection(); - } - - // Load text strings from file - public function load($fileName, $languageDefault) { - $path = dirname($fileName); - $regex = "/^".basename($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; - 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 string for specific language - public function setText($key, $value, $language) { - if (is_null($this->text[$language])) $this->text[$language] = new YellowDataCollection(); - $this->text[$language][$key] = $value; - } - - // Return text string - public function get($key) { - return $this->getText($key, $this->language); - } - - // Return text string, HTML encoded - public function getHtml($key) { - return htmlspecialchars($this->getText($key, $this->language)); - } - - // Return text string for specific language - public function getText($key, $language) { - return $this->isExisting($key, $language) ? $this->text[$language][$key] : "[$key]"; - } - - // Return text string for specific language, HTML encoded - public function getTextHtml($key, $language) { - return htmlspecialchars($this->getText($key, $language)); - } - - // Return text strings - 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 $text; - } - - // Return human readable date, custom date - public function getDateFormatted($timestamp, $format) { - $dateMonths = preg_split("/\s*,\s*/", $this->get("dateMonths")); - $dateWeekdays = preg_split("/\s*,\s*/", $this->get("dateWeekdays")); - $month = $dateMonths[date("n", $timestamp) - 1]; - $weekday = $dateWeekdays[date("N", $timestamp) - 1]; - $timeZone = $this->yellow->config->get("timezone"); - $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); - } - - // Return human readable date, relative to today - public function getDateRelative($timestamp, $format, $daysLimit) { - $timeDifference = time() - $timestamp; - $days = abs(intval($timeDifference / 86400)); - if ($days<=$daysLimit || $daysLimit==0) { - $tokens = preg_split("/\s*,\s*/", $this->get($timeDifference>=0 ? "datePast" : "dateFuture")); - 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 = $this->getDateFormatted($timestamp, $format); - } - return $output; - } - - // Return languages - public function getLanguages() { - $languages = array(); - foreach ($this->text as $key=>$value) { - array_push($languages, $key); - } - return $languages; - } - - // Return text modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; - } - - // Normalise date into known format - public function normaliseDate($text) { - if (preg_match("/^\d+\-\d+$/", $text)) { - $output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatShort")); - } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) { - $output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatMedium")); - } elseif (preg_match("/^\d+\-\d+\-\d+ \d+\:\d+$/", $text)) { - $output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatLong")); - } else { - $output = $text; - } - return $output; - } - - // Check if language exists - public function isLanguage($language) { - return !is_null($this->text[$language]); - } - - // Check if text string exists - public function isExisting($key, $language = "") { - if (empty($language)) $language = $this->language; - return !is_null($this->text[$language]) && !is_null($this->text[$language][$key]); - } -} - -class YellowLookup { - public $yellow; //access to API - public $requestHandler; //request handler name - public $commandHandler; //command handler name - public $snippetArgs; //snippet arguments - - public function __construct($yellow) { - $this->yellow = $yellow; - } - - // Load file system information - public function load() { - list($pathRoot, $pathHome) = $this->detectFileSystem(); - $this->yellow->config->set("contentRootDir", $pathRoot); - $this->yellow->config->set("contentHomeDir", $pathHome); - date_default_timezone_set($this->yellow->config->get("timezone")); - } - - // Detect file system - public function detectFileSystem() { - $path = $this->yellow->config->get("contentDir"); - $pathRoot = $this->yellow->config->get("contentRootDir"); - $pathHome = $this->yellow->config->get("contentHomeDir"); - if (!$this->yellow->config->get("multiLanguageMode")) $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; - } - } - $pathHome = $this->normaliseToken($token)."/"; - } - return array($pathRoot, $pathHome); - } - - // Return root locations - public function findRootLocations($includePath = true) { - $locations = array(); - $pathBase = $this->yellow->config->get("contentDir"); - $pathRoot = $this->yellow->config->get("contentRootDir"); - 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"; - } - } else { - array_push($locations, $includePath ? "root/ $pathBase" : "root/"); - } - return $locations; - } - - // Return location from file path - public function findLocationFromFile($fileName) { - $location = "/"; - $pathBase = $this->yellow->config->get("contentDir"); - $pathRoot = $this->yellow->config->get("contentRootDir"); - $pathHome = $this->yellow->config->get("contentHomeDir"); - $fileDefault = $this->yellow->config->get("contentDefaultFile"); - $fileExtension = $this->yellow->config->get("contentExtension"); - if (substru($fileName, 0, strlenu($pathBase))==$pathBase) { - $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); - } - $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"; - } - return $invalid ? "" : $location; - } - - // Return file path from location - public function findFileFromLocation($location, $directory = false) { - $path = $this->yellow->config->get("contentDir"); - $pathRoot = $this->yellow->config->get("contentRootDir"); - $pathHome = $this->yellow->config->get("contentHomeDir"); - $fileDefault = $this->yellow->config->get("contentDefaultFile"); - $fileExtension = $this->yellow->config->get("contentExtension"); - $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"; - } - } - } - return $invalid ? "" : $path; - } - - // 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; - } - } - } - if ($directory) $token .= "/"; - return ($default || $found) ? $token : ""; - } - - // 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; - } - } - } - return $includePath ? "$path/$token" : $token; - } - - // Return children from location - public function findChildrenFromLocation($location) { - $fileNames = array(); - $fileDefault = $this->yellow->config->get("contentDefaultFile"); - $fileExtension = $this->yellow->config->get("contentExtension"); - 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); - } - } - } - return $fileNames; - } - - // Return language from file path - public function findLanguageFromFile($fileName, $languageDefault) { - $language = $languageDefault; - $pathBase = $this->yellow->config->get("contentDir"); - $pathRoot = $this->yellow->config->get("contentRootDir"); - 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; - } - - // Return file path from media location - public function findFileFromMedia($location) { - if ($this->isFileLocation($location)) { - $mediaLocationLength = strlenu($this->yellow->config->get("mediaLocation")); - if (substru($location, 0, $mediaLocationLength)==$this->yellow->config->get("mediaLocation")) { - $fileName = $this->yellow->config->get("mediaDir").substru($location, 7); - } - } - return $fileName; - } - - // Return file path from system location - public function findFileFromSystem($location) { - if (preg_match("/\.(css|gif|ico|js|jpg|png|svg|txt|woff|woff2)$/", $location)) { - $pluginLocationLength = strlenu($this->yellow->config->get("pluginLocation")); - $themeLocationLength = strlenu($this->yellow->config->get("themeLocation")); - if (substru($location, 0, $pluginLocationLength)==$this->yellow->config->get("pluginLocation")) { - $fileName = $this->yellow->config->get("pluginDir").substru($location, $pluginLocationLength); - } elseif (substru($location, 0, $themeLocationLength)==$this->yellow->config->get("themeLocation")) { - $fileName = $this->yellow->config->get("themeDir").substru($location, $themeLocationLength); - } - } - return $fileName; - } - - // Return file path from cache if possible - public function findFileFromCache($location, $fileName, $cacheable) { - if ($cacheable) { - $location .= $this->yellow->toolbox->getLocationArgs(); - $fileNameStatic = rtrim($this->yellow->config->get("cacheDir"), "/").$location; - if (!$this->isFileLocation($location)) $fileNameStatic .= $this->yellow->config->get("staticDefaultFile"); - if (is_readable($fileNameStatic)) $fileName = $fileNameStatic; - } - return $fileName; - } - - // 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); - } - - // 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); - } - - // Normalise prefix - public function normalisePrefix($text) { - if (preg_match("/^([\d\-\_\.]*)(.*)$/", $text, $matches)) $prefix = $matches[1]; - if (!empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-"; - return $prefix; - } - - // 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 $array; - } - - // 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->config->get("serverBase").$this->yellow->config->get("mediaLocation"); - if (preg_match("/^\#/", $location)) { - $location = $pageBase.$pageLocation.$location; - } elseif (!preg_match("/^\//", $location)) { - $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; - } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) { - $location = $pageBase.$location; - } - $location = strreplaceu("/./", "/", $location); - $location = strreplaceu(":", $this->yellow->toolbox->getLocationArgsSeparator(), $location); - } else { - if ($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter"; - } - return $location; - } - - // 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 && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter"; - $url = $location; - } - return $url; - } - - // Return URL information - public function getUrlInformation($url) { - if (preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, "/"), $matches)) { - $scheme = $matches[1]; - $address = $matches[2]; - $base = $matches[3]; - } - return array($scheme, $address, $base); - } - - // Return directory location - public function getDirectoryLocation($location) { - return ($pos = strrposu($location, "/")) ? substru($location, 0, $pos+1) : "/"; - } - - // Check if location is specifying root - public function isRootLocation($location) { - return $location[0]!="/"; - } - - // Check if location is specifying file or directory - public function isFileLocation($location) { - return substru($location, -1, 1)!="/"; - } - - // 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->config->get("multiLanguageMode"); - } - return $redirect; - } - - // Check if location contains nested directories - public function isNestedLocation($location, $fileName, $checkHomeLocation = false) { - $nested = false; - if (!$checkHomeLocation || $location==$this->yellow->pages->getHomeLocation($location)) { - $path = dirname($fileName); - if (count($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true; - } - return $nested; - } - - // Check if location is available - public function isAvailableLocation($location, $fileName) { - $available = true; - $pathBase = $this->yellow->config->get("contentDir"); - if (substru($fileName, 0, strlenu($pathBase))==$pathBase) { - $sharedLocation = $this->yellow->pages->getHomeLocation($location).$this->yellow->config->get("contentSharedDir"); - if (substru($location, 0, strlenu($sharedLocation))==$sharedLocation) $available = false; - } - return $available; - } - - // Check if location is visible - public function isVisibleLocation($location, $fileName) { - $visible = true; - $pathBase = $this->yellow->config->get("contentDir"); - if (substru($fileName, 0, strlenu($pathBase))==$pathBase) { - $fileName = substru($fileName, strlenu($pathBase)); - $tokens = explode("/", $fileName); - for ($i=0; $i<count($tokens)-1; ++$i) { - if (!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i])) { - $visible = false; - break; - } - } - } else { - $visible = false; - } - return $visible; - } - - // 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->pages->getHomeLocation($location)) { - $active = $this->getDirectoryLocation($currentLocation)==$location; - } else { - $active = substru($currentLocation, 0, strlenu($location))==$location; - } - } - return $active; - } - - // Check if file is valid - public function isValidFile($fileName) { - $contentDirLength = strlenu($this->yellow->config->get("contentDir")); - $mediaDirLength = strlenu($this->yellow->config->get("mediaDir")); - $systemDirLength = strlenu($this->yellow->config->get("systemDir")); - return substru($fileName, 0, $contentDirLength)==$this->yellow->config->get("contentDir") || - substru($fileName, 0, $mediaDirLength)==$this->yellow->config->get("mediaDir") || - substru($fileName, 0, $systemDirLength)==$this->yellow->config->get("systemDir"); - } - - // Check if content file - public function isContentFile($fileName) { - $contentDirLength = strlenu($this->yellow->config->get("contentDir")); - return substru($fileName, 0, $contentDirLength)==$this->yellow->config->get("contentDir"); - } - - // Check if media file - public function isMediaFile($fileName) { - $mediaDirLength = strlenu($this->yellow->config->get("mediaDir")); - return substru($fileName, 0, $mediaDirLength)==$this->yellow->config->get("mediaDir"); - } - - // Check if system file - public function isSystemFile($fileName) { - $systemDirLength = strlenu($this->yellow->config->get("systemDir")); - return substru($fileName, 0, $systemDirLength)==$this->yellow->config->get("systemDir"); - } -} - -class YellowToolbox { - - // Return server version from current HTTP request - public function getServerVersion($shortFormat = false) { - $serverVersion = strtoupperu(PHP_SAPI)." ".PHP_OS; - if (preg_match("/^(\S+)/", $_SERVER["SERVER_SOFTWARE"], $matches)) $serverVersion = $matches[1]." ".PHP_OS; - if ($shortFormat && preg_match("/^(\pL+)/u", $serverVersion, $matches)) $serverVersion = $matches[1]; - return $serverVersion; - } - - // Return server URL from current HTTP request - public function getServerUrl() { - $scheme = $this->getScheme(); - $address = $this->getAddress(); - $base = $this->getBase(); - return "$scheme://$address$base/"; - } - - // Return scheme from current HTTP request - public function getScheme() { - $scheme = ""; - if (preg_match("/^HTTP\//", $_SERVER["SERVER_PROTOCOL"])) { - $secure = isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"]!="off"; - $scheme = $secure ? "https" : "http"; - } - return $scheme; - } - - // Return address from current HTTP request - public function getAddress() { - $address = $_SERVER["SERVER_NAME"]; - $port = $_SERVER["SERVER_PORT"]; - if ($port!=80 && $port!=443) $address .= ":$port"; - return $address; - } - - // Return base from current HTTP request - public function getBase() { - $base = ""; - if (preg_match("/^(.*)\/.*\.php$/", $_SERVER["SCRIPT_NAME"], $matches)) $base = $matches[1]; - return $base; - } - - // Return location from current HTTP request - public function getLocation($filterStrict = true) { - $location = $_SERVER["REQUEST_URI"]; - $location = rawurldecode(($pos = strposu($location, "?")) ? substru($location, 0, $pos) : $location); - if ($filterStrict) { - $location = $this->normaliseTokens($location, true); - $separator = $this->getLocationArgsSeparator(); - if (preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches)) { - $_SERVER["LOCATION"] = $location = $matches[1]; - $_SERVER["LOCATION_ARGS"] = $matches[2]; - foreach (explode("/", $matches[2]) as $token) { - 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_ARGS"] = ""; - } - } - return $location; - } - - // Return location arguments from current HTTP request - public function getLocationArgs() { - return $_SERVER["LOCATION_ARGS"]; - } - - // Return location arguments from current HTTP request, modify existing arguments - public function getLocationArgsNew($arg, $pagination) { - $separator = $this->getLocationArgsSeparator(); - preg_match("/^(.*?):(.*)$/", $arg, $args); - foreach (explode("/", $_SERVER["LOCATION_ARGS"]) as $token) { - preg_match("/^(.*?)$separator(.*)$/", $token, $matches); - if ($matches[1]==$args[1]) { - $matches[2] = $args[2]; - $found = true; - } - if (!empty($matches[1]) && !strempty($matches[2])) { - if (!empty($locationArgs)) $locationArgs .= "/"; - $locationArgs .= "$matches[1]:$matches[2]"; - } - } - if (!$found && !empty($args[1]) && !strempty($args[2])) { - if (!empty($locationArgs)) $locationArgs .= "/"; - $locationArgs .= "$args[1]:$args[2]"; - } - if (!empty($locationArgs)) { - $locationArgs = $this->normaliseArgs($locationArgs, false, false); - if (!$this->isLocationArgsPagination($locationArgs, $pagination)) $locationArgs .= "/"; - } - return $locationArgs; - } - - // Return location arguments from current HTTP request, convert form parameters - public function getLocationArgsClean($pagination) { - foreach (array_merge($_GET, $_POST) as $key=>$value) { - if (!empty($key) && !strempty($value)) { - if (!empty($locationArgs)) $locationArgs .= "/"; - $key = strreplaceu(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $key); - $value = strreplaceu(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $value); - $locationArgs .= "$key:$value"; - } - } - if (!empty($locationArgs)) { - $locationArgs = $this->normaliseArgs($locationArgs, false, false); - if (!$this->isLocationArgsPagination($locationArgs, $pagination)) $locationArgs .= "/"; - } - return $locationArgs; - } - - // Return location arguments separator - public function getLocationArgsSeparator() { - return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ":" : "="; - } - - // Check if there are location arguments in current HTTP request - public function isLocationArgs($location = "") { - $location = empty($location) ? $_SERVER["LOCATION"].$_SERVER["LOCATION_ARGS"] : $location; - $separator = $this->getLocationArgsSeparator(); - return preg_match("/[^\/]+$separator.*$/", $location); - } - - // Check if there are pagination arguments in current HTTP request - public function isLocationArgsPagination($location, $pagination) { - $separator = $this->getLocationArgsSeparator(); - return preg_match("/^(.*\/)?$pagination$separator.*$/", $location); - } - - // Check if script location is requested - public function isRequestSelf() { - return substru($_SERVER["REQUEST_URI"], -10, 10)=="yellow.php"; - } - - // Check if clean URL is requested - public function isRequestCleanUrl($location) { - return (isset($_GET["clean-url"]) || isset($_POST["clean-url"])) && substru($location, -1, 1)=="/"; - } - - // Check if unmodified since last HTTP request - public function isRequestNotModified($lastModifiedFormatted) { - return isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && $_SERVER["HTTP_IF_MODIFIED_SINCE"]==$lastModifiedFormatted; - } - - // Normalise path or location, take care of relative path tokens - public function normaliseTokens($text, $prependSlash = false) { - $textFiltered = ""; - if ($prependSlash && $text[0]!="/") $textFiltered .= "/"; - for ($pos=0; $pos<strlenb($text); ++$pos) { - if ($text[$pos]=="/" || $pos==0) { - 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; - } - } - } - $textFiltered .= $text[$pos]; - } - return $textFiltered; - } - - // Normalise location arguments - public function normaliseArgs($text, $appendSlash = true, $filterStrict = true) { - if ($appendSlash) $text .= "/"; - if ($filterStrict) $text = strreplaceu(" ", "-", strtoloweru($text)); - $text = strreplaceu(":", $this->getLocationArgsSeparator(), $text); - return strreplaceu(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text)); - } - - // 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 $text; - } - - // Return timezone - public function getTimezone() { - $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 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"; - } - $serverProtocol = $_SERVER["SERVER_PROTOCOL"]; - if (!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1"; - return $shortFormat ? $text : "$serverProtocol $statusCode $text"; - } - - // Return human readable HTTP date - public function getHttpDateFormatted($timestamp) { - return gmdate("D, d M Y H:i:s", $timestamp)." GMT"; - } - - // 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]; - } - return $contentType; - } - - // Return file type - public function getFileType($fileName) { - return strtoloweru(($pos = strrposu($fileName, ".")) ? substru($fileName, $pos+1) : ""); - } - - // Return file group - public function getFileGroup($fileName, $path) { - preg_match("#^$path(.+?)\/#", $fileName, $matches); - return strtoloweru($matches[1]); - } - - // 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(); - $dirHandle = @opendir($path); - if ($dirHandle) { - $path = rtrim($path, "/"); - while (($entry = readdir($dirHandle))!==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); - } - } - } - if ($sort) natcasesort($entries); - closedir($dirHandle); - } - 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)); - } - } - return $entries; - } - - // Read file, empty string if not found - public function readFile($fileName, $sizeMax = 0) { - $fileData = ""; - $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); - } - fclose($fileHandle); - $ok = true; - } - return $ok; - } - - // 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 @copy($fileNameSource, $fileNameDestination); - } - - // 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 @rename($fileNameSource, $fileNameDestination); - } - - // Rename directory - public function renameDirectory($pathSource, $pathDestination, $mkdir = false) { - return $pathSource==$pathDestination || $this->renameFile($pathSource, $pathDestination, $mkdir); - } - - // 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); - } - 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->isDir()) { - @rmdir($file->getRealPath()); - } else { - @unlink($file->getRealPath()); - } - } - $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); - } - return $ok; - } - - // Set file modification date, Unix time - public function modifyFile($fileName, $modified) { - clearstatcache(true, $fileName); - return @touch($fileName, $modified); - } - - // Return file modification date, Unix time - public function getFileModified($fileName) { - return is_file($fileName) ? filemtime($fileName) : 0; - } - - // 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; - } - - // Return arguments from text string, space separated - public function getTextArgs($text, $optional = "-") { - $text = preg_replace("/\s+/s", " ", trim($text)); - $tokens = str_getcsv($text, " ", "\""); - foreach ($tokens as $key=>$value) { - if ($value==$optional) $tokens[$key] = ""; - } - return $tokens; - } - - // 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); - } - - // Create description from text string - public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") { - if (preg_match("/^<h1>.*?<\/h1>(.*)$/si", $text, $matches)) $text = $matches[1]; - if ($lengthMax==0) $lengthMax = strlenu($text); - if ($removeHtml) { - while (true) { - $elementFound = preg_match("/<\s*?([\/!]?\w*)(.*?)\s*?\>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); - $element = $matches[0][0]; - $elementName = $matches[1][0]; - $elementText = $matches[2][0]; - $elementOffsetBytes = $elementFound ? $matches[0][1] : strlenb($text); - $string = html_entity_decode(substrb($text, $offsetBytes, $elementOffsetBytes - $offsetBytes), ENT_QUOTES, "UTF-8"); - if (preg_match("/^(blockquote|br|div|h\d|hr|li|ol|p|pre|ul)/i", $elementName)) $string .= " "; - if (preg_match("/^\/(code|pre)/i", $elementName)) $string = preg_replace("/^(\d+\n){2,}$/", "", $string); - $string = preg_replace("/\s+/s", " ", $string); - if (substru($string, 0, 1)==" " && (empty($output) || substru($output, -1)==" ")) $string = substru($string, 1); - $length = strlenu($string); - $output .= substru($string, 0, $length<$lengthMax ? $length : $lengthMax-1); - $lengthMax -= $length; - if (!empty($element) && $element==$endMarker) { - $lengthMax = 0; - $endMarkerFound = true; - } - if ($lengthMax<=0 || !$elementFound) break; - $offsetBytes = $elementOffsetBytes + strlenb($element); - } - $output = rtrim($output); - if ($lengthMax<=0) $output .= $endMarkerFound ? $endMarkerText : "…"; - } else { - $elementsOpen = array(); - while (true) { - $elementFound = preg_match("/&.*?\;|<\s*?([\/!]?\w*)(.*?)\s*?\>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); - $element = $matches[0][0]; - $elementName = $matches[1][0]; - $elementText = $matches[2][0]; - $elementOffsetBytes = $elementFound ? $matches[0][1] : strlenb($text); - $string = substrb($text, $offsetBytes, $elementOffsetBytes - $offsetBytes); - $length = strlenu($string); - $output .= substru($string, 0, $length<$lengthMax ? $length : $lengthMax-1); - $lengthMax -= $length + ($element[0]=="&" ? 1 : 0); - if (!empty($element) && $element==$endMarker) { - $lengthMax = 0; - $endMarkerFound = true; - } - if ($lengthMax<=0 || !$elementFound) break; - if (!empty($elementName) && substru($elementText, -1)!="/" && - !preg_match("/^(area|br|col|hr|img|input|col|param|!)/i", $elementName)) { - if ($elementName[0]!="/") { - array_push($elementsOpen, $elementName); - } else { - array_pop($elementsOpen); - } - } - $output .= $element; - $offsetBytes = $elementOffsetBytes + strlenb($element); - } - $output = rtrim($output); - for ($i=count($elementsOpen)-1; $i>=0; --$i) { - if (!preg_match("/^(dl|ol|ul|table|tbody|thead|tfoot|tr)/i", $elementsOpen[$i])) break; - $output .= "</".$elementsOpen[$i].">"; - } - if ($lengthMax<=0) $output .= $endMarkerFound ? $endMarkerText : "…"; - for (; $i>=0; --$i) { - $output .= "</".$elementsOpen[$i].">"; - } - } - return $output; - } - - // Create keywords from text string - public function createTextKeywords($text, $keywordsMax = 0) { - $tokens = array_unique(preg_split("/[,\s\(\)\+\-]/", strtoloweru($text))); - foreach ($tokens as $key=>$value) { - if (strlenu($value)<3) unset($tokens[$key]); - } - if ($keywordsMax) $tokens = array_slice($tokens, 0, $keywordsMax); - return implode(", ", $tokens); - } - - // Create title from text string - public function createTextTitle($text) { - if (preg_match("/^.*\/([\w\-]+)/", $text, $matches)) $text = strreplaceu("-", " ", ucfirst($matches[1])); - return $text; - } - - // 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); - } - if (empty($dataBuffer) && function_exists("mcrypt_create_iv")) { - $dataBuffer = @mcrypt_create_iv($dataBufferSize, MCRYPT_DEV_URANDOM); - } - if (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 { - $salt = substrb(bin2hex($dataBuffer), 0, $length); - } - } - 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; - } - 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; - } - 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]; - } - } - return $ok; - } - - // 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) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (lcfirst($matches[1])==$key && !strempty($matches[2])) { - $value = $matches[2]; - break; - } - } - } - return $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)) { - $key = lcfirst($key); - foreach ($this->getTextLines($parts[2]) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (lcfirst($matches[1])==$key) { - $rawDataNew .= "$matches[1]: $value\n"; - $found = true; - } else { - $rawDataNew .= $line; - } - } - if (!$found) $rawDataNew .= ucfirst($key).": $value\n"; - $rawDataNew = $parts[1]."---\n".$rawDataNew."---\n".$parts[3]; - } else { - $rawDataNew = $rawData; - } - return $rawDataNew; - } - - // Detect web browser language - public function detectBrowserLanguage($languages, $languageDefault) { - $languageFound = $languageDefault; - if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) { - foreach (preg_split("/\s*,\s*/", $_SERVER["HTTP_ACCEPT_LANGUAGE"]) as $string) { - list($language) = explode(";", $string); - if (in_array($language, $languages)) { - $languageFound = $language; - break; - } - } - } - return $languageFound; - } - - // 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 (.*?)>/", $dataBuffer, $matches)) { - if (preg_match("/ width=\"(\d+)\"/", $matches[1], $tokens)) $width = $tokens[1]; - if (preg_match("/ height=\"(\d+)\"/", $matches[1], $tokens)) $height = $tokens[1]; - $type = $fileType; - } - } - fclose($fileHandle); - } - return array($width, $height, $type); - } - - // Start timer - public function timerStart(&$time) { - $time = microtime(true); - } - - // Stop timer and calculate elapsed time in milliseconds - public function timerStop(&$time) { - $time = intval((microtime(true)-$time) * 1000); - } -} - -// Unicode support for PHP -mb_internal_encoding("UTF-8"); -function strempty($string) { - return is_null($string) || $string===""; -} -function strencode($string) { - return addcslashes($string, "\'\"\\\/"); -} -function strreplaceu() { - return call_user_func_array("str_replace", func_get_args()); -} -function strtoloweru() { - return call_user_func_array("mb_strtolower", func_get_args()); -} -function strtoupperu() { - return call_user_func_array("mb_strtoupper", func_get_args()); -} -function strlenu() { - return call_user_func_array("mb_strlen", func_get_args()); -} -function strlenb() { - return call_user_func_array("strlen", func_get_args()); -} -function strposu() { - return call_user_func_array("mb_strpos", func_get_args()); -} -function strposb() { - return call_user_func_array("strpos", func_get_args()); -} -function strrposu() { - return call_user_func_array("mb_strrpos", func_get_args()); -} -function strrposb() { - return call_user_func_array("strrpos", func_get_args()); -} -function substru() { - return call_user_func_array("mb_substr", func_get_args()); -} -function substrb() { - return call_user_func_array("substr", func_get_args()); -} - -// Error reporting for PHP -error_reporting(E_ALL ^ E_NOTICE); diff --git a/system/plugins/edit.css b/system/plugins/edit.css @@ -1,553 +0,0 @@ -/* Edit plugin, https://github.com/datenstrom/yellow-plugins/tree/master/edit */ -/* Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se */ -/* This file may be used and distributed under the terms of the public license. */ - -.yellow-bar { - position: relative; - line-height: 2em; - margin-bottom: 10px; -} -.yellow-bar-left { - display: block; - float: left; -} -.yellow-bar-right { - display: block; - float: right; -} -.yellow-bar-right a { - margin-left: 1em; -} -.yellow-bar-right #yellow-pane-create-link { - padding: 0 0.5em; -} -.yellow-bar-right #yellow-pane-delete-link { - padding: 0 0.5em; -} -.yellow-bar-banner { - clear: both; -} -.yellow-body-modal-open { - overflow: hidden; -} -.yellow-pane { - position: absolute; - display: none; - z-index: 100; - padding: 10px; - background-color: #fff; - color: #000; - border: 1px solid #bbb; - border-radius: 4px; - box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2); -} -.yellow-pane h1 { - color: #000; - font-size: 2em; - margin: 0 1em; -} -.yellow-pane p { - margin: 0.5em; -} -.yellow-pane .yellow-status { - margin-bottom: 1em; -} -.yellow-pane .yellow-fields { - width: 15em; - text-align: left; - margin: 0 auto; -} -.yellow-pane .yellow-form-control { - width: 15em; - box-sizing: border-box; -} -.yellow-pane .yellow-fields .yellow-btn { - width: 15em; - margin: 1em 0 0.5em 0; -} -.yellow-pane .yellow-buttons .yellow-btn { - width: 15em; - margin: 0.5em 0; -} -.yellow-close { - position: absolute; - top: 0.8em; - right: 1em; - cursor: pointer; - font-size: 0.9em; - color: #bbb; - text-decoration: none; -} -.yellow-close:hover { - color: #000; - text-decoration: none; -} -.yellow-arrow { - position: absolute; - top: 0; - left: 0; -} -.yellow-arrow:after, -.yellow-arrow:before { - position: absolute; - pointer-events: none; - bottom: 100%; - height: 0; - width: 0; - border: solid transparent; - content: ""; -} -.yellow-arrow:after { - border-color: rgba(255, 255, 255, 0); - border-bottom-color: #fff; - border-width: 10px; - margin-left: -10px; -} -.yellow-arrow:before { - border-color: rgba(187, 187, 187, 0); - border-bottom-color: #bbb; - border-width: 11px; - margin-left: -11px; -} -.yellow-popup { - position: absolute; - display: none; - z-index: 200; - padding: 10px 0; - background-color: #fff; - color: #000; - border: 1px solid #bbb; - border-radius: 4px; - box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2); -} -.yellow-dropdown { - list-style: none; - margin: 0; - padding: 0; -} -.yellow-dropdown span { - display: block; - margin: 0; - padding: 0.25em 1em; -} -.yellow-dropdown a { - display: block; - padding: 0.2em 1em; - text-decoration: none; -} -.yellow-dropdown a:hover { - color: #fff; - background-color: #18e; - text-decoration: none; -} -.yellow-dropdown-menu a { - color: #000; -} -.yellow-toolbar { - list-style: none; - margin: 0; - padding: 0; -} -.yellow-toolbar-left { - display: inline-block; - float: left; -} -.yellow-toolbar-right { - display: inline-block; - float: right; -} -.yellow-toolbar-banner { - clear: both; -} -.yellow-toolbar li { - display: inline-block; - vertical-align: top; -} -.yellow-toolbar a { - display: inline-block; - padding: 6px 16px; - text-decoration: none; - background-color: #fff; - color: #000; - font-size: 0.9em; - font-weight: normal; - border: 1px solid #bbb; - border-radius: 4px; -} -.yellow-toolbar a:hover { - background-color: #18e; - background-image: none; - border-color: #18e; - color: #fff; - text-decoration: none; -} -.yellow-toolbar-left a { - margin-right: 4px; - margin-bottom: 10px; -} -.yellow-toolbar-right a { - margin-left: 4px; - margin-bottom: 10px; -} -.yellow-toolbar .yellow-icon { - font-size: 0.9em; - min-width: 1em; - text-align: center; -} -.yellow-toolbar .yellow-toolbar-btn { - padding: 6px 10px; - min-width: 4em; - text-align: center; -} -.yellow-toolbar .yellow-toolbar-btn-edit { - background-color: #29f; - border-color: #29f; - color: #fff; -} -.yellow-toolbar .yellow-toolbar-btn-create { - background-color: #29f; - border-color: #29f; - color: #fff; -} -.yellow-toolbar .yellow-toolbar-btn-delete { - background-color: #e55; - border-color: #e55; - color: #fff; -} -.yellow-toolbar .yellow-toolbar-btn-delete:hover { - background-color: #d44; - border-color: #d44; -} -.yellow-toolbar .yellow-toolbar-btn-separator { - visibility: hidden; - padding: 6px; -} -.yellow-toolbar .yellow-toolbar-checked { - background-color: #666; - border-color: #666; - color: #fff; -} -.yellow-toolbar-tooltip { - position: relative; -} -.yellow-toolbar-tooltip::after, -.yellow-toolbar-tooltip::before { - position: absolute; - z-index: 300; - display: none; - pointer-events: none; -} -.yellow-toolbar-tooltip::after { - padding: 2px 9px; - font-weight: normal; - font-size: 0.9em; - text-align: center; - white-space: nowrap; - content: attr(aria-label); - background-color: #111; - color: #ddd; - border-radius: 3px; - top: 100%; - right: 50%; - margin-top: 6px; - transform: translateX(50%); -} -.yellow-toolbar-tooltip::before { - width: 0; - height: 0; - content: ""; - border: 4px solid transparent; - top: auto; - right: 50%; - bottom: -6px; - margin-right: -4px; - border-bottom-color: #111; -} -.yellow-toolbar-tooltip:hover::before, -.yellow-toolbar-tooltip:hover::after { - display: inline-block; -} -.yellow-toolbar-selected.yellow-toolbar-tooltip::before, -.yellow-toolbar-selected.yellow-toolbar-tooltip::after { - display: none; -} -.yellow-form-control { - margin: 0; - padding: 2px 4px; - display: inline-block; - background-color: #fff; - color: #000; - 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; -} -.yellow-btn { - margin: 0; - padding: 4px 22px; - display: inline-block; - min-width: 8em; - 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; -} -.yellow-btn:hover, -.yellow-btn:focus, -.yellow-btn:active { - color: #333333; - background-image: none; - text-decoration: none; -} -.yellow-btn:active { - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); -} - -/* Specific panes */ - -#yellow-pane-login, -#yellow-pane-signup, -#yellow-pane-forgot, -#yellow-pane-recover, -#yellow-pane-settings, -#yellow-pane-version, -#yellow-pane-quit { - text-align: center; -} -#yellow-pane-edit-toolbar-title { - margin: -5px 0 0 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -#yellow-pane-edit-text { - padding: 0 2px; - outline: none; - resize: none; - border: none; -} -#yellow-pane-edit-preview { - padding: 0; - overflow: auto; -} -#yellow-pane-edit-preview h1 { - margin: 0.67em 0; -} -#yellow-pane-edit-preview p { - margin: 1em 0; -} -#yellow-pane-edit-preview .content { - margin: 0; - padding: 0; -} -#yellow-pane-user { - padding: 10px 0; -} - -/* Specific popups */ - -#yellow-popup-format, -#yellow-popup-heading, -#yellow-popup-list { - width: 16em; -} -#yellow-popup-format a, -#yellow-popup-heading a { - padding: 0.25em 16px; -} -#yellow-popup-format #yellow-popup-format-h1, -#yellow-popup-heading #yellow-popup-heading-h1 { - font-size: 2em; - font-weight: bold; -} -#yellow-popup-format #yellow-popup-format-h2, -#yellow-popup-heading #yellow-popup-heading-h2 { - font-size: 1.6em; - font-weight: bold; -} -#yellow-popup-format #yellow-popup-format-h3, -#yellow-popup-heading #yellow-popup-heading-h3 { - font-size: 1.3em; - font-weight: bold; -} -#yellow-popup-format #yellow-popup-format-quote { - font-style: italic; -} -#yellow-popup-format #yellow-popup-format-pre { - font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-size: 0.9em; - line-height: 1.8; -} -#yellow-popup-emojiawesome { - padding: 10px; - width: 14em; -} -#yellow-popup-emojiawesome a { - padding: 0.2em; -} -#yellow-popup-emojiawesome .yellow-dropdown li { - display: inline-block; -} -#yellow-popup-fontawesome { - padding: 10px; - width: 13em; -} -#yellow-popup-fontawesome a { - padding: 0.18em 0.3em; - min-width: 1em; - text-align: center; -} -#yellow-popup-fontawesome .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; -} -.yellow-spin { - -webkit-animation: yellow-spin 1s infinite steps(16); - animation: yellow-spin 1s infinite steps(16); -} -@-webkit-keyframes yellow-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -@keyframes yellow-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - 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-italic:before { - content: "\f0f7"; -} -.yellow-icon-strikethrough:before { - content: "\f108"; -} -.yellow-icon-quote:before { - content: "\f109"; -} -.yellow-icon-code:before { - content: "\f10a"; -} -.yellow-icon-pre:before { - content: "\f10a"; -} -.yellow-icon-link:before { - content: "\f10b"; -} -.yellow-icon-file:before { - content: "\f10c"; -} -.yellow-icon-list:before { - content: "\f10d"; -} -.yellow-icon-ul:before { - content: "\f10d"; -} -.yellow-icon-ol:before { - content: "\f10e"; -} -.yellow-icon-tl:before { - content: "\f10f"; -} -.yellow-icon-hr:before { - content: "\f110"; -} -.yellow-icon-table:before { - content: "\f111"; -} -.yellow-icon-emojiawesome:before { - content: "\f112"; -} -.yellow-icon-fontawesome:before { - content: "\f113"; -} -.yellow-icon-draft:before { - content: "\f114"; -} -.yellow-icon-undo:before { - content: "\f115"; -} -.yellow-icon-redo:before { - content: "\f116"; -} -.yellow-icon-spinner:before { - content: "\f200"; -} -.yellow-icon-search:before { - content: "\f201"; -} -.yellow-icon-close:before { - content: "\f202"; -} -.yellow-icon-help:before { - content: "\f203"; -} -.yellow-icon-markdown:before { - content: "\f203"; -} -.yellow-icon-logo:before { - content: "\f8ff"; -} diff --git a/system/plugins/edit.js b/system/plugins/edit.js @@ -1,1317 +0,0 @@ -// Edit plugin, https://github.com/datenstrom/yellow-plugins/tree/master/edit -// Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -var yellow = { - - // Main event handlers - action: function(action, status, args) { yellow.edit.action(action, status, args); }, - onLoad: function() { yellow.edit.load(); }, - onClickAction: function(e) { yellow.edit.clickAction(e); }, - onClick: function(e) { yellow.edit.click(e); }, - onKeydown: function(e) { yellow.edit.keydown(e); }, - onDrag: function(e) { yellow.edit.drag(e); }, - onDrop: function(e) { yellow.edit.drop(e); }, - onUpdate: function() { yellow.edit.updatePane(yellow.edit.paneId, yellow.edit.paneAction, yellow.edit.paneStatus); }, - onResize: function() { yellow.edit.resizePane(yellow.edit.paneId, yellow.edit.paneAction, yellow.edit.paneStatus); } -}; - -yellow.edit = { - paneId: 0, //visible pane ID - paneActionOld: 0, //previous pane action - paneAction: 0, //current pane action - paneStatus: 0, //current pane status - popupId: 0, //visible popup ID - intervalId: 0, //timer interval ID - - // Handle initialisation - load: function() { - var body = document.getElementsByTagName("body")[0]; - if (body && body.firstChild && !document.getElementById("yellow-bar")) { - this.createBar("yellow-bar"); - this.createPane("yellow-pane-edit", "none", "none"); - this.action(yellow.page.action, yellow.page.status); - clearInterval(this.intervalId); - } - }, - - // Handle action - action: function(action, status, args) { - status = status ? status : "none"; - args = args ? args : "none"; - switch (action) { - case "login": this.showPane("yellow-pane-login", action, status); break; - case "logout": this.sendPane("yellow-pane-logout", action); break; - case "signup": this.showPane("yellow-pane-signup", action, status); break; - case "confirm": this.showPane("yellow-pane-signup", action, status); break; - case "approve": this.showPane("yellow-pane-signup", action, status); break; - case "forgot": this.showPane("yellow-pane-forgot", action, status); break; - case "recover": this.showPane("yellow-pane-recover", action, status); break; - case "reactivate": this.showPane("yellow-pane-settings", action, status); break; - case "settings": this.showPane("yellow-pane-settings", action, status); break; - case "verify": this.showPane("yellow-pane-settings", action, status); break; - case "change": this.showPane("yellow-pane-settings", action, status); break; - case "version": this.showPane("yellow-pane-version", action, status); break; - case "update": this.sendPane("yellow-pane-update", action, status, args); break; - case "quit": this.showPane("yellow-pane-quit", action, status); break; - case "remove": this.showPane("yellow-pane-quit", action, status); break; - case "create": this.showPane("yellow-pane-edit", action, status, true); break; - case "edit": this.showPane("yellow-pane-edit", action, status, true); break; - case "delete": this.showPane("yellow-pane-edit", action, status, true); break; - case "user": this.showPane("yellow-pane-user", action, status); break; - case "send": this.sendPane(this.paneId, this.paneAction); break; - case "close": this.hidePane(this.paneId); break; - case "toolbar": this.processToolbar(status, args); break; - case "help": this.processHelp(); break; - } - }, - - // Handle action clicked - clickAction: function(e) { - e.stopPropagation(); - e.preventDefault(); - var element = e.target; - for (; element; element=element.parentNode) { - if (element.tagName=="A") break; - } - this.action(element.getAttribute("data-action"), element.getAttribute("data-status"), element.getAttribute("data-args")); - }, - - // Handle mouse clicked - click: function(e) { - if (this.popupId && !document.getElementById(this.popupId).contains(e.target)) this.hidePopup(this.popupId, true); - if (this.paneId && !document.getElementById(this.paneId).contains(e.target)) this.hidePane(this.paneId, true); - }, - - // Handle keyboard - keydown: function(e) { - if (this.paneId=="yellow-pane-edit") this.processShortcut(e); - if (this.paneId && e.keyCode==27) this.hidePane(this.paneId); - }, - - // Handle drag - drag: function(e) { - e.stopPropagation(); - e.preventDefault(); - }, - - // Handle drop - drop: function(e) { - e.stopPropagation(); - e.preventDefault(); - var elementText = document.getElementById("yellow-pane-edit-text"); - var files = e.dataTransfer ? e.dataTransfer.files : e.target.files; - for (var i=0; i<files.length; i++) this.uploadFile(elementText, files[i]); - }, - - // Create bar - createBar: function(barId) { - if (yellow.config.debug) console.log("yellow.edit.createBar id:"+barId); - var elementBar = document.createElement("div"); - elementBar.className = "yellow-bar"; - elementBar.setAttribute("id", barId); - if (barId=="yellow-bar") { - yellow.toolbox.addEvent(document, "click", yellow.onClick); - yellow.toolbox.addEvent(document, "keydown", yellow.onKeydown); - yellow.toolbox.addEvent(window, "resize", yellow.onResize); - } - var elementDiv = document.createElement("div"); - elementDiv.setAttribute("id", barId+"-content"); - if (yellow.config.userName) { - elementDiv.innerHTML = - "<div class=\"yellow-bar-left\">"+ - "<a href=\"#\" id=\"yellow-pane-edit-link\" data-action=\"edit\">"+this.getText("Edit")+"</a>"+ - "</div>"+ - "<div class=\"yellow-bar-right\">"+ - "<a href=\"#\" id=\"yellow-pane-create-link\" data-action=\"create\">"+this.getText("Create")+"</a>"+ - "<a href=\"#\" id=\"yellow-pane-delete-link\" data-action=\"delete\">"+this.getText("Delete")+"</a>"+ - "<a href=\"#\" id=\"yellow-pane-user-link\" data-action=\"user\">"+yellow.toolbox.encodeHtml(yellow.config.userName)+"</a>"+ - "</div>"+ - "<div class=\"yellow-bar-banner\"></div>"; - } - elementBar.appendChild(elementDiv); - yellow.toolbox.insertBefore(elementBar, document.getElementsByTagName("body")[0].firstChild); - this.bindActions(elementBar); - }, - - // Create pane - createPane: function(paneId, paneAction, paneStatus) { - if (yellow.config.debug) console.log("yellow.edit.createPane id:"+paneId); - var elementPane = document.createElement("div"); - elementPane.className = "yellow-pane"; - elementPane.setAttribute("id", paneId); - elementPane.style.display = "none"; - if (paneId=="yellow-pane-edit") { - yellow.toolbox.addEvent(elementPane, "input", yellow.onUpdate); - yellow.toolbox.addEvent(elementPane, "dragenter", yellow.onDrag); - yellow.toolbox.addEvent(elementPane, "dragover", yellow.onDrag); - yellow.toolbox.addEvent(elementPane, "drop", yellow.onDrop); - } - if (paneId=="yellow-pane-edit" || paneId=="yellow-pane-user") { - var elementArrow = document.createElement("span"); - elementArrow.className = "yellow-arrow"; - elementArrow.setAttribute("id", paneId+"-arrow"); - elementPane.appendChild(elementArrow); - } - var elementDiv = document.createElement("div"); - elementDiv.className = "yellow-content"; - elementDiv.setAttribute("id", paneId+"-content"); - switch (paneId) { - 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>"+ - "<div class=\"yellow-title\"><h1>"+this.getText("LoginTitle")+"</h1></div>"+ - "<div class=\"yellow-fields\" id=\"yellow-pane-login-fields\">"+ - "<input type=\"hidden\" name=\"action\" value=\"login\" />"+ - "<p><label for=\"yellow-pane-login-email\">"+this.getText("LoginEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-login-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(yellow.config.editLoginEmail)+"\" /></p>"+ - "<p><label for=\"yellow-pane-login-password\">"+this.getText("LoginPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-login-password\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(yellow.config.editLoginPassword)+"\" /></p>"+ - "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("LoginButton")+"\" /></p>"+ - "</div>"+ - "<div class=\"yellow-actions\" id=\"yellow-pane-login-actions\">"+ - "<p><a href=\"#\" id=\"yellow-pane-login-forgot\" data-action=\"forgot\">"+this.getText("LoginForgot")+"</a><br /><a href=\"#\" id=\"yellow-pane-login-signup\" data-action=\"signup\">"+this.getText("LoginSignup")+"</a></p>"+ - "</div>"+ - "</form>"; - break; - 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>"+ - "<div class=\"yellow-title\"><h1>"+this.getText("SignupTitle")+"</h1></div>"+ - "<div class=\"yellow-status\"><p id=\"yellow-pane-signup-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ - "<div class=\"yellow-fields\" id=\"yellow-pane-signup-fields\">"+ - "<input type=\"hidden\" name=\"action\" value=\"signup\" />"+ - "<p><label for=\"yellow-pane-signup-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-signup-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+ - "<p><label for=\"yellow-pane-signup-email\">"+this.getText("SignupEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-signup-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ - "<p><label for=\"yellow-pane-signup-password\">"+this.getText("SignupPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-signup-password\" maxlength=\"64\" value=\"\" /></p>"+ - "<p><input type=\"checkbox\" name=\"consent\" value=\"consent\" id=\"consent\""+(this.getRequest("consent") ? " checked=\"checked\"" : "")+"> <label for=\"consent\">"+this.getText("SignupConsent")+"</label></p>"+ - "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("SignupButton")+"\" /></p>"+ - "</div>"+ - "<div class=\"yellow-buttons\" id=\"yellow-pane-signup-buttons\">"+ - "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ - "</div>"+ - "</form>"; - break; - 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>"+ - "<div class=\"yellow-title\"><h1>"+this.getText("ForgotTitle")+"</h1></div>"+ - "<div class=\"yellow-status\"><p id=\"yellow-pane-forgot-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ - "<div class=\"yellow-fields\" id=\"yellow-pane-forgot-fields\">"+ - "<input type=\"hidden\" name=\"action\" value=\"forgot\" />"+ - "<p><label for=\"yellow-pane-forgot-email\">"+this.getText("ForgotEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-forgot-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ - "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+ - "</div>"+ - "<div class=\"yellow-buttons\" id=\"yellow-pane-forgot-buttons\">"+ - "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ - "</div>"+ - "</form>"; - break; - 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>"+ - "<div class=\"yellow-title\"><h1>"+this.getText("RecoverTitle")+"</h1></div>"+ - "<div class=\"yellow-status\"><p id=\"yellow-pane-recover-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ - "<div class=\"yellow-fields\" id=\"yellow-pane-recover-fields\">"+ - "<p><label for=\"yellow-pane-recover-password\">"+this.getText("RecoverPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-recover-password\" maxlength=\"64\" value=\"\" /></p>"+ - "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+ - "</div>"+ - "<div class=\"yellow-buttons\" id=\"yellow-pane-recover-buttons\">"+ - "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ - "</div>"+ - "</form>"; - break; - case "yellow-pane-settings": - var rawDataLanguages = ""; - if (yellow.config.serverLanguages && Object.keys(yellow.config.serverLanguages).length>1) { - rawDataLanguages += "<p>"; - for (var language in yellow.config.serverLanguages) { - var checked = language==this.getRequest("language") ? " checked=\"checked\"" : ""; - rawDataLanguages += "<label for=\"yellow-pane-settings-"+language+"\"><input type=\"radio\" name=\"language\" id=\"yellow-pane-settings-"+language+"\" value=\""+language+"\""+checked+"> "+yellow.toolbox.encodeHtml(yellow.config.serverLanguages[language])+"</label><br />"; - } - rawDataLanguages += "</p>"; - } - 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-settings-title\">"+this.getText("SettingsTitle")+"</h1></div>"+ - "<div class=\"yellow-status\"><p id=\"yellow-pane-settings-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ - "<div class=\"yellow-fields\" id=\"yellow-pane-settings-fields\">"+ - "<input type=\"hidden\" name=\"action\" value=\"settings\" />"+ - "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+ - "<p><label for=\"yellow-pane-settings-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-settings-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+ - "<p><label for=\"yellow-pane-settings-email\">"+this.getText("SignupEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-settings-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+ - "<p><label for=\"yellow-pane-settings-password\">"+this.getText("SignupPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-settings-password\" maxlength=\"64\" value=\"\" /></p>"+rawDataLanguages+ - "<p>"+this.getText("SettingsQuit")+" <a href=\"#\" data-action=\"quit\">"+this.getText("SettingsMore")+"</a></p>"+ - "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+ - "</div>"+ - "<div class=\"yellow-buttons\" id=\"yellow-pane-settings-buttons\">"+ - "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ - "</div>"+ - "</form>"; - break; - case "yellow-pane-version": - 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-version-title\">"+yellow.toolbox.encodeHtml(yellow.config.serverVersion)+"</h1></div>"+ - "<div class=\"yellow-status\"><p id=\"yellow-pane-version-status\" class=\""+paneStatus+"\">"+this.getText("VersionStatus", "", paneStatus)+"</p></div>"+ - "<div class=\"yellow-output\" id=\"yellow-pane-version-output\">"+yellow.page.rawDataOutput+"</div>"+ - "<div class=\"yellow-buttons\" id=\"yellow-pane-version-buttons\">"+ - "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ - "</div>"+ - "</form>"; - break; - 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>"+ - "<div class=\"yellow-title\"><h1>"+this.getText("QuitTitle")+"</h1></div>"+ - "<div class=\"yellow-status\"><p id=\"yellow-pane-quit-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+ - "<div class=\"yellow-fields\" id=\"yellow-pane-quit-fields\">"+ - "<input type=\"hidden\" name=\"action\" value=\"quit\" />"+ - "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+ - "<p><label for=\"yellow-pane-quit-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-quit-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+ - "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("DeleteButton")+"\" /></p>"+ - "</div>"+ - "<div class=\"yellow-buttons\" id=\"yellow-pane-quit-buttons\">"+ - "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+ - "</div>"+ - "</form>"; - break; - case "yellow-pane-edit": - var rawDataButtons = ""; - if (yellow.config.editToolbarButtons && yellow.config.editToolbarButtons!="none") { - var tokens = yellow.config.editToolbarButtons.split(","); - for (var i=0; i<tokens.length; i++) { - var token = tokens[i].trim(); - if (token!="separator") { - rawDataButtons += "<li><a href=\"#\" id=\"yellow-toolbar-"+yellow.toolbox.encodeHtml(token)+"\" class=\"yellow-toolbar-btn-icon yellow-toolbar-tooltip\" data-action=\"toolbar\" data-status=\""+yellow.toolbox.encodeHtml(token)+"\" aria-label=\""+this.getText("Toolbar", "", token)+"\"><i class=\"yellow-icon yellow-icon-"+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>"; - } else { - rawDataButtons += "<li><a href=\"#\" class=\"yellow-toolbar-btn-separator\"></a></li>"; - } - } - if (yellow.config.debug) console.log("yellow.edit.createPane buttons:"+yellow.config.editToolbarButtons); - } - elementDiv.innerHTML = - "<form method=\"post\">"+ - "<div id=\"yellow-pane-edit-toolbar\">"+ - "<h1 id=\"yellow-pane-edit-toolbar-title\" class=\"yellow-toolbar yellow-toolbar-left\">"+this.getText("Edit")+"</h1>"+ - "<ul id=\"yellow-pane-edit-toolbar-buttons\" class=\"yellow-toolbar yellow-toolbar-left\">"+rawDataButtons+"</ul>"+ - "<ul id=\"yellow-pane-edit-toolbar-main\" class=\"yellow-toolbar yellow-toolbar-right\">"+ - "<li><a href=\"#\" id=\"yellow-pane-edit-cancel\" class=\"yellow-toolbar-btn\" data-action=\"close\">"+this.getText("CancelButton")+"</a></li>"+ - "<li><a href=\"#\" id=\"yellow-pane-edit-send\" class=\"yellow-toolbar-btn\" data-action=\"send\">"+this.getText("EditButton")+"</a></li>"+ - "</ul>"+ - "<ul class=\"yellow-toolbar yellow-toolbar-banner\"></ul>"+ - "</div>"+ - "<textarea id=\"yellow-pane-edit-text\" class=\"yellow-form-control\"></textarea>"+ - "<div id=\"yellow-pane-edit-preview\"></div>"+ - "</form>"; - break; - case "yellow-pane-user": - elementDiv.innerHTML = - "<ul class=\"yellow-dropdown\">"+ - "<li><span>"+yellow.toolbox.encodeHtml(yellow.config.userEmail)+"</span></li>"+ - "<li><a href=\"#\" data-action=\"settings\">"+this.getText("SettingsTitle")+"</a></li>" + - "<li><a href=\"#\" data-action=\"help\">"+this.getText("UserHelp")+"</a></li>" + - "<li><a href=\"#\" data-action=\"logout\">"+this.getText("UserLogout")+"</a></li>"+ - "</ul>"; - break; - } - elementPane.appendChild(elementDiv); - yellow.toolbox.insertAfter(elementPane, document.getElementsByTagName("body")[0].firstChild); - this.bindActions(elementPane); - }, - - // Update pane - updatePane: function(paneId, paneAction, paneStatus, init) { - if (yellow.config.debug) console.log("yellow.edit.updatePane id:"+paneId); - var showFields = paneStatus!="next" && paneStatus!="done"; - switch (paneId) { - case "yellow-pane-login": - if (yellow.config.editLoginRestrictions) { - yellow.toolbox.setVisible(document.getElementById("yellow-pane-login-signup"), false); - } - break; - case "yellow-pane-signup": - yellow.toolbox.setVisible(document.getElementById("yellow-pane-signup-fields"), showFields); - yellow.toolbox.setVisible(document.getElementById("yellow-pane-signup-buttons"), !showFields); - break; - case "yellow-pane-forgot": - yellow.toolbox.setVisible(document.getElementById("yellow-pane-forgot-fields"), showFields); - yellow.toolbox.setVisible(document.getElementById("yellow-pane-forgot-buttons"), !showFields); - break; - case "yellow-pane-recover": - yellow.toolbox.setVisible(document.getElementById("yellow-pane-recover-fields"), showFields); - yellow.toolbox.setVisible(document.getElementById("yellow-pane-recover-buttons"), !showFields); - break; - case "yellow-pane-settings": - yellow.toolbox.setVisible(document.getElementById("yellow-pane-settings-fields"), showFields); - yellow.toolbox.setVisible(document.getElementById("yellow-pane-settings-buttons"), !showFields); - if (paneStatus=="none") { - document.getElementById("yellow-pane-settings-status").innerHTML = "<a href=\"#\" data-action=\"version\">"+this.getText("VersionTitle")+"</a>"; - document.getElementById("yellow-pane-settings-name").value = yellow.config.userName; - document.getElementById("yellow-pane-settings-email").value = yellow.config.userEmail; - document.getElementById("yellow-pane-settings-"+yellow.config.userLanguage).checked = true; - } - break; - case "yellow-pane-version": - if (paneStatus=="none" && this.isPlugin("update")) { - document.getElementById("yellow-pane-version-status").innerHTML = this.getText("VersionStatusCheck"); - document.getElementById("yellow-pane-version-output").innerHTML = ""; - setTimeout("yellow.action('send');", 500); - } - if (paneStatus=="updates" && this.isPlugin("update")) { - document.getElementById("yellow-pane-version-status").innerHTML = "<a href=\"#\" data-action=\"update\">"+this.getText("VersionStatusUpdates")+"</a>"; - } - break; - case "yellow-pane-quit": - yellow.toolbox.setVisible(document.getElementById("yellow-pane-quit-fields"), showFields); - yellow.toolbox.setVisible(document.getElementById("yellow-pane-quit-buttons"), !showFields); - if (paneStatus=="none") { - document.getElementById("yellow-pane-quit-status").innerHTML = this.getText("QuitStatusNone"); - document.getElementById("yellow-pane-quit-name").value = ""; - } - break; - case "yellow-pane-edit": - document.getElementById("yellow-pane-edit-text").focus(); - if (init) { - yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-text"), true); - yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-preview"), false); - document.getElementById("yellow-pane-edit-toolbar-title").innerHTML = yellow.toolbox.encodeHtml(yellow.page.title); - document.getElementById("yellow-pane-edit-text").value = paneAction=="create" ? yellow.page.rawDataNew : yellow.page.rawDataEdit; - var matches = document.getElementById("yellow-pane-edit-text").value.match(/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+/); - var position = document.getElementById("yellow-pane-edit-text").value.indexOf("\n", matches ? matches[0].length : 0); - document.getElementById("yellow-pane-edit-text").setSelectionRange(position, position); - if (yellow.config.editToolbarButtons!="none") { - yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-toolbar-title"), false); - this.updateToolbar(0, "yellow-toolbar-checked"); - } - if (yellow.config.userRestrictions) { - yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-send"), false); - document.getElementById("yellow-pane-edit-text").readOnly = true; - } - } - if (!yellow.config.userRestrictions) { - var key, className; - switch (this.getAction(paneId, paneAction)) { - case "create": key = "CreateButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-create"; break; - case "edit": key = "EditButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-edit"; break; - case "delete": key = "DeleteButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-delete"; break; - } - if (document.getElementById("yellow-pane-edit-send").className != className) { - document.getElementById("yellow-pane-edit-send").innerHTML = this.getText(key); - document.getElementById("yellow-pane-edit-send").className = className; - this.resizePane(paneId, paneAction, paneStatus); - } - } - break; - } - this.bindActions(document.getElementById(paneId)); - }, - - // Resize pane - resizePane: function(paneId, paneAction, paneStatus) { - var elementBar = document.getElementById("yellow-bar-content"); - var paneLeft = yellow.toolbox.getOuterLeft(elementBar); - var paneTop = yellow.toolbox.getOuterTop(elementBar) + yellow.toolbox.getOuterHeight(elementBar) + 10; - var paneWidth = yellow.toolbox.getOuterWidth(elementBar); - 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-login": - case "yellow-pane-signup": - case "yellow-pane-forgot": - case "yellow-pane-recover": - case "yellow-pane-settings": - case "yellow-pane-version": - case "yellow-pane-quit": - yellow.toolbox.setOuterLeft(document.getElementById(paneId), paneLeft); - yellow.toolbox.setOuterTop(document.getElementById(paneId), paneTop); - yellow.toolbox.setOuterWidth(document.getElementById(paneId), paneWidth); - break; - case "yellow-pane-edit": - yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-edit"), paneLeft); - yellow.toolbox.setOuterTop(document.getElementById("yellow-pane-edit"), paneTop); - yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit"), paneHeight); - yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit"), paneWidth); - var elementWidth = yellow.toolbox.getWidth(document.getElementById("yellow-pane-edit")); - yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-text"), elementWidth); - yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-preview"), elementWidth); - var buttonsWidth = 0; - var buttonsWidthMax = yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-edit-toolbar")) - - yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-edit-toolbar-main")) - 1; - var element = document.getElementById("yellow-pane-edit-toolbar-buttons").firstChild; - for (; element; element=element.nextSibling) { - element.removeAttribute("style"); - buttonsWidth += yellow.toolbox.getOuterWidth(element); - if (buttonsWidth>buttonsWidthMax) yellow.toolbox.setVisible(element, false); - } - yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-toolbar-title"), buttonsWidthMax); - var height1 = yellow.toolbox.getHeight(document.getElementById("yellow-pane-edit")); - var height2 = yellow.toolbox.getOuterHeight(document.getElementById("yellow-pane-edit-toolbar")); - yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit-text"), height1 - height2); - yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit-preview"), height1 - height2); - var elementLink = document.getElementById("yellow-pane-"+paneAction+"-link"); - var position = yellow.toolbox.getOuterLeft(elementLink) + yellow.toolbox.getOuterWidth(elementLink)/2; - position -= yellow.toolbox.getOuterLeft(document.getElementById("yellow-pane-edit")) + 1; - yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-edit-arrow"), position); - break; - case "yellow-pane-user": - yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-user"), paneLeft + paneWidth - yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-user"))); - yellow.toolbox.setOuterTop(document.getElementById("yellow-pane-user"), paneTop); - var elementLink = document.getElementById("yellow-pane-user-link"); - var position = yellow.toolbox.getOuterLeft(elementLink) + yellow.toolbox.getOuterWidth(elementLink)/2; - position -= yellow.toolbox.getOuterLeft(document.getElementById("yellow-pane-user")); - yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-user-arrow"), position); - break; - } - }, - - // Show or hide pane - showPane: function(paneId, paneAction, paneStatus, modal) { - if (this.paneId!=paneId || this.paneAction!=paneAction) { - this.hidePane(this.paneId); - if (!document.getElementById(paneId)) this.createPane(paneId, paneAction, paneStatus); - var element = document.getElementById(paneId); - if (!yellow.toolbox.isVisible(element)) { - if (yellow.config.debug) console.log("yellow.edit.showPane id:"+paneId); - yellow.toolbox.setVisible(element, true); - if (modal) { - yellow.toolbox.addClass(document.body, "yellow-body-modal-open"); - yellow.toolbox.addValue("meta[name=viewport]", "content", ", maximum-scale=1, user-scalable=0"); - } - this.paneId = paneId; - this.paneAction = paneAction; - this.paneStatus = paneStatus; - this.updatePane(paneId, paneAction, paneStatus, this.paneActionOld!=this.paneAction); - this.resizePane(paneId, paneAction, paneStatus); - } - } else { - this.hidePane(this.paneId, true); - } - }, - - // Hide pane - hidePane: function(paneId, fadeout) { - var element = document.getElementById(paneId); - if (yellow.toolbox.isVisible(element)) { - 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); - this.paneId = 0; - this.paneActionOld = this.paneAction; - this.paneAction = 0; - this.paneStatus = 0; - } - this.hidePopup(this.popupId); - }, - - // Send pane - sendPane: function(paneId, paneAction, paneStatus, paneArgs) { - if (yellow.config.debug) console.log("yellow.edit.sendPane id:"+paneId); - var args = { "action":paneAction, "csrftoken":this.getCookie("csrftoken") }; - if (paneId=="yellow-pane-edit") { - args.action = this.getAction(paneId, paneAction); - args.rawdatasource = yellow.page.rawDataSource; - args.rawdataedit = document.getElementById("yellow-pane-edit-text").value; - args.rawdataendofline = yellow.page.rawDataEndOfLine; - } - if (paneArgs) { - var tokens = paneArgs.split("/"); - for (var i=0; i<tokens.length; i++) { - var pair = tokens[i].split(/[:=]/); - if (!pair[0] || !pair[1]) continue; - args[pair[0]] = pair[1]; - } - } - yellow.toolbox.submitForm(args); - }, - - // Process help - processHelp: function() { - this.hidePane(this.paneId); - window.open(this.getText("HelpUrl", "yellow"), "_self"); - }, - - // Process shortcut - processShortcut: function(e) { - var shortcut = yellow.toolbox.getEventShortcut(e); - if (shortcut) { - var tokens = yellow.config.editKeyboardShortcuts.split(","); - for (var i=0; i<tokens.length; i++) { - var pair = tokens[i].trim().split(" "); - if (shortcut==pair[0] || shortcut.replace("meta+", "ctrl+")==pair[0]) { - e.stopPropagation(); - e.preventDefault(); - this.processToolbar(pair[1]); - } - } - } - }, - - // Process toolbar - processToolbar: function(status, args) { - if (yellow.config.debug) console.log("yellow.edit.processToolbar status:"+status); - var elementText = document.getElementById("yellow-pane-edit-text"); - var elementPreview = document.getElementById("yellow-pane-edit-preview"); - if (!yellow.config.userRestrictions && this.paneAction!="delete" && !yellow.toolbox.isVisible(elementPreview)) { - switch (status) { - case "h1": yellow.editor.setMarkdown(elementText, "# ", "insert-multiline-block", true); break; - case "h2": yellow.editor.setMarkdown(elementText, "## ", "insert-multiline-block", true); break; - case "h3": yellow.editor.setMarkdown(elementText, "### ", "insert-multiline-block", true); break; - case "paragraph": yellow.editor.setMarkdown(elementText, "", "remove-multiline-block"); - yellow.editor.setMarkdown(elementText, "", "remove-fenced-block"); break; - case "quote": yellow.editor.setMarkdown(elementText, "> ", "insert-multiline-block", true); break; - case "pre": yellow.editor.setMarkdown(elementText, "```\n", "insert-fenced-block", true); break; - case "bold": yellow.editor.setMarkdown(elementText, "**", "insert-inline", true); break; - case "italic": yellow.editor.setMarkdown(elementText, "*", "insert-inline", true); break; - case "strikethrough": yellow.editor.setMarkdown(elementText, "~~", "insert-inline", true); break; - case "code": yellow.editor.setMarkdown(elementText, "`", "insert-autodetect", true); break; - case "ul": yellow.editor.setMarkdown(elementText, "* ", "insert-multiline-block", true); break; - case "ol": yellow.editor.setMarkdown(elementText, "1. ", "insert-multiline-block", true); break; - case "tl": yellow.editor.setMarkdown(elementText, "- [ ] ", "insert-multiline-block", true); break; - case "link": yellow.editor.setMarkdown(elementText, "[link](url)", "insert", false, yellow.editor.getMarkdownLink); break; - case "text": yellow.editor.setMarkdown(elementText, args, "insert"); break; - case "draft": yellow.editor.setMetaData(elementText, "status", "draft", true); break; - case "file": this.showFileDialog(); break; - case "undo": yellow.editor.undo(); break; - case "redo": yellow.editor.redo(); break; - } - } - if (status=="preview") this.showPreview(elementText, elementPreview); - if (status=="save" && !yellow.config.userRestrictions && this.paneAction!="delete") this.action("send"); - if (status=="help") window.open(this.getText("HelpUrl", "yellow"), "_blank"); - if (status=="markdown") window.open(this.getText("MarkdownUrl", "yellow"), "_blank"); - if (status=="format" || status=="heading" || status=="list" || status=="emojiawesome" || status=="fontawesome") { - this.showPopup("yellow-popup-"+status, status); - } else { - this.hidePopup(this.popupId); - } - }, - - // Update toolbar - updateToolbar: function(status, name) { - if (status) { - var element = document.getElementById("yellow-toolbar-"+status); - if (element) yellow.toolbox.addClass(element, name); - } else { - var elements = document.getElementsByClassName(name); - for (var i=0, l=elements.length; i<l; i++) { - yellow.toolbox.removeClass(elements[i], name); - } - } - }, - - // Create popup - createPopup: function(popupId) { - if (yellow.config.debug) console.log("yellow.edit.createPopup id:"+popupId); - var elementPopup = document.createElement("div"); - elementPopup.className = "yellow-popup"; - elementPopup.setAttribute("id", popupId); - elementPopup.style.display = "none"; - var elementDiv = document.createElement("div"); - elementDiv.setAttribute("id", popupId+"-content"); - switch (popupId) { - case "yellow-popup-format": - elementDiv.innerHTML = - "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+ - "<li><a href=\"#\" id=\"yellow-popup-format-h1\" data-action=\"toolbar\" data-status=\"h1\">"+this.getText("ToolbarH1")+"</a></li>"+ - "<li><a href=\"#\" id=\"yellow-popup-format-h2\" data-action=\"toolbar\" data-status=\"h2\">"+this.getText("ToolbarH2")+"</a></li>"+ - "<li><a href=\"#\" id=\"yellow-popup-format-h3\" data-action=\"toolbar\" data-status=\"h3\">"+this.getText("ToolbarH3")+"</a></li>"+ - "<li><a href=\"#\" id=\"yellow-popup-format-paragraph\" data-action=\"toolbar\" data-status=\"paragraph\">"+this.getText("ToolbarParagraph")+"</a></li>"+ - "<li><a href=\"#\" id=\"yellow-popup-format-pre\" data-action=\"toolbar\" data-status=\"pre\">"+this.getText("ToolbarPre")+"</a></li>"+ - "<li><a href=\"#\" id=\"yellow-popup-format-quote\" data-action=\"toolbar\" data-status=\"quote\">"+this.getText("ToolbarQuote")+"</a></li>"+ - "</ul>"; - break; - case "yellow-popup-heading": - elementDiv.innerHTML = - "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+ - "<li><a href=\"#\" id=\"yellow-popup-heading-h1\" data-action=\"toolbar\" data-status=\"h1\">"+this.getText("ToolbarH1")+"</a></li>"+ - "<li><a href=\"#\" id=\"yellow-popup-heading-h2\" data-action=\"toolbar\" data-status=\"h2\">"+this.getText("ToolbarH2")+"</a></li>"+ - "<li><a href=\"#\" id=\"yellow-popup-heading-h3\" data-action=\"toolbar\" data-status=\"h3\">"+this.getText("ToolbarH3")+"</a></li>"+ - "</ul>"; - break; - case "yellow-popup-list": - elementDiv.innerHTML = - "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+ - "<li><a href=\"#\" id=\"yellow-popup-list-ul\" data-action=\"toolbar\" data-status=\"ul\">"+this.getText("ToolbarUl")+"</a></li>"+ - "<li><a href=\"#\" id=\"yellow-popup-list-ol\" data-action=\"toolbar\" data-status=\"ol\">"+this.getText("ToolbarOl")+"</a></li>"+ - "</ul>"; - break; - case "yellow-popup-emojiawesome": - var rawDataEmojis = ""; - if (yellow.config.emojiawesomeToolbarButtons && yellow.config.emojiawesomeToolbarButtons!="none") { - var tokens = yellow.config.emojiawesomeToolbarButtons.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-args=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"ea ea-"+yellow.toolbox.encodeHtml(className)+"\"></i></a></li>"; - } - } - elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataEmojis+"</ul>"; - break; - case "yellow-popup-fontawesome": - var rawDataIcons = ""; - if (yellow.config.fontawesomeToolbarButtons && yellow.config.fontawesomeToolbarButtons!="none") { - var tokens = yellow.config.fontawesomeToolbarButtons.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-args=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"fa "+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>"; - } - } - elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataIcons+"</ul>"; - break; - } - elementPopup.appendChild(elementDiv); - yellow.toolbox.insertAfter(elementPopup, document.getElementsByTagName("body")[0].firstChild); - this.bindActions(elementPopup); - }, - - // Show or hide popup - showPopup: function(popupId, status) { - if (this.popupId!=popupId) { - this.hidePopup(this.popupId); - if (!document.getElementById(popupId)) this.createPopup(popupId); - var element = document.getElementById(popupId); - if (yellow.config.debug) console.log("yellow.edit.showPopup id:"+popupId); - yellow.toolbox.setVisible(element, true); - this.popupId = popupId; - this.updateToolbar(status, "yellow-toolbar-selected"); - var elementParent = document.getElementById("yellow-toolbar-"+status); - var popupLeft = yellow.toolbox.getOuterLeft(elementParent); - var popupTop = yellow.toolbox.getOuterTop(elementParent) + yellow.toolbox.getOuterHeight(elementParent) - 1; - yellow.toolbox.setOuterLeft(document.getElementById(popupId), popupLeft); - yellow.toolbox.setOuterTop(document.getElementById(popupId), popupTop); - } else { - this.hidePopup(this.popupId, true); - } - }, - - // Hide popup - hidePopup: function(popupId, fadeout) { - var element = document.getElementById(popupId); - if (yellow.toolbox.isVisible(element)) { - yellow.toolbox.setVisible(element, false, fadeout); - this.popupId = 0; - this.updateToolbar(0, "yellow-toolbar-selected"); - } - }, - - // Show or hide preview - showPreview: function(elementText, elementPreview) { - if (!yellow.toolbox.isVisible(elementPreview)) { - var thisObject = this; - var formData = new FormData(); - formData.append("action", "preview"); - formData.append("csrftoken", this.getCookie("csrftoken")); - formData.append("rawdataedit", elementText.value); - formData.append("rawdataendofline", yellow.page.rawDataEndOfLine); - var request = new XMLHttpRequest(); - request.open("POST", window.location.pathname, true); - request.onload = function() { if (this.status==200) thisObject.showPreviewDone.call(thisObject, elementText, elementPreview, this.responseText); }; - request.send(formData); - } else { - this.showPreviewDone(elementText, elementPreview, ""); - } - }, - - // Preview done - showPreviewDone: function(elementText, elementPreview, responseText) { - var showPreview = responseText.length!=0; - yellow.toolbox.setVisible(elementText, !showPreview); - yellow.toolbox.setVisible(elementPreview, showPreview); - if (showPreview) { - this.updateToolbar("preview", "yellow-toolbar-checked"); - elementPreview.innerHTML = responseText; - dispatchEvent(new Event("load")); - } else { - this.updateToolbar(0, "yellow-toolbar-checked"); - elementText.focus(); - } - }, - - // Show file dialog and trigger upload - showFileDialog: function() { - var element = document.createElement("input"); - element.setAttribute("id", "yellow-file-dialog"); - element.setAttribute("type", "file"); - element.setAttribute("accept", yellow.config.editUploadExtensions); - element.setAttribute("multiple", "multiple"); - yellow.toolbox.addEvent(element, "change", yellow.onDrop); - element.click(); - }, - - // Upload file - uploadFile: function(elementText, file) { - var extension = (file.name.lastIndexOf(".")!=-1 ? file.name.substring(file.name.lastIndexOf("."), file.name.length) : "").toLowerCase(); - var extensions = yellow.config.editUploadExtensions.split(/\s*,\s*/); - if (file.size<=yellow.config.serverFileSizeMax && extensions.indexOf(extension)!=-1) { - var text = this.getText("UploadProgress")+"\u200b"; - yellow.editor.setMarkdown(elementText, text, "insert"); - var thisObject = this; - var formData = new FormData(); - formData.append("action", "upload"); - formData.append("csrftoken", this.getCookie("csrftoken")); - formData.append("file", file); - var request = new XMLHttpRequest(); - request.open("POST", window.location.pathname, true); - request.onload = function() { if (this.status==200) { thisObject.uploadFileDone.call(thisObject, elementText, this.responseText); } else { thisObject.uploadFileError.call(thisObject, elementText, this.responseText); } }; - request.send(formData); - } - }, - - // Upload done - uploadFileDone: function(elementText, responseText) { - var result = JSON.parse(responseText); - if (result) { - var textOld = this.getText("UploadProgress")+"\u200b"; - var textNew; - if (result.location.substring(0, yellow.config.imageLocation.length)==yellow.config.imageLocation) { - textNew = "[image "+result.location.substring(yellow.config.imageLocation.length)+"]"; - } else { - textNew = "[link]("+result.location+")"; - } - yellow.editor.replace(elementText, textOld, textNew); - } - }, - - // Upload error - uploadFileError: function(elementText, responseText) { - var result = JSON.parse(responseText); - if (result) { - var textOld = this.getText("UploadProgress")+"\u200b"; - var textNew = "["+result.error+"]"; - yellow.editor.replace(elementText, textOld, textNew); - } - }, - - // Bind actions to links - bindActions: function(element) { - var elements = element.getElementsByTagName("a"); - for (var i=0, l=elements.length; i<l; i++) { - 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(); }; - } - }, - - // Return action - getAction: function(paneId, paneAction) { - var action = ""; - if (paneId=="yellow-pane-edit") { - switch (paneAction) { - case "create": action = "create"; break; - case "edit": action = document.getElementById("yellow-pane-edit-text").value.length!=0 ? "edit" : "delete"; break; - case "delete": action = "delete"; break; - } - if (yellow.page.statusCode==434 && paneAction!="delete") action = "create"; - } - return action; - }, - - // Return request string - getRequest: function(key, prefix) { - if (!prefix) prefix = "request"; - key = prefix + yellow.toolbox.toUpperFirst(key); - return (key in yellow.page) ? yellow.page[key] : ""; - }, - - // Return text string - 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 cookie string - getCookie: function(name) { - return yellow.toolbox.getCookie(name); - }, - - // Check if plugin exists - isPlugin: function(name) { - return name in yellow.config.serverPlugins; - } -}; - -yellow.editor = { - - // Set Markdown formatting - setMarkdown: function(element, prefix, type, toggle, callback) { - var information = this.getMarkdownInformation(element, prefix, type); - var selectionStart = (information.type.indexOf("block")!=-1) ? information.top : information.start; - var selectionEnd = (information.type.indexOf("block")!=-1) ? information.bottom : information.end; - if (information.found && toggle) information.type = information.type.replace("insert", "remove"); - if (information.type=="remove-fenced-block" || information.type=="remove-inline") { - selectionStart -= information.prefix.length; selectionEnd += information.prefix.length; - } - var text = information.text; - var textSelectionBefore = text.substring(0, selectionStart); - var textSelection = text.substring(selectionStart, selectionEnd); - var textSelectionAfter = text.substring(selectionEnd, text.length); - var textSelectionNew, selectionStartNew, selectionEndNew; - switch (information.type) { - case "insert-multiline-block": - textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information); - selectionStartNew = information.start + this.getMarkdownDifference(textSelection, textSelectionNew, true); - selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew); - if (information.start==information.top && information.start!=information.end) selectionStartNew = information.top; - if (information.end==information.top && information.start!=information.end) selectionEndNew = information.top; - break; - case "remove-multiline-block": - textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information); - selectionStartNew = information.start + this.getMarkdownDifference(textSelection, textSelectionNew, true); - selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew); - if (selectionStartNew<=information.top) selectionStartNew = information.top; - if (selectionEndNew<=information.top) selectionEndNew = information.top; - break; - case "insert-fenced-block": - textSelectionNew = this.getMarkdownFencedBlock(textSelection, information); - selectionStartNew = information.start + information.prefix.length; - selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew) - information.prefix.length; - break; - case "remove-fenced-block": - textSelectionNew = this.getMarkdownFencedBlock(textSelection, information); - selectionStartNew = information.start - information.prefix.length; - selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew) + information.prefix.length; - break; - case "insert-inline": - textSelectionNew = information.prefix + textSelection + information.prefix; - selectionStartNew = information.start + information.prefix.length; - selectionEndNew = information.end + information.prefix.length; - break; - case "remove-inline": - textSelectionNew = text.substring(information.start, information.end); - selectionStartNew = information.start - information.prefix.length; - selectionEndNew = information.end - information.prefix.length; - break; - case "insert": - textSelectionNew = callback ? callback(textSelection, information) : information.prefix; - selectionStartNew = information.start + textSelectionNew.length; - selectionEndNew = selectionStartNew; - } - if (textSelection!=textSelectionNew || selectionStart!=selectionStartNew || selectionEnd!=selectionEndNew) { - element.focus(); - element.setSelectionRange(selectionStart, selectionEnd); - document.execCommand("insertText", false, textSelectionNew); - element.value = textSelectionBefore + textSelectionNew + textSelectionAfter; - element.setSelectionRange(selectionStartNew, selectionEndNew); - } - if (yellow.config.debug) console.log("yellow.editor.setMarkdown type:"+information.type); - }, - - // Return Markdown formatting information - getMarkdownInformation: function(element, prefix, type) { - var text = element.value; - var start = element.selectionStart; - var end = element.selectionEnd; - var top = start, bottom = end; - while (text.charAt(top-1)!="\n" && top>0) top--; - if (bottom==top && bottom<text.length) bottom++; - while (text.charAt(bottom-1)!="\n" && bottom<text.length) bottom++; - if (type=="insert-autodetect") { - if (text.substring(start, end).indexOf("\n")!=-1) { - type = "insert-fenced-block"; prefix = "```\n"; - } else { - type = "insert-inline"; prefix = "`"; - } - } - var found = false; - if (type.indexOf("multiline-block")!=-1) { - if (text.substring(top, top+prefix.length)==prefix) found = true; - } else if (type.indexOf("fenced-block")!=-1) { - if (text.substring(top-prefix.length, top)==prefix && text.substring(bottom, bottom+prefix.length)==prefix) { - found = true; - } - } else { - if (text.substring(start-prefix.length, start)==prefix && text.substring(end, end+prefix.length)==prefix) { - if (prefix=="*") { - var lettersBefore = 0, lettersAfter = 0; - for (var index=start-1; text.charAt(index)=="*"; index--) lettersBefore++; - for (var index=end; text.charAt(index)=="*"; index++) lettersAfter++; - found = lettersBefore!=2 && lettersAfter!=2; - } else { - found = true; - } - } - } - return { "text":text, "prefix":prefix, "type":type, "start":start, "end":end, "top":top, "bottom":bottom, "found":found }; - }, - - // Return Markdown length difference - getMarkdownDifference: function(textSelection, textSelectionNew, firstTextLine) { - var textSelectionLength, textSelectionLengthNew; - if (firstTextLine) { - var position = textSelection.indexOf("\n"); - var positionNew = textSelectionNew.indexOf("\n"); - textSelectionLength = position!=-1 ? position+1 : textSelection.length+1; - textSelectionLengthNew = positionNew!=-1 ? positionNew+1 : textSelectionNew.length+1; - } else { - var position = textSelection.indexOf("\n"); - var positionNew = textSelectionNew.indexOf("\n"); - textSelectionLength = position!=-1 ? textSelection.length : textSelection.length+1; - textSelectionLengthNew = positionNew!=-1 ? textSelectionNew.length : textSelectionNew.length+1; - } - return textSelectionLengthNew - textSelectionLength; - }, - - // Return Markdown for multiline block - getMarkdownMultilineBlock: function(textSelection, information) { - var textSelectionNew = ""; - var lines = yellow.toolbox.getTextLines(textSelection); - for (var i=0; i<lines.length; i++) { - var matches = lines[i].match(/^(\s*[\#\*\-\>\s]+)?(\s+\[.\]|\s*\d+\.)?[ \t]+/); - if (matches) { - textSelectionNew += lines[i].substring(matches[0].length); - } else { - textSelectionNew += lines[i]; - } - } - textSelection = textSelectionNew; - if (information.type.indexOf("remove")==-1) { - textSelectionNew = ""; - var linePrefix = information.prefix; - lines = yellow.toolbox.getTextLines(textSelection.length!=0 ? textSelection : "\n"); - for (var i=0; i<lines.length; i++) { - textSelectionNew += linePrefix+lines[i]; - if (information.prefix=="1. ") { - var matches = linePrefix.match(/^(\d+)\.\s/); - if (matches) linePrefix = (parseInt(matches[1])+1)+". "; - } - } - textSelection = textSelectionNew; - } - return textSelection; - }, - - // Return Markdown for fenced block - getMarkdownFencedBlock: function(textSelection, information) { - var textSelectionNew = ""; - var lines = yellow.toolbox.getTextLines(textSelection); - for (var i=0; i<lines.length; i++) { - var matches = lines[i].match(/^```/); - if (!matches) textSelectionNew += lines[i]; - } - textSelection = textSelectionNew; - if (information.type.indexOf("remove")==-1) { - if (textSelection.length==0) textSelection = "\n"; - textSelection = information.prefix + textSelection + information.prefix; - } - return textSelection; - }, - - // Return Markdown for link - getMarkdownLink: function(textSelection, information) { - return textSelection.length!=0 ? information.prefix.replace("link", textSelection) : information.prefix; - }, - - // Set meta data - setMetaData: function(element, key, value, toggle) { - var information = this.getMetaDataInformation(element, key); - if (information.bottom!=0) { - var selectionStart = information.found ? information.start : information.bottom; - var selectionEnd = information.found ? information.end : information.bottom; - var text = information.text; - var textSelectionBefore = text.substring(0, selectionStart); - var textSelection = text.substring(selectionStart, selectionEnd); - var textSelectionAfter = text.substring(selectionEnd, text.length); - var textSelectionNew = yellow.toolbox.toUpperFirst(key)+": "+value+"\n"; - if (information.found && information.value==value && toggle) textSelectionNew = ""; - var selectionStartNew = selectionStart; - var selectionEndNew = selectionStart + textSelectionNew.trim().length; - element.focus(); - element.setSelectionRange(selectionStart, selectionEnd); - document.execCommand("insertText", false, textSelectionNew); - element.value = textSelectionBefore + textSelectionNew + textSelectionAfter; - element.setSelectionRange(selectionStartNew, selectionEndNew); - element.scrollTop = 0; - if (yellow.config.debug) console.log("yellow.editor.setMetaData key:"+key); - } - }, - - // Return meta data information - getMetaDataInformation: function(element, key) { - var text = element.value; - var value = ""; - var start = 0, end = 0, top = 0, bottom = 0; - var found = false; - var parts = text.match(/^(\xEF\xBB\xBF)?(\-\-\-[\r\n]+)([\s\S]+?)\-\-\-[\r\n]+/); - if (parts) { - key = yellow.toolbox.toLowerFirst(key); - start = end = top = ((parts[1] ? parts[1] : "")+parts[2]).length; - bottom = ((parts[1] ? parts[1] : "")+parts[2]+parts[3]).length; - var lines = yellow.toolbox.getTextLines(parts[3]); - for (var i=0; i<lines.length; i++) { - var matches = lines[i].match(/^\s*(.*?)\s*:\s*(.*?)\s*$/); - if (matches && yellow.toolbox.toLowerFirst(matches[1])==key && matches[2].length!=0) { - value = matches[2]; - end = start + lines[i].length; - found = true; - break; - } - start = end = start + lines[i].length; - } - } - return { "text":text, "value":value, "start":start, "end":end, "top":top, "bottom":bottom, "found":found }; - }, - - // Replace text - replace: function(element, textOld, textNew) { - var text = element.value; - var selectionStart = element.selectionStart; - var selectionEnd = element.selectionEnd; - var selectionStartFound = text.indexOf(textOld); - var selectionEndFound = selectionStartFound + textOld.length; - if (selectionStartFound!=-1) { - var selectionStartNew = selectionStart<selectionStartFound ? selectionStart : selectionStart+textNew.length-textOld.length; - var selectionEndNew = selectionEnd<selectionEndFound ? selectionEnd : selectionEnd+textNew.length-textOld.length; - var textBefore = text.substring(0, selectionStartFound); - var textAfter = text.substring(selectionEndFound, text.length); - if (textOld!=textNew) { - element.focus(); - element.setSelectionRange(selectionStartFound, selectionEndFound); - document.execCommand("insertText", false, textNew); - element.value = textBefore + textNew + textAfter; - element.setSelectionRange(selectionStartNew, selectionEndNew); - } - } - }, - - // Undo changes - undo: function() { - document.execCommand("undo"); - }, - - // Redo changes - redo: function() { - document.execCommand("redo"); - } -}; - -yellow.toolbox = { - - // Insert element before reference element - insertBefore: function(element, elementReference) { - elementReference.parentNode.insertBefore(element, elementReference); - }, - - // Insert element after reference element - insertAfter: function(element, elementReference) { - elementReference.parentNode.insertBefore(element, elementReference.nextSibling); - }, - - // Add element class - addClass: function(element, name) { - element.classList.add(name); - }, - - // Remove element class - removeClass: function(element, name) { - element.classList.remove(name); - }, - - // Add attribute information - addValue: function(selector, name, value) { - var element = document.querySelector(selector); - element.setAttribute(name, element.getAttribute(name) + value); - }, - - // Remove attribute information - removeValue: function(selector, name, value) { - var element = document.querySelector(selector); - element.setAttribute(name, element.getAttribute(name).replace(value, "")); - }, - - // Add event handler - addEvent: function(element, type, handler) { - element.addEventListener(type, handler, false); - }, - - // Remove event handler - removeEvent: function(element, type, handler) { - element.removeEventListener(type, handler, false); - }, - - // Return shortcut from keyboard event, alphanumeric only - getEventShortcut: function(e) { - var shortcut = ""; - if (e.keyCode>=48 && e.keyCode<=90) { - shortcut += (e.ctrlKey ? "ctrl+" : "")+(e.metaKey ? "meta+" : "")+(e.altKey ? "alt+" : "")+(e.shiftKey ? "shift+" : ""); - shortcut += String.fromCharCode(e.keyCode).toLowerCase(); - } - return shortcut; - }, - - // Return element width in pixel - getWidth: function(element) { - return element.offsetWidth - this.getBoxSize(element).width; - }, - - // Return element height in pixel - getHeight: function(element) { - return element.offsetHeight - this.getBoxSize(element).height; - }, - - // Set element width in pixel, including padding and border - setOuterWidth: function(element, width) { - element.style.width = Math.max(0, width - this.getBoxSize(element).width) + "px"; - }, - - // Set element height in pixel, including padding and border - setOuterHeight: function(element, height) { - element.style.height = Math.max(0, height - this.getBoxSize(element).height) + "px"; - }, - - // Return element width in pixel, including padding and border - getOuterWidth: function(element, includeMargin) { - var width = element.offsetWidth; - if (includeMargin) width += this.getMarginSize(element).width; - return width; - }, - - // Return element height in pixel, including padding and border - getOuterHeight: function(element, includeMargin) { - var height = element.offsetHeight; - if (includeMargin) height += this.getMarginSize(element).height; - return height; - }, - - // Set element left position in pixel - setOuterLeft: function(element, left) { - element.style.left = Math.max(0, left) + "px"; - }, - - // Set element top position in pixel - setOuterTop: function(element, top) { - element.style.top = Math.max(0, top) + "px"; - }, - - // Return element left position in pixel - getOuterLeft: function(element) { - return element.getBoundingClientRect().left + window.pageXOffset; - }, - - // Return element top position in pixel - getOuterTop: function(element) { - return element.getBoundingClientRect().top + window.pageYOffset; - }, - - // Return window width in pixel - getWindowWidth: function() { - return window.innerWidth; - }, - - // Return window height in pixel - getWindowHeight: function() { - return window.innerHeight; - }, - - // Return element CSS property - getStyle: function(element, property) { - return window.getComputedStyle(element).getPropertyValue(property); - }, - - // Return element CSS padding and border - getBoxSize: function(element) { - var paddingLeft = parseFloat(this.getStyle(element, "padding-left")) || 0; - var paddingRight = parseFloat(this.getStyle(element, "padding-right")) || 0; - var borderLeft = parseFloat(this.getStyle(element, "border-left-width")) || 0; - var borderRight = parseFloat(this.getStyle(element, "border-right-width")) || 0; - var width = paddingLeft + paddingRight + borderLeft + borderRight; - var paddingTop = parseFloat(this.getStyle(element, "padding-top")) || 0; - var paddingBottom = parseFloat(this.getStyle(element, "padding-bottom")) || 0; - var borderTop = parseFloat(this.getStyle(element, "border-top-width")) || 0; - var borderBottom = parseFloat(this.getStyle(element, "border-bottom-width")) || 0; - var height = paddingTop + paddingBottom + borderTop + borderBottom; - return { "width":width, "height":height }; - }, - - // Return element CSS margin - getMarginSize: function(element) { - var marginLeft = parseFloat(this.getStyle(element, "margin-left")) || 0; - var marginRight = parseFloat(this.getStyle(element, "margin-right")) || 0; - var width = marginLeft + marginRight; - var marginTop = parseFloat(this.getStyle(element, "margin-top")) || 0; - var marginBottom = parseFloat(this.getStyle(element, "margin-bottom")) || 0; - var height = marginTop + marginBottom; - return { "width":width, "height":height }; - }, - - // Set element visibility - setVisible: function(element, show, fadeout) { - if (fadeout && !show) { - var opacity = 1; - function renderFrame() { - opacity -= .1; - if (opacity<=0) { - element.style.opacity = "initial"; - element.style.display = "none"; - } else { - element.style.opacity = opacity; - requestAnimationFrame(renderFrame); - } - } - renderFrame(); - } else { - element.style.display = show ? "block" : "none"; - } - }, - - // Check if element exists and is visible - isVisible: function(element) { - return element && element.style.display!="none"; - }, - - // Convert first letter to lowercase - toLowerFirst: function(string) { - return string.charAt(0).toLowerCase()+string.slice(1); - }, - - // Convert first letter to uppercase - toUpperFirst: function(string) { - return string.charAt(0).toUpperCase()+string.slice(1); - }, - - // Return lines from text string, including newline - getTextLines: function(string) { - var lines = string.split("\n"); - for (var i=0; i<lines.length; i++) lines[i] = lines[i]+"\n"; - if (string.length==0 || string.charAt(string.length-1)=="\n") lines.pop(); - return lines; - }, - - // Return cookie string - getCookie: function(name) { - var matches = document.cookie.match("(^|; )"+name+"=([^;]+)"); - return matches ? unescape(matches[2]) : ""; - }, - - // Encode HTML special characters - encodeHtml: function(string) { - return string - .replace(/&/g, "&amp;") - .replace(/</g, "&lt;") - .replace(/>/g, "&gt;") - .replace(/"/g, "&quot;"); - }, - - // Submit form with post method - submitForm: function(args) { - var elementForm = document.createElement("form"); - elementForm.setAttribute("method", "post"); - for (var key in args) { - if (!args.hasOwnProperty(key)) continue; - var elementInput = document.createElement("input"); - elementInput.setAttribute("type", "hidden"); - elementInput.setAttribute("name", key); - elementInput.setAttribute("value", args[key]); - elementForm.appendChild(elementInput); - } - document.body.appendChild(elementForm); - elementForm.submit(); - } -}; - -yellow.edit.intervalId = setInterval("yellow.onLoad()", 1); diff --git a/system/plugins/edit.php b/system/plugins/edit.php @@ -1,1862 +0,0 @@ -<?php -// Edit plugin, https://github.com/datenstrom/yellow-plugins/tree/master/edit -// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowEdit { - const VERSION = "0.8.1"; - public $yellow; //access to API - public $response; //web response - public $users; //user accounts - public $merge; //text merge - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - $this->response = new YellowResponse($yellow); - $this->users = new YellowUsers($yellow); - $this->merge = new YellowMerge($yellow); - $this->yellow->config->setDefault("editLocation", "/edit/"); - $this->yellow->config->setDefault("editUploadNewLocation", "/media/@group/@filename"); - $this->yellow->config->setDefault("editUploadExtensions", ".gif, .jpg, .pdf, .png, .svg, .tgz, .zip"); - $this->yellow->config->setDefault("editKeyboardShortcuts", "ctrl+b bold, ctrl+i italic, ctrl+e code, ctrl+k link, ctrl+s save, ctrl+shift+p preview"); - $this->yellow->config->setDefault("editToolbarButtons", "auto"); - $this->yellow->config->setDefault("editEndOfLine", "auto"); - $this->yellow->config->setDefault("editUserFile", "user.ini"); - $this->yellow->config->setDefault("editUserPasswordMinLength", "8"); - $this->yellow->config->setDefault("editUserHashAlgorithm", "bcrypt"); - $this->yellow->config->setDefault("editUserHashCost", "10"); - $this->yellow->config->setDefault("editUserHome", "/"); - $this->yellow->config->setDefault("editLoginRestrictions", "0"); - $this->yellow->config->setDefault("editLoginSessionTimeout", "2592000"); - $this->yellow->config->setDefault("editBruteForceProtection", "25"); - $this->users->load($this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile")); - } - - // Handle startup - public function onStartup($update) { - if ($update) { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $fileData = $this->yellow->toolbox->readFile($fileNameUser); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2]) && $matches[1][0]!="#") { - list($hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) = explode(",", $matches[2]); - if ($errors=="none") { $home=$pending; $pending=$errors; $errors=$modified; $modified=$stamp; $stamp=""; } //TODO: remove later - if (strlenb($stamp)!=20) $stamp=$this->users->createStamp(); //TODO: remove later, converts old file format - if ($status!="active" && $status!="inactive") { - unset($this->users->users[$matches[1]]); - continue; - } - $pending = "none"; - $this->users->set($matches[1], $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home); - $fileDataNew .= "$matches[1]: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n"; - } else { - $fileDataNew .= $line; - } - } - if ($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileNameUser, $fileDataNew); - } - } - - // Handle request - public function onRequest($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->checkRequest($location)) { - $scheme = $this->yellow->config->get("serverScheme"); - $address = $this->yellow->config->get("serverAddress"); - $base = rtrim($this->yellow->config->get("serverBase").$this->yellow->config->get("editLocation"), "/"); - list($scheme, $address, $base, $location, $fileName) = $this->yellow->getRequestInformation($scheme, $address, $base); - $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName); - } - return $statusCode; - } - - // Handle page meta data - public function onParseMeta($page) { - if ($page==$this->yellow->page && $this->response->isActive()) { - if ($this->response->isUser()) { - if (empty($this->response->rawDataSource)) $this->response->rawDataSource = $page->rawData; - if (empty($this->response->rawDataEdit)) $this->response->rawDataEdit = $page->rawData; - if (empty($this->response->rawDataEndOfLine)) $this->response->rawDataEndOfLine = $this->response->getEndOfLine($page->rawData); - if ($page->statusCode==434) $this->response->rawDataEdit = $this->response->getRawDataNew($page, true); - } - if (empty($this->response->language)) $this->response->language = $page->get("language"); - if (empty($this->response->action)) $this->response->action = $this->response->isUser() ? "none" : "login"; - if (empty($this->response->status)) $this->response->status = "none"; - if ($this->response->status=="error") $this->response->action = "error"; - } - } - - // 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>"; - } - return $output; - } - - // Handle page extra data - public function onParsePageExtra($page, $name) { - $output = null; - if ($name=="header" && $this->response->isActive()) { - $pluginLocation = $this->yellow->config->get("serverBase").$this->yellow->config->get("pluginLocation"); - $output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" data-bundle=\"none\" href=\"{$pluginLocation}edit.css\" />\n"; - $output .= "<script type=\"text/javascript\" data-bundle=\"none\" src=\"{$pluginLocation}edit.js\"></script>\n"; - $output .= "<script type=\"text/javascript\">\n"; - $output .= "// <![CDATA[\n"; - $output .= "yellow.page = ".json_encode($this->response->getPageData($page)).";\n"; - $output .= "yellow.config = ".json_encode($this->response->getConfigData()).";\n"; - $output .= "yellow.text = ".json_encode($this->response->getTextData()).";\n"; - $output .= "// ]]>\n"; - $output .= "</script>\n"; - } - return $output; - } - - // Handle command - public function onCommand($args) { - list($command) = $args; - switch ($command) { - case "user": $statusCode = $this->processCommandUser($args); break; - default: $statusCode = 0; - } - return $statusCode; - } - - // Handle command help - public function onCommandHelp() { - return "user [option email password name]\n"; - } - - // Process command to update user account - public function processCommandUser($args) { - list($command, $option) = $args; - switch ($option) { - case "": $statusCode = $this->userShow($args); break; - case "add": $statusCode = $this->userAdd($args); break; - case "change": $statusCode = $this->userChange($args); break; - case "remove": $statusCode = $this->userRemove($args); break; - default: $statusCode = 400; echo "Yellow $command: Invalid arguments\n"; - } - return $statusCode; - } - - // Show user accounts - public function userShow($args) { - list($command) = $args; - foreach ($this->users->getData() as $line) { - echo "$line\n"; - } - if (!$this->users->getNumber()) echo "Yellow $command: No user accounts\n"; - return 200; - } - - // Add user account - public function userAdd($args) { - $status = "ok"; - list($command, $option, $email, $password, $name) = $args; - if (empty($email) || empty($password)) $status = $this->response->status = "incomplete"; - if ($status=="ok") $status = $this->getUserAccount($email, $password, "add"); - if ($status=="ok" && $this->users->isTaken($email)) $status = "taken"; - switch ($status) { - case "incomplete": echo "ERROR updating configuration: Please enter email and password!\n"; break; - case "invalid": echo "ERROR updating configuration: Please enter a valid email!\n"; break; - case "taken": echo "ERROR updating configuration: Please enter a different email!\n"; break; - case "weak": echo "ERROR updating configuration: Please enter a different password!\n"; break; - } - if ($status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $status = $this->users->save($fileNameUser, $email, $password, $name, "", "active") ? "ok" : "error"; - if ($status=="error") echo "ERROR updating configuration: Can't write file '$fileNameUser'!\n"; - } - if ($status=="ok") { - $algorithm = $this->yellow->config->get("editUserHashAlgorithm"); - $status = substru($this->users->getHash($email), 0, 10)!="error-hash" ? "ok" : "error"; - if ($status=="error") echo "ERROR updating configuration: Hash algorithm '$algorithm' not supported!\n"; - } - $statusCode = $status=="ok" ? 200 : 500; - echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."added\n"; - return $statusCode; - } - - // Change user account - public function userChange($args) { - $status = "ok"; - list($command, $option, $email, $password, $name) = $args; - 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"; - switch ($status) { - case "invalid": echo "ERROR updating configuration: Please enter a valid email!\n"; break; - case "unknown": echo "ERROR updating configuration: Can't find email '$email'!\n"; break; - case "weak": echo "ERROR updating configuration: Please enter a different password!\n"; break; - } - if ($status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $status = $this->users->save($fileNameUser, $email, $password, $name) ? "ok" : "error"; - if ($status=="error") echo "ERROR updating configuration: Can't write file '$fileNameUser'!\n"; - } - $statusCode = $status=="ok" ? 200 : 500; - echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."changed\n"; - return $statusCode; - } - - // Remove user account - public function userRemove($args) { - $status = "ok"; - list($command, $option, $email) = $args; - if (empty($email)) $status = $this->response->status = "invalid"; - if ($status=="ok") $status = $this->getUserAccount($email, "", "remove"); - if ($status=="ok" && !$this->users->isExisting($email)) $status = "unknown"; - switch ($status) { - case "invalid": echo "ERROR updating configuration: Please enter a valid email!\n"; break; - case "unknown": echo "ERROR updating configuration: Can't find email '$email'!\n"; break; - } - if ($status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $status = $this->users->remove($fileNameUser, $email) ? "ok" : "error"; - if ($status=="error") echo "ERROR updating configuration: Can't write file '$fileNameUser'!\n"; - } - $statusCode = $status=="ok" ? 200 : 500; - echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."removed\n"; - return $statusCode; - } - - // Process request - public function processRequest($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->checkUserAuth($scheme, $address, $base, $location, $fileName)) { - switch ($_REQUEST["action"]) { - case "": $statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break; - case "login": $statusCode = $this->processRequestLogin($scheme, $address, $base, $location, $fileName); break; - case "logout": $statusCode = $this->processRequestLogout($scheme, $address, $base, $location, $fileName); break; - case "settings": $statusCode = $this->processRequestSettings($scheme, $address, $base, $location, $fileName); break; - case "version": $statusCode = $this->processRequestVersion($scheme, $address, $base, $location, $fileName); break; - case "update": $statusCode = $this->processRequestUpdate($scheme, $address, $base, $location, $fileName); break; - case "quit": $statusCode = $this->processRequestQuit($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 "preview": $statusCode = $this->processRequestPreview($scheme, $address, $base, $location, $fileName); break; - case "upload": $statusCode = $this->processRequestUpload($scheme, $address, $base, $location, $fileName); break; - } - } elseif ($this->checkUserUnauth($scheme, $address, $base, $location, $fileName)) { - $this->yellow->lookup->requestHandler = "core"; - switch ($_REQUEST["action"]) { - case "": $statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break; - case "signup": $statusCode = $this->processRequestSignup($scheme, $address, $base, $location, $fileName); break; - case "forgot": $statusCode = $this->processRequestForgot($scheme, $address, $base, $location, $fileName); break; - case "confirm": $statusCode = $this->processRequestConfirm($scheme, $address, $base, $location, $fileName); break; - case "approve": $statusCode = $this->processRequestApprove($scheme, $address, $base, $location, $fileName); break; - case "recover": $statusCode = $this->processRequestRecover($scheme, $address, $base, $location, $fileName); break; - case "reactivate": $statusCode = $this->processRequestReactivate($scheme, $address, $base, $location, $fileName); break; - case "verify": $statusCode = $this->processRequestVerify($scheme, $address, $base, $location, $fileName); break; - case "change": $statusCode = $this->processRequestChange($scheme, $address, $base, $location, $fileName); break; - case "remove": $statusCode = $this->processRequestRemove($scheme, $address, $base, $location, $fileName); break; - } - } - if ($statusCode==0) $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - $this->checkUserFailed($scheme, $address, $base, $location, $fileName); - return $statusCode; - } - - // Process request to show file - public function processRequestShow($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if (is_readable($fileName)) { - $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->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(301, $location); - } else { - $this->yellow->page->error($this->response->isUserRestrictions() ? 404 : 434); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - } - return $statusCode; - } - - // Process request for user login - public function processRequestLogin($scheme, $address, $base, $location, $fileName) { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - if ($this->users->save($fileNameUser, $this->response->userEmail)) { - $home = $this->users->getHome($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); - } else { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $home); - $statusCode = $this->yellow->sendStatus(302, $location); - } - } else { - $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - return $statusCode; - } - - // Process request for user logout - public function processRequestLogout($scheme, $address, $base, $location, $fileName) { - $this->response->userEmail = ""; - $this->response->destroyCookies($scheme, $address, $base); - $location = $this->yellow->lookup->normaliseUrl( - $this->yellow->config->get("serverScheme"), - $this->yellow->config->get("serverAddress"), - $this->yellow->config->get("serverBase"), - $location); - $statusCode = $this->yellow->sendStatus(302, $location); - return $statusCode; - } - - // Process request for user signup - public function processRequestSignup($scheme, $address, $base, $location, $fileName) { - $this->response->action = "signup"; - $this->response->status = "ok"; - $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"])); - $email = trim($_REQUEST["email"]); - $password = trim($_REQUEST["password"]); - $consent = trim($_REQUEST["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 ($this->response->status=="ok" && $this->response->isLoginRestrictions()) $this->response->status = "next"; - if ($this->response->status=="ok" && $this->users->isTaken($email)) $this->response->status = "next"; - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $email, $password, $name, "", "unconfirmed") ? "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->config->get("editUserHashAlgorithm"); - $this->response->status = substru($this->users->getHash($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") { - $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "confirm") ? "next" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - return $statusCode; - } - - // Process request to confirm user signup - public function processRequestConfirm($scheme, $address, $base, $location, $fileName) { - $this->response->action = "confirm"; - $this->response->status = "ok"; - $email = $_REQUEST["email"]; - $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "unapproved") ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "approve") ? "done" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - return $statusCode; - } - - // Process request to approve user signup - public function processRequestApprove($scheme, $address, $base, $location, $fileName) { - $this->response->action = "approve"; - $this->response->status = "ok"; - $email = $_REQUEST["email"]; - $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "welcome") ? "done" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - return $statusCode; - } - - // Process request for forgotten password - public function processRequestForgot($scheme, $address, $base, $location, $fileName) { - $this->response->action = "forgot"; - $this->response->status = "ok"; - $email = trim($_REQUEST["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->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!"); - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - return $statusCode; - } - - // Process request to recover password - public function processRequestRecover($scheme, $address, $base, $location, $fileName) { - $this->response->action = "recover"; - $this->response->status = "ok"; - $email = trim($_REQUEST["email"]); - $password = trim($_REQUEST["password"]); - $this->response->status = $this->getUserStatus($email, $_REQUEST["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 ($this->response->status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $email, $password) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $this->response->destroyCookies($scheme, $address, $base); - $this->response->status = "done"; - } - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - return $statusCode; - } - - // Process request to reactivate account - public function processRequestReactivate($scheme, $address, $base, $location, $fileName) { - $this->response->action = "reactivate"; - $this->response->status = "ok"; - $email = $_REQUEST["email"]; - $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "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); - return $statusCode; - } - - // Process request to change settings - public function processRequestSettings($scheme, $address, $base, $location, $fileName) { - $this->response->action = "settings"; - $this->response->status = "ok"; - $email = trim($_REQUEST["email"]); - $emailSource = $this->response->userEmail; - $password = trim($_REQUEST["password"]); - $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"])); - $language = trim($_REQUEST["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 ($this->response->status=="ok" && $email!=$emailSource) { - $pending = $emailSource; - $home = $this->users->getHome($emailSource); - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $email, "no", $name, $language, "unverified", "", "", "", $pending, $home) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $pending = $email.":".(empty($password) ? $this->users->getHash($emailSource) : $this->users->createHash($password)); - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $emailSource, "", $name, $language, "", "", "", "", $pending) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $action = $email!=$emailSource ? "verify" : "change"; - $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, $action) ? "next" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); - } - } else { - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $email, "", $name, $language) ? "done" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - } - if ($this->response->status=="done") { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - return $statusCode; - } - - // Process request to verify email - public function processRequestVerify($scheme, $address, $base, $location, $fileName) { - $this->response->action = "verify"; - $this->response->status = "ok"; - $email = $emailSource = $_REQUEST["email"]; - $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); - if ($this->response->status=="ok") { - $emailSource = $this->users->getPending($email); - if ($this->users->getStatus($emailSource)!="active") $this->response->status = "done"; - } - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "unchanged") ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $this->response->status = $this->response->sendMail($scheme, $address, $base, $emailSource, "change") ? "done" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - return $statusCode; - } - - // Process request to change email or password - public function processRequestChange($scheme, $address, $base, $location, $fileName) { - $this->response->action = "change"; - $this->response->status = "ok"; - $email = $emailSource = trim($_REQUEST["email"]); - $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); - if ($this->response->status=="ok") { - list($email, $hash) = explode(":", $this->users->getPending($email), 2); - if (!$this->users->isExisting($email) || empty($hash)) $this->response->status = "done"; - } - if ($this->response->status=="ok") { - $this->users->users[$email]["hash"] = $hash; - $this->users->users[$email]["pending"] = "none"; - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "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->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->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") { - $this->response->destroyCookies($scheme, $address, $base); - $this->response->status = "done"; - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - return $statusCode; - } - - // Process request to show software version - public function processRequestVersion($scheme, $address, $base, $location, $fileName) { - $this->response->action = "version"; - $this->response->status = "ok"; - if ($this->yellow->plugins->isExisting("update")) { - list($statusCodeCurrent, $dataCurrent) = $this->yellow->plugins->get("update")->getSoftwareVersion(); - list($statusCodeLatest, $dataLatest) = $this->yellow->plugins->get("update")->getSoftwareVersion(true); - list($statusCodeModified, $dataModified) = $this->yellow->plugins->get("update")->getSoftwareModified(); - $statusCode = max($statusCodeCurrent, $statusCodeLatest, $statusCodeModified); - if ($this->response->isUserWebmaster()) { - foreach ($dataCurrent as $key=>$value) { - if (strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0) { - ++$updates; - $rawData = htmlspecialchars("$key $dataLatest[$key]")."<br />\n"; - $this->response->rawDataOutput .= $rawData; - } - } - if ($updates==0) { - foreach ($dataCurrent as $key=>$value) { - if (!is_null($dataModified[$key]) && !is_null($dataLatest[$key])) { - $rawData = $this->yellow->text->getTextHtml("editVersionUpdateModified", $this->response->language)." - <a href=\"#\" data-action=\"update\" data-status=\"update\" data-args=\"".$this->yellow->toolbox->normaliseArgs("feature:$key/option:force")."\">".$this->yellow->text->getTextHtml("editVersionUpdateForce", $this->response->language)."</a><br />\n"; - $rawData = preg_replace("/@software/i", htmlspecialchars("$key $dataLatest[$key]"), $rawData); - $this->response->rawDataOutput .= $rawData; - } - } - } - $this->response->status = $updates ? "updates" : "done"; - } else { - foreach ($dataCurrent as $key=>$value) { - if (strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0) ++$updates; - } - $this->response->status = $updates ? "warning" : "done"; - } - if ($statusCode!=200) $this->response->status = "error"; - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - return $statusCode; - } - - // Process request to update website - public function processRequestUpdate($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->yellow->plugins->isExisting("update") && $this->response->isUserWebmaster()) { - $feature = trim($_REQUEST["feature"]); - $option = trim($_REQUEST["option"]); - $statusCode = $this->yellow->command("update", $feature, $option); - if ($statusCode==200) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } - } - return $statusCode; - } - - // Process request to quit account - public function processRequestQuit($scheme, $address, $base, $location, $fileName) { - $this->response->action = "quit"; - $this->response->status = "ok"; - $name = trim($_REQUEST["name"]); - $email = $this->response->userEmail; - if (empty($name)) $this->response->status = "none"; - if ($this->response->status=="ok" && $name!=$this->users->getName($email)) $this->response->status = "mismatch"; - if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, "", $this->response->action); - 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!"); - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - return $statusCode; - } - - // Process request to remove account - public function processRequestRemove($scheme, $address, $base, $location, $fileName) { - $this->response->action = "remove"; - $this->response->status = "ok"; - $email = $_REQUEST["email"]; - $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]); - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "removed") ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - 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->config->get("configDir").$this->yellow->config->get("editUserFile"); - $this->response->status = $this->users->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") { - $this->response->destroyCookies($scheme, $address, $base); - $this->response->status = "done"; - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - return $statusCode; - } - - // Process request to create page - public function processRequestCreate($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if (!$this->response->isUserRestrictions() && !empty($_REQUEST["rawdataedit"])) { - $this->response->rawDataSource = $_REQUEST["rawdatasource"]; - $this->response->rawDataEdit = $_REQUEST["rawdatasource"]; - $this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"]; - $rawData = $_REQUEST["rawdataedit"]; - $page = $this->response->getPageNew($scheme, $address, $base, $location, $fileName, - $rawData, $this->response->getEndOfLine()); - if (!$page->isError()) { - if ($this->yellow->toolbox->createFile($page->fileName, $page->rawData, true)) { - $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")); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - } - return $statusCode; - } - - // Process request to edit page - public function processRequestEdit($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if (!$this->response->isUserRestrictions() && !empty($_REQUEST["rawdataedit"])) { - $this->response->rawDataSource = $_REQUEST["rawdatasource"]; - $this->response->rawDataEdit = $_REQUEST["rawdataedit"]; - $this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"]; - $rawDataFile = $this->yellow->toolbox->readFile($fileName); - $page = $this->response->getPageEdit($scheme, $address, $base, $location, $fileName, - $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); - } - } 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); - } - } - } else { - $this->yellow->page->error(500, $page->get("pageError")); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - } - return $statusCode; - } - - // Process request to delete page - public function processRequestDelete($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if (!$this->response->isUserRestrictions() && is_file($fileName)) { - $this->response->rawDataSource = $_REQUEST["rawdatasource"]; - $this->response->rawDataEdit = $_REQUEST["rawdatasource"]; - $this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"]; - $rawDataFile = $this->yellow->toolbox->readFile($fileName); - $page = $this->response->getPageDelete($scheme, $address, $base, $location, $fileName, - $rawDataFile, $this->response->rawDataEndOfLine); - if (!$page->isError()) { - if ($this->yellow->lookup->isFileLocation($location)) { - if ($this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir"))) { - $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 { - if ($this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->config->get("trashDir"))) { - $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")); - $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, - $_REQUEST["rawdataedit"], $_REQUEST["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"; - } - return $statusCode; - } - - // Process request to upload file - public function processRequestUpload($scheme, $address, $base, $location, $fileName) { - $data = array(); - $fileNameTemp = $_FILES["file"]["tmp_name"]; - $fileNameShort = preg_replace("/[^\pL\d\-\.]/u", "-", basename($_FILES["file"]["name"])); - $fileSizeMax = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize")); - $extension = strtoloweru(($pos = strrposu($fileNameShort, ".")) ? substru($fileNameShort, $pos) : ""); - $extensions = preg_split("/\s*,\s*/", $this->yellow->config->get("editUploadExtensions")); - if (!$this->response->isUserRestrictions() && is_uploaded_file($fileNameTemp) && - filesize($fileNameTemp)<=$fileSizeMax && in_array($extension, $extensions)) { - $file = $this->response->getFileUpload($scheme, $address, $base, $location, $fileNameTemp, $fileNameShort); - if (!$file->isError() && $this->yellow->toolbox->copyFile($fileNameTemp, $file->fileName, true)) { - $data["location"] = $file->getLocation(); - } else { - $data["error"] = "Can't write file '$file->fileName'!"; - } - } else { - $data["error"] = "Can't write file '$fileNameShort'!"; - } - $statusCode = $this->yellow->sendData(is_null($data["error"]) ? 200 : 500, json_encode($data), "a.json", false); - return $statusCode; - } - - // Check request - public function checkRequest($location) { - $locationLength = strlenu($this->yellow->config->get("editLocation")); - $this->response->active = substru($location, 0, $locationLength)==$this->yellow->config->get("editLocation"); - return $this->response->isActive(); - } - - // Check user authentication - public function checkUserAuth($scheme, $address, $base, $location, $fileName) { - if ($this->isRequestSameSite("POST", $scheme, $address) || $_REQUEST["action"]=="") { - if ($_REQUEST["action"]=="login") { - $email = $_REQUEST["email"]; - $password = $_REQUEST["password"]; - if ($this->users->checkAuthLogin($email, $password)) { - $this->response->createCookies($scheme, $address, $base, $email); - $this->response->userEmail = $email; - $this->response->userRestrictions = $this->getUserRestrictions($email, $location, $fileName); - $this->response->language = $this->getUserLanguage($email); - } else { - $this->response->userFailedError = "login"; - $this->response->userFailedEmail = $email; - $this->response->userFailedExpire = PHP_INT_MAX; - } - } elseif (isset($_COOKIE["authtoken"]) && isset($_COOKIE["csrftoken"])) { - if ($this->users->checkAuthToken($_COOKIE["authtoken"], $_COOKIE["csrftoken"], $_POST["csrftoken"], $_REQUEST["action"]=="")) { - $this->response->userEmail = $email = $this->users->getAuthEmail($_COOKIE["authtoken"]); - $this->response->userRestrictions = $this->getUserRestrictions($email, $location, $fileName); - $this->response->language = $this->getUserLanguage($email); - } else { - $this->response->userFailedError = "auth"; - $this->response->userFailedEmail = $this->users->getAuthEmail($_COOKIE["authtoken"]); - $this->response->userFailedExpire = $this->users->getAuthExpire($_COOKIE["authtoken"]); - } - } - } - return $this->response->isUser(); - } - - // Check user without authentication - public function checkUserUnauth($scheme, $address, $base, $location, $fileName) { - $ok = false; - if ($_REQUEST["action"]=="" || $_REQUEST["action"]=="signup" || $_REQUEST["action"]=="forgot") { - $ok = true; - } elseif (isset($_REQUEST["actiontoken"])) { - if ($this->users->checkActionToken($_REQUEST["actiontoken"], $_REQUEST["email"], $_REQUEST["action"], $_REQUEST["expire"])) { - $ok = true; - $this->response->language = $this->getUserLanguage($_REQUEST["email"]); - } else { - $this->response->userFailedError = "action"; - $this->response->userFailedEmail = $_REQUEST["email"]; - $this->response->userFailedExpire = $_REQUEST["expire"]; - } - } - return $ok; - } - - // 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)) { - $email = $this->response->userFailedEmail; - $modified = $this->users->getModified($email); - $errors = $this->users->getErrors($email)+1; - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $status = $this->users->save($fileNameUser, $email, "", "", "", "", "", $modified, $errors) ? "ok" : "error"; - if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - if ($errors==$this->yellow->config->get("editBruteForceProtection")) { - $statusBeforeProtection = $this->users->getStatus($email); - $statusAfterProtection = ($statusBeforeProtection=="active" || $statusBeforeProtection=="inactive") ? "inactive" : "failed"; - if ($status=="ok") { - $status = $this->users->save($fileNameUser, $email, "", "", "", $statusAfterProtection, "", $modified, $errors) ? "ok" : "error"; - if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($status=="ok" && $statusBeforeProtection=="active") { - $status = $this->response->sendMail($scheme, $address, $base, $email, "reactivate") ? "done" : "error"; - if ($status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); - } - } - } - if ($this->response->userFailedError=="login" || $this->response->userFailedError=="auth") { - $this->response->destroyCookies($scheme, $address, $base); - $this->response->status = "error"; - $this->yellow->page->error(430); - } else { - $this->response->status = "error"; - $this->yellow->page->error(500, "Link has expired!"); - } - } - } - - // Return user status changes - public function getUserStatus($email, $action) { - switch ($action) { - case "confirm": $statusExpected = "unconfirmed"; break; - case "approve": $statusExpected = "unapproved"; break; - case "recover": $statusExpected = "active"; break; - case "reactivate": $statusExpected = "inactive"; break; - case "verify": $statusExpected = "unverified"; break; - case "change": $statusExpected = "active"; break; - case "remove": $statusExpected = "active"; break; - } - return $this->users->getStatus($email)==$statusExpected ? "ok" : "done"; - } - - // Return user account changes - public function getUserAccount($email, $password, $action) { - $status = null; - foreach ($this->yellow->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onEditUserAccount")) { - $status = $value["obj"]->onEditUserAccount($email, $password, $action, $this->users); - if (!is_null($status)) break; - } - } - if (is_null($status)) { - $status = "ok"; - if (!empty($password) && strlenu($password)<$this->yellow->config->get("editUserPasswordMinLength")) $status = "weak"; - if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) $status = "invalid"; - } - return $status; - } - - // Return user restrictions - public function getUserRestrictions($email, $location, $fileName) { - $userRestrictions = null; - foreach ($this->yellow->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onEditUserRestrictions")) { - $userRestrictions = $value["obj"]->onEditUserRestrictions($email, $location, $fileName, $this->users); - if (!is_null($userRestrictions)) break; - } - } - if (is_null($userRestrictions)) { - $userRestrictions = substru($location, 0, strlenu($this->users->getHome($email)))!=$this->users->getHome($email); - $userRestrictions |= empty($fileName) || strlenu(dirname($fileName))>128 || strlenu(basename($fileName))>128; - } - return $userRestrictions; - } - - // Return user language - public function getUserLanguage($email) { - $language = $this->users->getLanguage($email); - if (!$this->yellow->text->isLanguage($language)) $language = $this->yellow->config->get("language"); - return $language; - } - - // Check if request came from same site - public function isRequestSameSite($method, $scheme, $address) { - if (preg_match("#^(\w+)://([^/]+)(.*)$#", $_SERVER["HTTP_REFERER"], $matches)) $origin = "$matches[1]://$matches[2]"; - if (isset($_SERVER["HTTP_ORIGIN"])) $origin = $_SERVER["HTTP_ORIGIN"]; - return $_SERVER["REQUEST_METHOD"]==$method && $origin=="$scheme://$address"; - } -} - -class YellowResponse { - public $yellow; //access to API - public $plugin; //access to plugin - public $active; //location is active? (boolean) - public $userEmail; //user email - public $userRestrictions; //user can change page? (boolean) - 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 $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->plugin = $yellow->plugins->get("edit"); - } - - // Return new page - public function getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0); - $this->editContentFile($page, "create"); - if ($this->yellow->pages->find($page->location)) { - $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation")); - $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); - while ($this->yellow->pages->find($page->location) || empty($page->fileName)) { - $rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData)); - $page->rawData = $this->normaliseLines($rawData, $endOfLine); - $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation")); - $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); - if (++$pageCounter>999) break; - } - if ($this->yellow->pages->find($page->location) || empty($page->fileName)) { - $page->error(500, "Page '".$page->get("title")."' is not possible!"); - } - } else { - $page->fileName = $this->getPageNewFile($page->location); - } - if ($this->plugin->getUserRestrictions($this->userEmail, $page->location, $page->fileName)) { - $page->error(500, "Page '".$page->get("title")."' is restricted!"); - } - return $page; - } - - // Return modified page - public function getPageEdit($scheme, $address, $base, $location, $fileName, $rawDataSource, $rawDataEdit, $rawDataFile, $endOfLine) { - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $rawData = $this->plugin->merge->merge( - $this->normaliseLines($rawDataSource, $endOfLine), - $this->normaliseLines($rawDataEdit, $endOfLine), - $this->normaliseLines($rawDataFile, $endOfLine)); - $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0); - $pageSource = new YellowPage($this->yellow); - $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName); - $pageSource->parseData($this->normaliseLines($rawDataSource, $endOfLine), false, 0); - $this->editContentFile($page, "edit"); - if ($this->isMetaModified($pageSource, $page) && $page->location!=$this->yellow->pages->getHomeLocation($page->location)) { - $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation"), true); - $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); - if ($page->location!=$pageSource->location && ($this->yellow->pages->find($page->location) || 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 ($this->plugin->getUserRestrictions($this->userEmail, $page->location, $page->fileName) || - $this->plugin->getUserRestrictions($this->userEmail, $pageSource->location, $pageSource->fileName)) { - $page->error(500, "Page '".$page->get("title")."' is restricted!"); - } - return $page; - } - - // Return deleted page - public function getPageDelete($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0); - $this->editContentFile($page, "delete"); - if ($this->plugin->getUserRestrictions($this->userEmail, $page->location, $page->fileName)) { - $page->error(500, "Page '".$page->get("title")."' is restricted!"); - } - return $page; - } - - // Return preview page - public function getPagePreview($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 200); - $this->yellow->text->setLanguage($page->get("language")); - $page->set("pageClass", "page-preview"); - $page->set("pageClass", $page->get("pageClass")." template-".$page->get("template")); - $output = "<div class=\"".$page->getHtml("pageClass")."\"><div class=\"content\">"; - if ($this->yellow->config->get("editToolbarButtons")!="none") $output .= "<h1>".$page->getHtml("titleContent")."</h1>\n"; - $output .= $page->getContent(); - $output .= "</div></div>"; - $page->setOutput($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->set("fileNameShort", $fileNameShort); - $this->editMediaFile($file, "upload"); - $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation")); - $file->fileName = substru($file->location, 1); - while (is_file($file->fileName)) { - $fileNameShort = $this->getFileNext(basename($file->fileName)); - $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation")); - $file->fileName = substru($file->location, 1); - if (++$fileCounter>999) break; - } - if (is_file($file->fileName)) $file->error(500, "File '".$file->get("fileNameShort")."' is not possible!"); - return $file; - } - - // Return page data including status information - public function getPageData($page) { - $data = array(); - if ($this->isUser()) { - $data["title"] = $this->yellow->toolbox->getMetaData($this->rawDataEdit, "title"); - $data["rawDataSource"] = $this->rawDataSource; - $data["rawDataEdit"] = $this->rawDataEdit; - $data["rawDataNew"] = $this->getRawDataNew($page); - $data["rawDataOutput"] = strval($this->rawDataOutput); - $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; - $data["safeMode"] = $this->yellow->page->safeMode; - } - if ($this->action!="none") $data = array_merge($data, $this->getRequestData()); - $data["action"] = $this->action; - $data["status"] = $this->status; - $data["statusCode"] = $this->yellow->page->statusCode; - return $data; - } - - // Return configuration data including user information - public function getConfigData() { - $data = $this->yellow->config->getData("", "Location"); - if ($this->isUser()) { - $data["userEmail"] = $this->userEmail; - $data["userName"] = $this->plugin->users->getName($this->userEmail); - $data["userLanguage"] = $this->plugin->users->getLanguage($this->userEmail); - $data["userStatus"] = $this->plugin->users->getStatus($this->userEmail); - $data["userHome"] = $this->plugin->users->getHome($this->userEmail); - $data["userRestrictions"] = intval($this->isUserRestrictions()); - $data["userWebmaster"] = intval($this->isUserWebmaster()); - $data["serverScheme"] = $this->yellow->config->get("serverScheme"); - $data["serverAddress"] = $this->yellow->config->get("serverAddress"); - $data["serverBase"] = $this->yellow->config->get("serverBase"); - $data["serverFileSizeMax"] = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize")); - $data["serverVersion"] = "Datenstrom Yellow ".YellowCore::VERSION; - $data["serverPlugins"] = array(); - foreach ($this->yellow->plugins->plugins as $key=>$value) { - $data["serverPlugins"][$key] = $value["plugin"]; - } - $data["serverLanguages"] = array(); - foreach ($this->yellow->text->getLanguages() as $language) { - $data["serverLanguages"][$language] = $this->yellow->text->getTextHtml("languageDescription", $language); - } - $data["editUploadExtensions"] = $this->yellow->config->get("editUploadExtensions"); - $data["editKeyboardShortcuts"] = $this->yellow->config->get("editKeyboardShortcuts"); - $data["editToolbarButtons"] = $this->getToolbarButtons("edit"); - $data["emojiawesomeToolbarButtons"] = $this->getToolbarButtons("emojiawesome"); - $data["fontawesomeToolbarButtons"] = $this->getToolbarButtons("fontawesome"); - } else { - $data["editLoginEmail"] = $this->yellow->page->get("editLoginEmail"); - $data["editLoginPassword"] = $this->yellow->page->get("editLoginPassword"); - $data["editLoginRestrictions"] = intval($this->isLoginRestrictions()); - } - if (defined("DEBUG") && DEBUG>=1) $data["debug"] = DEBUG; - return $data; - } - - // Return request strings - public function getRequestData() { - $data = array(); - foreach ($_REQUEST as $key=>$value) { - if ($key=="password" || $key=="authtoken" || $key=="csrftoken" || $key=="actiontoken" || substru($key, 0, 7)=="rawdata") continue; - $data["request".ucfirst($key)] = trim($value); - } - return $data; - } - - // Return text strings - public function getTextData() { - $textLanguage = $this->yellow->text->getData("language", $this->language); - $textEdit = $this->yellow->text->getData("edit", $this->language); - $textYellow = $this->yellow->text->getData("yellow", $this->language); - return array_merge($textLanguage, $textEdit, $textYellow); - } - - // Return toolbar buttons - public function getToolbarButtons($name) { - if ($name=="edit") { - $toolbarButtons = $this->yellow->config->get("editToolbarButtons"); - if ($toolbarButtons=="auto") { - $toolbarButtons = ""; - if ($this->yellow->plugins->isExisting("markdown")) $toolbarButtons = "preview, format, bold, italic, code, list, link, file"; - if ($this->yellow->plugins->isExisting("emojiawesome")) $toolbarButtons .= ", emojiawesome"; - if ($this->yellow->plugins->isExisting("fontawesome")) $toolbarButtons .= ", fontawesome"; - if ($this->yellow->plugins->isExisting("draft")) $toolbarButtons .= ", draft"; - if ($this->yellow->plugins->isExisting("markdown")) $toolbarButtons .= ", markdown"; - } - } else { - $toolbarButtons = $this->yellow->config->get("{$name}ToolbarButtons"); - } - return $toolbarButtons; - } - - // Return end of line format - public function getEndOfLine($rawData = "") { - $endOfLine = $this->yellow->config->get("editEndOfLine"); - if ($endOfLine=="auto") { - $rawData = empty($rawData) ? PHP_EOL : substru($rawData, 0, 4096); - $endOfLine = strposu($rawData, "\r")===false ? "lf" : "crlf"; - } - return $endOfLine; - } - - // Return raw data for new page - public function getRawDataNew($page, $customTitle = false) { - foreach ($this->yellow->pages->path($page->location)->reverse() as $ancestor) { - if ($ancestor->isExisting("templateNew")) { - $name = $this->yellow->lookup->normaliseName($ancestor->get("templateNew")); - $location = $this->yellow->pages->getHomeLocation($page->location).$this->yellow->config->get("contentSharedDir"); - $fileName = $this->yellow->lookup->findFileFromLocation($location, true).$this->yellow->config->get("newFile"); - $fileName = strreplaceu("(.*)", $name, $fileName); - if (is_file($fileName)) break; - } - } - if (!is_file($fileName)) { - $name = $this->yellow->lookup->normaliseName($this->yellow->config->get("template")); - $location = $this->yellow->pages->getHomeLocation($page->location).$this->yellow->config->get("contentSharedDir"); - $fileName = $this->yellow->lookup->findFileFromLocation($location, true).$this->yellow->config->get("newFile"); - $fileName = strreplaceu("(.*)", $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->plugin->users->getName($this->userEmail), " "), $rawData); - $rawData = preg_replace("/@username/i", $this->plugin->users->getName($this->userEmail), $rawData); - $rawData = preg_replace("/@userlanguage/i", $this->plugin->users->getLanguage($this->userEmail), $rawData); - } else { - $rawData = "---\nTitle: Page\n---\n"; - } - if ($customTitle) { - $title = $this->yellow->toolbox->createTextTitle($page->location); - $rawData = $this->yellow->toolbox->setMetaData($rawData, "title", $title); - } - return $rawData; - } - - // Return location for new/modified page - public function getPageNewLocation($rawData, $pageLocation, $pageNewLocation, $pageMatchLocation = false) { - $location = empty($pageNewLocation) ? "@title" : $pageNewLocation; - $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); - if (!preg_match("/^\//", $location)) { - if ($this->yellow->lookup->isFileLocation($pageLocation) || !$pageMatchLocation) { - $location = $this->yellow->lookup->getDirectoryLocation($pageLocation).$location; - } else { - $location = $this->yellow->lookup->getDirectoryLocation(rtrim($pageLocation, "/")).$location; - } - } - if ($pageMatchLocation) { - $location = rtrim($location, "/").($this->yellow->lookup->isFileLocation($pageLocation) ? "" : "/"); - } - return $location; - } - - // Return title for new/modified page - 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); - return trim(preg_replace("/-+/", "-", $value), "-"); - } - - // Return data for new/modified page - public function getPageNewData($rawData, $key, $filterFirst = false, $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); - 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)) { - if (!is_dir(dirname($fileName))) { - $path = ""; - $tokens = explode("/", $fileName); - for ($i=0; $i<count($tokens)-1; ++$i) { - if (!is_dir($path.$tokens[$i])) { - if (!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i])) { - $number = 1; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^[\d\-\_\.]+(.*)$/", true, true, false) as $entry) { - if ($number!=1 && $number!=intval($entry)) break; - $number = intval($entry)+1; - } - $tokens[$i] = "$number-".$tokens[$i]; - } - $tokens[$i] = $this->yellow->lookup->normaliseName($tokens[$i], false, false, true); - } - $path .= $tokens[$i]."/"; - } - $fileName = $path.$tokens[$i]; - $pageFileName = empty($pageFileName) ? $fileName : $pageFileName; - } - $prefix = $this->getPageNewPrefix($location, $pageFileName, $pagePrefix); - if ($this->yellow->lookup->isFileLocation($location)) { - preg_match("#^(.*)\/(.+?)$#", $fileName, $matches); - $path = $matches[1]; - $text = $this->yellow->lookup->normaliseName($matches[2], true, true); - if (preg_match("/^[\d\-\_\.]*$/", $text)) $prefix = ""; - $fileName = $path."/".$prefix.$text.$this->yellow->config->get("contentExtension"); - } else { - preg_match("#^(.*)\/(.+?)$#", dirname($fileName), $matches); - $path = $matches[1]; - $text = $this->yellow->lookup->normaliseName($matches[2], true, false); - if (preg_match("/^[\d\-\_\.]*$/", $text)) $prefix = ""; - $fileName = $path."/".$prefix.$text."/".$this->yellow->config->get("contentDefaultFile"); - } - } - return $fileName; - } - - // Return prefix for new/modified page - public function getPageNewPrefix($location, $pageFileName, $pagePrefix) { - if (empty($pagePrefix)) { - if ($this->yellow->lookup->isFileLocation($location)) { - preg_match("#^(.*)\/(.+?)$#", $pageFileName, $matches); - $pagePrefix = $matches[2]; - } else { - preg_match("#^(.*)\/(.+?)$#", dirname($pageFileName), $matches); - $pagePrefix = $matches[2]; - } - } - return $this->yellow->lookup->normalisePrefix($pagePrefix, true); - } - - // Return location for new file - public function getFileNewLocation($fileNameShort, $pageLocation, $fileNewLocation) { - $location = empty($fileNewLocation) ? $this->yellow->config->get("editUploadNewLocation") : $fileNewLocation; - $location = preg_replace("/@timestamp/i", time(), $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); - $location = preg_replace("/@filename/i", strtoloweru($fileNameShort), $location); - if (!preg_match("/^\//", $location)) { - $location = $this->yellow->config->get("mediaLocation").$location; - } - return $location; - } - - // Return group for new file - public function getFileNewGroup($fileNameShort) { - $path = $this->yellow->config->get("mediaDir"); - $fileType = $this->yellow->toolbox->getFileType($fileNameShort); - $fileName = $this->yellow->config->get(preg_match("/(gif|jpg|png|svg)$/", $fileType) ? "imageDir" : "downloadDir").$fileNameShort; - preg_match("#^$path(.+?)\/#", $fileName, $matches); - return strtoloweru($matches[1]); - } - - // Return folder for new file - public function getFileNewFolder($pageLocation) { - $parentTopLocation = $this->yellow->pages->getParentTopLocation($pageLocation); - if ($parentTopLocation==$this->yellow->pages->getHomeLocation($pageLocation)) $parentTopLocation .= "home"; - return strtoloweru(trim($parentTopLocation, "/")); - } - - // Return next file name - public function getFileNext($fileNameShort) { - preg_match("/^(.*?)(\d*)(\..*?)?$/", $fileNameShort, $matches); - $fileText = $matches[1]; - $fileNumber = strempty($matches[2]) ? "-2" : $matches[2]+1; - $fileExtension = $matches[3]; - return $fileText.$fileNumber.$fileExtension; - } - - // Return next title - public function getTitleNext($rawData) { - preg_match("/^(.*?)(\d*)$/", $this->yellow->toolbox->getMetaData($rawData, "title"), $matches); - $titleText = $matches[1]; - $titleNumber = strempty($matches[2]) ? " 2" : $matches[2]+1; - return $titleText.$titleNumber; - } - - // 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 $text; - } - - // Create browser cookies - public function createCookies($scheme, $address, $base, $email) { - $expire = time() + $this->yellow->config->get("editLoginSessionTimeout"); - $authToken = $this->plugin->users->createAuthToken($email, $expire); - $csrfToken = $this->plugin->users->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); - } - - // Send mail to user - public function sendMail($scheme, $address, $base, $email, $action) { - if ($action=="welcome" || $action=="goodbye") { - $url = "$scheme://$address$base/"; - } else { - $expire = time() + 60*60*24; - $actionToken = $this->plugin->users->createActionToken($email, $action, $expire); - $url = "$scheme://$address$base"."/action:$action/email:$email/expire:$expire/actiontoken:$actionToken/"; - } - if ($action=="approve") { - $account = $email; - $name = $this->yellow->config->get("author"); - $email = $this->yellow->config->get("email"); - } else { - $account = $email; - $name = $this->plugin->users->getName($email); - } - $language = $this->plugin->users->getLanguage($email); - if (!$this->yellow->text->isLanguage($language)) $language = $this->yellow->config->get("language"); - $sitename = $this->yellow->config->get("sitename"); - $prefix = "edit".ucfirst($action); - $message = $this->yellow->text->getText("{$prefix}Message", $language); - $message = strreplaceu("\\n", "\n", $message); - $message = preg_replace("/@useraccount/i", $account, $message); - $message = preg_replace("/@usershort/i", strtok($name, " "), $message); - $message = preg_replace("/@username/i", $name, $message); - $message = preg_replace("/@userlanguage/i", $language, $message); - $mailTo = mb_encode_mimeheader("$name")." <$email>"; - $mailSubject = mb_encode_mimeheader($this->yellow->text->getText("{$prefix}Subject", $language)); - $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"; - $mailMessage = "$message\r\n\r\n$url\r\n-- \r\n$sitename"; - return mail($mailTo, $mailSubject, $mailMessage, $mailHeaders); - } - - // Change content file - public function editContentFile($page, $action) { - if (!$page->isError()) { - foreach ($this->yellow->plugins->plugins 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->plugins->plugins as $key=>$value) { - if (method_exists($value["obj"], "onEditMediaFile")) $value["obj"]->onEditMediaFile($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 is webmaster - public function isUserWebmaster() { - return !empty($this->userEmail) && $this->userEmail==$this->yellow->config->get("email"); - } - - // Check if user has restrictions - public function isUserRestrictions() { - return empty($this->userEmail) || $this->userRestrictions; - } - - // Check if login has restrictions - public function isLoginRestrictions() { - return $this->yellow->config->get("editLoginRestrictions"); - } -} - -class YellowUsers { - 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 "YellowUsers::load file:$fileName<br/>\n"; - $fileData = $this->yellow->toolbox->readFile($fileName); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\#/", $line)) continue; - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2])) { - list($hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) = explode(",", $matches[2]); - $this->set($matches[1], $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home); - if (defined("DEBUG") && DEBUG>=3) echo "YellowUsers::load email:$matches[1]<br/>\n"; - } - } - } - - // Save user to file - public function save($fileName, $email, $password = "", $name = "", $language = "", $status = "", $stamp = "", $modified = "", $errors = "", $pending = "", $home = "") { - if (!empty($password)) $hash = $this->createHash($password); - if ($this->isExisting($email)) { - $email = strreplaceu(",", "-", $email); - $hash = strreplaceu(",", "-", empty($hash) ? $this->users[$email]["hash"] : $hash); - $name = strreplaceu(",", "-", empty($name) ? $this->users[$email]["name"] : $name); - $language = strreplaceu(",", "-", empty($language) ? $this->users[$email]["language"] : $language); - $status = strreplaceu(",", "-", empty($status) ? $this->users[$email]["status"] : $status); - $stamp = strreplaceu(",", "-", empty($stamp) ? $this->users[$email]["stamp"] : $stamp); - $modified = strreplaceu(",", "-", empty($modified) ? time() : $modified); - $errors = strreplaceu(",", "-", empty($errors) ? "0" : $errors); - $pending = strreplaceu(",", "-", empty($pending) ? $this->users[$email]["pending"] : $pending); - $home = strreplaceu(",", "-", empty($home) ? $this->users[$email]["home"] : $home); - } else { - $email = strreplaceu(",", "-", empty($email) ? "none" : $email); - $hash = strreplaceu(",", "-", empty($hash) ? "none" : $hash); - $name = strreplaceu(",", "-", empty($name) ? $this->yellow->config->get("sitename") : $name); - $language = strreplaceu(",", "-", empty($language) ? $this->yellow->config->get("language") : $language); - $status = strreplaceu(",", "-", empty($status) ? "active" : $status); - $stamp = strreplaceu(",", "-", empty($stamp) ? $this->createStamp() : $stamp); - $modified = strreplaceu(",", "-", empty($modified) ? time() : $modified); - $errors = strreplaceu(",", "-", empty($errors) ? "0" : $errors); - $pending = strreplaceu(",", "-", empty($pending) ? "none" : $pending); - $home = strreplaceu(",", "-", empty($home) ? $this->yellow->config->get("editUserHome") : $home); - } - $this->set($email, $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home); - $fileData = $this->yellow->toolbox->readFile($fileName); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && $matches[1]==$email) { - $fileDataNew .= "$email: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n"; - $found = true; - } else { - $fileDataNew .= $line; - } - } - if (!$found) $fileDataNew .= "$email: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n"; - return $this->yellow->toolbox->createFile($fileName, $fileDataNew); - } - - // Remove user from file - public function remove($fileName, $email) { - unset($this->users[$email]); - $fileData = $this->yellow->toolbox->readFile($fileName); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2]) && $matches[1]!=$email) $fileDataNew .= $line; - } - return $this->yellow->toolbox->createFile($fileName, $fileDataNew); - } - - // Set user data - public function set($email, $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) { - $this->users[$email] = array(); - $this->users[$email]["email"] = $email; - $this->users[$email]["hash"] = $hash; - $this->users[$email]["name"] = $name; - $this->users[$email]["language"] = $language; - $this->users[$email]["status"] = $status; - $this->users[$email]["stamp"] = $stamp; - $this->users[$email]["modified"] = $modified; - $this->users[$email]["errors"] = $errors; - $this->users[$email]["pending"] = $pending; - $this->users[$email]["home"] = $home; - } - - // Check user authentication from email and password - public function checkAuthLogin($email, $password) { - $algorithm = $this->yellow->config->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, $ignoreCsrfToken) { - $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) || $ignoreCsrfToken); - } - - // 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); - } - - // 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->getStamp($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"; - return substrb($signature, 4); - } - - // Create CSRF token - public function createCsrfToken() { - return $this->yellow->toolbox->createSalt(64); - } - - // Create password hash - public function createHash($password) { - $algorithm = $this->yellow->config->get("editUserHashAlgorithm"); - $cost = $this->yellow->config->get("editUserHashCost"); - $hash = $this->yellow->toolbox->createHash($password, $algorithm, $cost); - if (empty($hash)) $hash = "error-hash-algorithm-$algorithm"; - return $hash; - } - - // Create user stamp - public function createStamp() { - $stamp = $this->yellow->toolbox->createSalt(20); - while ($this->getAuthEmail("none", $stamp)) { - $stamp = $this->yellow->toolbox->createSalt(20); - } - return $stamp; - } - - // Return user email from authentication, timing attack safe email lookup - public function getAuthEmail($authToken, $stamp = "") { - if (empty($stamp)) $stamp = substrb($authToken, 96, 20); - foreach ($this->users as $key=>$value) { - if ($this->yellow->toolbox->verifyToken($value["stamp"], $stamp)) $email = $key; - } - return $email; - } - - // Return expiration time from authentication - public function getAuthExpire($authToken) { - return hexdec(substrb($authToken, 96+20)); - } - - // Return user hash - public function getHash($email) { - return $this->isExisting($email) ? $this->users[$email]["hash"] : ""; - } - - // Return user name - public function getName($email) { - return $this->isExisting($email) ? $this->users[$email]["name"] : ""; - } - - // Return user language - public function getLanguage($email) { - return $this->isExisting($email) ? $this->users[$email]["language"] : ""; - } - - // Return user status - public function getStatus($email) { - return $this->isExisting($email) ? $this->users[$email]["status"] : ""; - } - - // Return user stamp - public function getStamp($email) { - return $this->isExisting($email) ? $this->users[$email]["stamp"] : ""; - } - - // Return user modified - public function getModified($email) { - return $this->isExisting($email) ? $this->users[$email]["modified"] : ""; - } - - // Return user errors - public function getErrors($email) { - return $this->isExisting($email) ? $this->users[$email]["errors"] : ""; - } - - // Return user pending - public function getPending($email) { - return $this->isExisting($email) ? $this->users[$email]["pending"] : ""; - } - - // Return user home - public function getHome($email) { - return $this->isExisting($email) ? $this->users[$email]["home"] : ""; - } - - // Return number of users - public function getNumber() { - return count($this->users); - } - - // Return user data - public function getData() { - $data = array(); - foreach ($this->users as $key=>$value) { - $name = $value["name"]; - $status = $value["status"]; - if (preg_match("/\s/", $name)) $name = "\"$name\""; - if (preg_match("/\s/", $status)) $status = "\"$status\""; - $data[$key] = "$value[email] $name $status"; - } - 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 = $this->users[$email]["modified"] + 60*60*24; - if ($status=="active" || $status=="inactive" || $reserved>time()) $taken = true; - } - return $taken; - } - - // Check if user exists - public function isExisting($email) { - return !is_null($this->users[$email]); - } -} - -class YellowMerge { - public $yellow; //access to API - const ADD = "+"; //merge types - const MODIFY = "*"; - const REMOVE = "-"; - const SAME = " "; - - public function __construct($yellow) { - $this->yellow = $yellow; - } - - // Merge text, null if not possible - public function merge($textSource, $textMine, $textYours, $showDiff = false) { - if ($textMine!=$textYours) { - $diffMine = $this->buildDiff($textSource, $textMine); - $diffYours = $this->buildDiff($textSource, $textYours); - $diff = $this->mergeDiff($diffMine, $diffYours); - $output = $this->getOutput($diff, $showDiff); - } else { - $output = $textMine; - } - return $output; - } - - // Build differences to common source - public function buildDiff($textSource, $textOther) { - $diff = array(); - $lastRemove = -1; - $textStart = 0; - $textSource = $this->yellow->toolbox->getTextLines($textSource); - $textOther = $this->yellow->toolbox->getTextLines($textOther); - $sourceEnd = $sourceSize = count($textSource); - $otherEnd = $otherSize = count($textOther); - while ($textStart<$sourceEnd && $textStart<$otherEnd && $textSource[$textStart]==$textOther[$textStart]) { - ++$textStart; - } - while ($textStart<$sourceEnd && $textStart<$otherEnd && $textSource[$sourceEnd-1]==$textOther[$otherEnd-1]) { - --$sourceEnd; - --$otherEnd; - } - for ($pos=0; $pos<$textStart; ++$pos) { - array_push($diff, array(YellowMerge::SAME, $textSource[$pos], false)); - } - $lcs = $this->buildDiffLCS($textSource, $textOther, $textStart, $sourceEnd-$textStart, $otherEnd-$textStart); - for ($x=0,$y=0,$xEnd=$otherEnd-$textStart,$yEnd=$sourceEnd-$textStart; $x<$xEnd || $y<$yEnd;) { - $max = $lcs[$y][$x]; - if ($y<$yEnd && $lcs[$y+1][$x]==$max) { - array_push($diff, array(YellowMerge::REMOVE, $textSource[$textStart+$y], false)); - if ($lastRemove==-1) $lastRemove = count($diff)-1; - ++$y; - continue; - } - if ($x<$xEnd && $lcs[$y][$x+1]==$max) { - if ($lastRemove==-1 || $diff[$lastRemove][0]!=YellowMerge::REMOVE) { - array_push($diff, array(YellowMerge::ADD, $textOther[$textStart+$x], false)); - $lastRemove = -1; - } else { - $diff[$lastRemove] = array(YellowMerge::MODIFY, $textOther[$textStart+$x], false); - ++$lastRemove; - if (count($diff)==$lastRemove) $lastRemove = -1; - } - ++$x; - continue; - } - array_push($diff, array(YellowMerge::SAME, $textSource[$textStart+$y], false)); - $lastRemove = -1; - ++$x; - ++$y; - } - for ($pos=$sourceEnd;$pos<$sourceSize; ++$pos) { - array_push($diff, array(YellowMerge::SAME, $textSource[$pos], false)); - } - return $diff; - } - - // Build longest common subsequence - public function buildDiffLCS($textSource, $textOther, $textStart, $yEnd, $xEnd) { - $lcs = array_fill(0, $yEnd+1, array_fill(0, $xEnd+1, 0)); - for ($y=$yEnd-1; $y>=0; --$y) { - for ($x=$xEnd-1; $x>=0; --$x) { - if ($textSource[$textStart+$y]==$textOther[$textStart+$x]) { - $lcs[$y][$x] = $lcs[$y+1][$x+1]+1; - } else { - $lcs[$y][$x] = max($lcs[$y][$x+1], $lcs[$y+1][$x]); - } - } - } - return $lcs; - } - - // Merge differences - public function mergeDiff($diffMine, $diffYours) { - $diff = array(); - $posMine = $posYours = 0; - while ($posMine<count($diffMine) && $posYours<count($diffYours)) { - $typeMine = $diffMine[$posMine][0]; - $typeYours = $diffYours[$posYours][0]; - if ($typeMine==YellowMerge::SAME) { - array_push($diff, $diffYours[$posYours]); - } elseif ($typeYours==YellowMerge::SAME) { - array_push($diff, $diffMine[$posMine]); - } elseif ($typeMine==YellowMerge::ADD && $typeYours==YellowMerge::ADD) { - $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false); - } elseif ($typeMine==YellowMerge::MODIFY && $typeYours==YellowMerge::MODIFY) { - $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false); - } elseif ($typeMine==YellowMerge::REMOVE && $typeYours==YellowMerge::REMOVE) { - array_push($diff, $diffMine[$posMine]); - } elseif ($typeMine==YellowMerge::ADD) { - array_push($diff, $diffMine[$posMine]); - } elseif ($typeYours==YellowMerge::ADD) { - array_push($diff, $diffYours[$posYours]); - } else { - $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], true); - } - if (defined("DEBUG") && DEBUG>=2) echo "YellowMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n"; - if ($typeMine==YellowMerge::ADD || $typeYours==YellowMerge::ADD) { - if ($typeMine==YellowMerge::ADD) ++$posMine; - if ($typeYours==YellowMerge::ADD) ++$posYours; - } else { - ++$posMine; - ++$posYours; - } - } - for (;$posMine<count($diffMine); ++$posMine) { - array_push($diff, $diffMine[$posMine]); - $typeMine = $diffMine[$posMine][0]; - $typeYours = " "; - if (defined("DEBUG") && DEBUG>=2) echo "YellowMerge::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 "YellowMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n"; - } - return $diff; - } - - // Merge potential conflict - public function mergeConflict(&$diff, $diffMine, $diffYours, $conflict) { - if (!$conflict && $diffMine[1]==$diffYours[1]) { - array_push($diff, $diffMine); - } else { - array_push($diff, array($diffMine[0], $diffMine[1], true)); - array_push($diff, array($diffYours[0], $diffYours[1], true)); - } - } - - // Return merged text, null if not possible - public function getOutput($diff, $showDiff = false) { - $output = ""; - if (!$showDiff) { - for ($i=0; $i<count($diff); ++$i) { - if ($diff[$i][0]!=YellowMerge::REMOVE) $output .= $diff[$i][1]; - $conflict |= $diff[$i][2]; - } - } else { - for ($i=0; $i<count($diff); ++$i) { - $output .= $diff[$i][2] ? "! " : $diff[$i][0]." "; - $output .= $diff[$i][1]; - } - } - return !$conflict ? $output : null; - } -} diff --git a/system/plugins/image.php b/system/plugins/image.php @@ -1,260 +0,0 @@ -<?php -// Image plugin, https://github.com/datenstrom/yellow-plugins/tree/master/image -// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowImage { - const VERSION = "0.8.1"; - public $yellow; //access to API - public $graphicsLibrary; //graphics library support? (boolean) - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - $this->yellow->config->setDefault("imageAlt", "Image"); - $this->yellow->config->setDefault("imageUploadWidthMax", "1280"); - $this->yellow->config->setDefault("imageUploadHeightMax", "1280"); - $this->yellow->config->setDefault("imageUploadJpgQuality", "80"); - $this->yellow->config->setDefault("imageThumbnailLocation", "/media/thumbnails/"); - $this->yellow->config->setDefault("imageThumbnailDir", "media/thumbnails/"); - $this->yellow->config->setDefault("imageThumbnailJpgQuality", "80"); - $this->graphicsLibrary = $this->isGraphicsLibrary(); - } - - // Handle page content of shortcut - public function onParseContentShortcut($page, $name, $text, $type) { - $output = null; - if ($name=="image" && $type=="inline") { - if (!$this->graphicsLibrary) { - $this->yellow->page->error(500, "Plugin 'image' requires GD library with gif/jpg/png support!"); - return $output; - } - list($name, $alt, $style, $width, $height) = $this->yellow->toolbox->getTextArgs($text); - if (!preg_match("/^\w+:/", $name)) { - if (empty($alt)) $alt = $this->yellow->config->get("imageAlt"); - if (empty($width)) $width = "100%"; - if (empty($height)) $height = $width; - list($src, $width, $height) = $this->getImageInformation($this->yellow->config->get("imageDir").$name, $width, $height); - } else { - if (empty($alt)) $alt = $this->yellow->config->get("imageAlt"); - $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)."\""; - $output .= " />"; - } - return $output; - } - - // Handle media file changes - public function onEditMediaFile($file, $action) { - if ($action=="upload" && $this->graphicsLibrary) { - $fileName = $file->fileName; - $fileType = $this->yellow->toolbox->getFileType($file->get("fileNameShort")); - list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInformation($fileName, $fileType); - $widthMax = $this->yellow->config->get("imageUploadWidthMax"); - $heightMax = $this->yellow->config->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); - if (!$this->saveImage($image, $fileName, $type, $this->yellow->config->get("imageUploadJpgQuality"))) { - $file->error(500, "Can't write file '$fileName'!"); - } - } - if ($this->yellow->config->get("safeMode") && $fileType=="svg") { - $output = $this->sanitiseXmlData($this->yellow->toolbox->readFile($fileName)); - if (empty($output) || !$this->yellow->toolbox->createFile($fileName, $output)) { - $file->error(500, "Can't write file '$fileName'!"); - } - } - } - } - - // Handle command - public function onCommand($args) { - list($command) = $args; - switch ($command) { - case "clean": $statusCode = $this->processCommandClean($args); break; - default: $statusCode = 0; - } - return $statusCode; - } - - // Process command to clean thumbnails - public function processCommandClean($args) { - $statusCode = 0; - list($command, $path) = $args; - if ($path=="all") { - $path = $this->yellow->config->get("imageThumbnailDir"); - 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 - public function getImageInformation($fileName, $widthOutput, $heightOutput) { - $fileNameShort = substru($fileName, strlenu($this->yellow->config->get("imageDir"))); - list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInformation($fileName); - $widthOutput = $this->convertValueAndUnit($widthOutput, $widthInput); - $heightOutput = $this->convertValueAndUnit($heightOutput, $heightInput); - if (($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg") { - $src = $this->yellow->config->get("serverBase").$this->yellow->config->get("imageLocation").$fileNameShort; - $width = $widthOutput; - $height = $heightOutput; - } else { - $fileNameThumb = ltrim(str_replace(array("/", "\\", "."), "-", dirname($fileNameShort)."/".pathinfo($fileName, PATHINFO_FILENAME)), "-"); - $fileNameThumb .= "-".$widthOutput."x".$heightOutput; - $fileNameThumb .= ".".pathinfo($fileName, PATHINFO_EXTENSION); - $fileNameOutput = $this->yellow->config->get("imageThumbnailDir").$fileNameThumb; - if ($this->isFileNotUpdated($fileName, $fileNameOutput)) { - $image = $this->loadImage($fileName, $type); - $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); - if (is_file($fileNameOutput)) $this->yellow->toolbox->deleteFile($fileNameOutput); - if (!$this->saveImage($image, $fileNameOutput, $type, $this->yellow->config->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->config->get("serverBase").$this->yellow->config->get("imageThumbnailLocation").$fileNameThumb; - list($width, $height) = $this->yellow->toolbox->detectImageInformation($fileNameOutput); - } - return array($src, $width, $height); - } - - // Return image dimensions that fit, scale proportional - public function getImageDimensionsFit($widthInput, $heightInput, $widthMax, $heightMax) { - $widthOutput = $widthMax; - $heightOutput = $widthMax * ($heightInput / $widthInput); - if ($heightOutput>$heightMax) { - $widthOutput = $widthOutput * ($heightMax / $heightOutput); - $heightOutput = $heightOutput * ($heightMax / $heightOutput); - } - return array(intval($widthOutput), intval($heightOutput)); - } - - // Load image from file - public function loadImage($fileName, $type) { - $image = false; - switch ($type) { - case "gif": $image = @imagecreatefromgif($fileName); break; - case "jpg": $image = @imagecreatefromjpeg($fileName); break; - case "png": $image = @imagecreatefrompng($fileName); break; - } - return $image; - } - - // Save image to file - public function saveImage($image, $fileName, $type, $quality) { - $ok = false; - switch ($type) { - case "gif": $ok = @imagegif($image, $fileName); break; - case "jpg": $ok = @imagejpeg($image, $fileName, $quality); break; - case "png": $ok = @imagepng($image, $fileName); break; - } - return $ok; - } - - // Create image from scratch - public function createImage($width, $height) { - $image = imagecreatetruecolor($width, $height); - imagealphablending($image, false); - imagesavealpha($image, true); - return $image; - } - - // Resize image - public function resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput) { - $widthFit = $widthInput * ($heightOutput / $heightInput); - $heightFit = $heightInput * ($widthOutput / $widthInput); - $widthDiff = abs($widthOutput - $widthFit); - $heightDiff = abs($heightOutput - $heightFit); - $imageOutput = $this->createImage($widthOutput, $heightOutput); - if ($heightFit>$heightOutput) { - imagecopyresampled($imageOutput, $image, 0, $heightDiff/-2, 0, 0, $widthOutput, $heightFit, $widthInput, $heightInput); - } else { - imagecopyresampled($imageOutput, $image, $widthDiff/-2, 0, 0, 0, $widthFit, $heightOutput, $widthInput, $heightInput); - } - return $imageOutput; - } - - // Return value according to unit - public function convertValueAndUnit($text, $valueBase) { - $value = $unit = ""; - if (preg_match("/([\d\.]+)(\S*)/", $text, $matches)) { - $value = $matches[1]; - $unit = $matches[2]; - if ($unit=="%") $value = $valueBase * $value / 100; - } - return intval($value); - } - - // Return sanitised XML data - public function sanitiseXmlData($rawData) { - $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", "image", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "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", "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", "alt", "autocomplete", "background", "bgcolor", "border", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "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", "preload", "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", "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", "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", "xmlns", "y", "y1", "y2", "z", "zoomandpan"); - $attributesXml = array( - "xlink:href", "xml:id", "xml:space"); - if (!empty($rawData)) { - $entityLoader = libxml_disable_entity_loader(true); - $internalErrors = libxml_use_internal_errors(true); - $document = new DOMDocument(); - $document->recover = true; - if ($document->loadXML($rawData)) { - $elementsSafe = array_merge($elementsHtml, $elementsSvg); - $attributesSafe = array_merge($attributesHtml, $attributesSvg, $attributesXml); - $elements = $document->getElementsByTagName("*"); - for ($i=$elements->length-1; $i>=0; --$i) { - $element = $elements->item($i); - if (!in_array(strtolower($element->tagName), $elementsSafe)) { - $element->parentNode->removeChild($element); - continue; - } - for ($j=$element->attributes->length-1; $j>=0; --$j) { - $attribute = $element->attributes->item($j); - if (!in_array(strtolower($attribute->name), $attributesSafe) && !preg_match("/^(aria|data)-/i", $attribute->name)) { - $element->removeAttribute($attribute->name); - } - } - $href = $element->getAttribute("href"); - if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { - $element->setAttribute("href", "error-xss-filter"); - } - $href = $element->getAttribute("xlink:href"); - if (preg_match("/^\w+:/", $href) && !preg_match("/^(http|https|ftp|mailto):/", $href)) { - $element->setAttribute("xlink:href", "error-xss-filter"); - } - } - $output = $document->saveXML(); - if (!preg_match("/^<\?xml /", $rawData) && preg_match("/^<\?xml (.*?)>\s*(.*)$/s", $output, $matches)) $output = $matches[2]; - } - libxml_disable_entity_loader($entityLoader); - libxml_use_internal_errors($internalErrors); - } - return $output; - } - - // Check if file needs to be updated - public function isFileNotUpdated($fileNameInput, $fileNameOutput) { - return $this->yellow->toolbox->getFileModified($fileNameInput)!=$this->yellow->toolbox->getFileModified($fileNameOutput); - } - - // Check graphics library support - public function isGraphicsLibrary() { - return extension_loaded("gd") && function_exists("gd_info") && - ((imagetypes()&(IMG_GIF|IMG_JPG|IMG_PNG))==(IMG_GIF|IMG_JPG|IMG_PNG)); - } -} diff --git a/system/plugins/install-blog.zip b/system/plugins/install-blog.zip Binary files differ. diff --git a/system/plugins/install-language.zip b/system/plugins/install-language.zip Binary files differ. diff --git a/system/plugins/install-wiki.zip b/system/plugins/install-wiki.zip Binary files differ. diff --git a/system/plugins/install.php b/system/plugins/install.php @@ -1,303 +0,0 @@ -<?php -// Install plugin, https://github.com/datenstrom/yellow -// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowInstall { - const VERSION = "0.8.1"; - const PRIORITY = "1"; - public $yellow; //access to API - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - } - - // Handle request - public function onRequest($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->yellow->lookup->isContentFile($fileName)) { - $server = $this->yellow->toolbox->getServerVersion(true); - $this->checkServerRewrite($scheme, $address, $base, $location, $fileName) || die("Datenstrom Yellow requires $server rewrite module!"); - $this->checkServerAccess() || die("Datenstrom Yellow requires $server read/write access!"); - $statusCode = $this->processRequestInstall($scheme, $address, $base, $location, $fileName); - } - return $statusCode; - } - - // Handle command - public function onCommand($args) { - return $this->processCommandInstall(); - } - - // Process command to set up website - public function processCommandInstall() { - $statusCode = $this->updateLanguage(); - if ($statusCode==200) $statusCode = $this->updateConfig($this->getConfigData()); - if ($statusCode==200) $statusCode = $this->removeInstall(); - if ($statusCode==200) { - $statusCode = 0; - } else { - 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 set up website - public function processRequestInstall($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"])); - $email = trim($_REQUEST["email"]); - $password = trim($_REQUEST["password"]); - $language = trim($_REQUEST["language"]); - $feature = trim($_REQUEST["feature"]); - $status = trim($_REQUEST["status"]); - $this->yellow->pages->pages["root/"] = array(); - $this->yellow->page = new YellowPage($this->yellow); - $statusCode = $this->updateLanguage(); - $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName); - $this->yellow->page->parseData($this->getRawDataInstall(), false, $statusCode, $this->yellow->page->get("pageError")); - $this->yellow->page->safeMode = false; - if ($status=="install") $status = $this->updateUser($email, $password, $name, $language)==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateFeature($feature)==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateContent($language, "Home", "/")==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateContent($language, "About", "/about/")==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateContent($language, "Footer", "/shared/footer")==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateConfig($this->getConfigData()) ? "ok" : "error"; - if ($status=="ok") $status = $this->removeInstall() ? "done" : "error"; - if ($status=="done") { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $statusCode = $this->yellow->sendPage(); - } - return $statusCode; - } - - // Update language - public function updateLanguage() { - $statusCode = 200; - $path = $this->yellow->config->get("pluginDir")."install-language.zip"; - if (is_file($path) && $this->yellow->plugins->isExisting("update")) { - $zip = new ZipArchive(); - if ($zip->open($path)===true) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowInstall::updateLanguage file:$path<br/>\n"; - $languages = $this->getLanguageData("en, de, fr"); - if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1]; - $fileData = $zip->getFromName($pathBase.$this->yellow->config->get("updateInformationFile")); - 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], "/")) { - list($dummy, $entry) = explode("/", $matches[1], 2); - if (preg_match("/^language-(.*)\.txt$/", $entry, $tokens) && !is_null($languages[$tokens[1]])) { - $languages[$tokens[1]] = $entry; - } - } - } - $languages = array_slice(array_filter($languages, "strlen"), 0, 3); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (lcfirst($matches[1])=="plugin" || lcfirst($matches[1])=="theme") $software = $matches[2]; - if (lcfirst($matches[1])=="published") $modified = strtotime($matches[2]); - if (!empty($matches[1]) && !empty($matches[2]) && strposu($matches[1], "/")) { - list($dummy, $entry) = explode("/", $matches[1], 2); - list($fileName) = explode(",", $matches[2], 2); - $fileData = $zip->getFromName($pathBase.$entry); - if (preg_match("/^language.php$/", $entry)) { - $statusCode = $this->yellow->plugins->get("update")->updateSoftwareFile($fileName, $fileData, - $modified, 0, 0, "create,update", false, $software); - } - if (preg_match("/^language-(.*)\.txt$/", $entry, $tokens) && !is_null($languages[$tokens[1]])) { - $statusCode = $this->yellow->plugins->get("update")->updateSoftwareFile($fileName, $fileData, - $modified, 0, 0, "create,update", false, $software); - } - } - } - $zip->close(); - if ($statusCode==200) { - $this->yellow->text->load($this->yellow->config->get("pluginDir").$this->yellow->config->get("languageFile"), ""); - } - } else { - $statusCode = 500; - $this->yellow->page->error(500, "Can't open file '$path'!"); - } - } - return $statusCode; - } - - // Update user - public function updateUser($email, $password, $name, $language) { - $statusCode = 200; - if (!empty($email) && !empty($password) && $this->yellow->plugins->isExisting("edit")) { - $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"); - $status = $this->yellow->plugins->get("edit")->users->save($fileNameUser, $email, $password, $name, $language) ? "ok" : "error"; - if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - return $statusCode; - } - - // Update feature - public function updateFeature($feature) { - $statusCode = 200; - $path = $this->yellow->config->get("pluginDir"); - if (!empty($feature) && $this->yellow->plugins->isExisting("update")) { - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { - if (preg_match("/^install-(.*?)\./", basename($entry), $matches)) { - if (strtoloweru($matches[1])==strtoloweru($feature)) { - $statusCode = $this->yellow->plugins->get("update")->updateSoftwareArchive($entry); - break; - } - } - } - } - return $statusCode; - } - - // Update content - public function updateContent($language, $name, $location) { - $statusCode = 200; - if ($language!="en") { - $titleOld = "Title: ".$this->yellow->text->getText("install{$name}Title", "en"); - $titleNew = "Title: ".$this->yellow->text->getText("install{$name}Title", $language); - $textOld = strreplaceu("\\n", "\n", $this->yellow->text->getText("install{$name}Text", "en")); - $textNew = strreplaceu("\\n", "\n", $this->yellow->text->getText("install{$name}Text", $language)); - $fileName = $this->yellow->lookup->findFileFromLocation($location); - $fileData = strreplaceu("\r\n", "\n", $this->yellow->toolbox->readFile($fileName)); - $fileData = strreplaceu($titleOld, $titleNew, $fileData); - $fileData = strreplaceu($textOld, $textNew, $fileData); - if (!$this->yellow->toolbox->createFile($fileName, $fileData)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - } - return $statusCode; - } - - // Update config - public function updateConfig($config) { - $statusCode = 200; - $fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile"); - if (!$this->yellow->config->save($fileNameConfig, $config)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileNameConfig'!"); - } - return $statusCode; - } - - // Remove install - public function removeInstall() { - $statusCode = 200; - if (function_exists("opcache_reset")) opcache_reset(); - $path = $this->yellow->config->get("pluginDir"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { - if (preg_match("/^install-(.*?)\./", basename($entry), $matches)) { - if (!$this->yellow->toolbox->deleteFile($entry)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't delete file '$entry'!"); - } - } - } - $path = $this->yellow->config->get("pluginDir")."install.php"; - if ($statusCode==200 && !$this->yellow->toolbox->deleteFile($path)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't delete file '$path'!"); - } - if ($statusCode==200) unset($this->yellow->plugins->plugins["install"]); - return $statusCode; - } - - // Check web server rewrite - public function checkServerRewrite($scheme, $address, $base, $location, $fileName) { - $curlHandle = curl_init(); - $location = $this->yellow->config->get("assetLocation").$this->yellow->config->get("theme").".css"; - $url = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - curl_setopt($curlHandle, CURLOPT_URL, $url); - curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowCore/".YellowCore::VERSION).")"; - curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); - $rawData = curl_exec($curlHandle); - $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); - curl_close($curlHandle); - return !empty($rawData) && $statusCode==200; - } - - // Check web server read/write access - public function checkServerAccess() { - $fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile"); - return $this->yellow->config->save($fileNameConfig, array()); - } - - // Return language data, detect browser languages - public function getLanguageData($languagesDefault) { - $data = array(); - if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) { - foreach (preg_split("/\s*,\s*/", $_SERVER["HTTP_ACCEPT_LANGUAGE"]) as $string) { - list($language) = explode(";", $string); - if (!empty($language)) $data[$language] = ""; - } - } - foreach (preg_split("/\s*,\s*/", $languagesDefault) as $language) { - if (!empty($language)) $data[$language] = ""; - } - return $data; - } - - // Return configuration data, detect server URL - public function getConfigData() { - $data = array(); - foreach ($_REQUEST as $key=>$value) { - if (!$this->yellow->config->isExisting($key)) continue; - $data[$key] = trim($value); - } - $data["timezone"] = $this->yellow->toolbox->getTimezone(); - $data["staticUrl"] = $this->yellow->toolbox->getServerUrl(); - if ($this->yellow->isCommandLine()) $data["staticUrl"] = getenv("URL"); - return $data; - } - - // Return raw data for install page - public function getRawDataInstall() { - $language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->text->getLanguages(), $this->yellow->config->get("language")); - $fileName = strreplaceu("(.*)", "install", $this->yellow->config->get("configDir").$this->yellow->config->get("newFile")); - $rawData = $this->yellow->toolbox->readFile($fileName); - if (empty($rawData)) { - $this->yellow->text->setLanguage($language); - $rawData = "---\nTitle:".$this->yellow->text->get("installTitle")."\nLanguage:$language\nNavigation:navigation\n---\n"; - $rawData .= "<form class=\"install-form\" action=\"".$this->yellow->page->getLocation(true)."\" method=\"post\">\n"; - $rawData .= "<p><label for=\"name\">".$this->yellow->text->get("editSignupName")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"name\" id=\"name\" value=\"\"></p>\n"; - $rawData .= "<p><label for=\"email\">".$this->yellow->text->get("editSignupEmail")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"email\" id=\"email\" value=\"\"></p>\n"; - $rawData .= "<p><label for=\"password\">".$this->yellow->text->get("editSignupPassword")."</label><br /><input class=\"form-control\" type=\"password\" maxlength=\"64\" name=\"password\" id=\"password\" value=\"\"></p>\n"; - if (count($this->yellow->text->getLanguages())>1) { - $rawData .= "<p>"; - foreach ($this->yellow->text->getLanguages() as $language) { - $checked = $language==$this->yellow->text->language ? " checked=\"checked\"" : ""; - $rawData .= "<label for=\"$language\"><input type=\"radio\" name=\"language\" id=\"$language\" value=\"$language\"$checked> ".$this->yellow->text->getTextHtml("languageDescription", $language)."</label><br />"; - } - $rawData .= "</p>\n"; - } - if (count($this->getFeaturesInstall())>1) { - $rawData .= "<p>".$this->yellow->text->get("installFeature")."<p>"; - foreach ($this->getFeaturesInstall() as $feature) { - $checked = $feature=="website" ? " checked=\"checked\"" : ""; - $rawData .= "<label for=\"$feature\"><input type=\"radio\" name=\"feature\" id=\"$feature\" value=\"$feature\"$checked> ".ucfirst($feature)."</label><br />"; - } - $rawData .= "</p>\n"; - } - $rawData .= "<input class=\"btn\" type=\"submit\" value=\"".$this->yellow->text->get("editOkButton")."\" />\n"; - $rawData .= "<input type=\"hidden\" name=\"status\" value=\"install\" />\n"; - $rawData .= "</form>\n"; - } - return $rawData; - } - - // Return features for install page - public function getFeaturesInstall() { - $features = array("website"); - $path = $this->yellow->config->get("pluginDir"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false, false) as $entry) { - if (preg_match("/^install-(.*?)\./", $entry, $matches) && $matches[1]!="language") array_push($features, $matches[1]); - } - return $features; - } -} diff --git a/system/plugins/markdown.php b/system/plugins/markdown.php @@ -1,3868 +0,0 @@ -<?php -// Markdown plugin, https://github.com/datenstrom/yellow-plugins/tree/master/markdown -// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowMarkdown { - const VERSION = "0.8.1"; - public $yellow; //access to API - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - } - - // Handle page content in raw format - public function onParseContentRaw($page, $text) { - $markdown = new YellowMarkdownExtraParser($this->yellow, $page); - return $markdown->transform($text); - } -} - -// PHP Markdown Lib -// Copyright (c) 2004-2018 Michel Fortin -// <https://michelf.ca/> -// All rights reserved. -// -// Original Markdown -// Copyright (c) 2004-2006 John Gruber -// <https://daringfireball.net/> -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// * Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// -// * Neither the name "Markdown" nor the names of its contributors may -// be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// This software is provided by the copyright holders and contributors "as -// is" and any express or implied warranties, including, but not limited -// to, the implied warranties of merchantability and fitness for a -// particular purpose are disclaimed. In no event shall the copyright owner -// or contributors be liable for any direct, indirect, incidental, special, -// exemplary, or consequential damages (including, but not limited to, -// procurement of substitute goods or services; loss of use, data, or -// profits; or business interruption) however caused and on any theory of -// liability, whether in contract, strict liability, or tort (including -// negligence or otherwise) arising in any way out of the use of this -// software, even if advised of the possibility of such damage. - -class MarkdownParser { - /** - * Define the package version - * @var string - */ - const MARKDOWNLIB_VERSION = "1.8.0"; - - /** - * Simple function interface - Initialize the parser and return the result - * of its transform method. This will work fine for derived classes too. - * - * @api - * - * @param string $text - * @return string - */ - public static function defaultTransform($text) { - // Take parser class on which this function was called. - $parser_class = \get_called_class(); - - // Try to take parser from the static parser list - static $parser_list; - $parser =& $parser_list[$parser_class]; - - // Create the parser it not already set - if (!$parser) { - $parser = new $parser_class; - } - - // Transform text using parser. - return $parser->transform($text); - } - - /** - * Configuration variables - */ - - /** - * Change to ">" for HTML output. - * @var string - */ - public $empty_element_suffix = " />"; - - /** - * The width of indentation of the output markup - * @var int - */ - public $tab_width = 4; - - /** - * Change to `true` to disallow markup or entities. - * @var boolean - */ - public $no_markup = false; - public $no_entities = false; - - - /** - * Change to `true` to enable line breaks on \n without two trailling spaces - * @var boolean - */ - public $hard_wrap = false; - - /** - * Predefined URLs and titles for reference links and images. - * @var array - */ - public $predef_urls = array(); - public $predef_titles = array(); - - /** - * Optional filter function for URLs - * @var callable - */ - public $url_filter_func = null; - - /** - * Optional header id="" generation callback function. - * @var callable - */ - public $header_id_func = null; - - /** - * Optional function for converting code block content to HTML - * @var callable - */ - public $code_block_content_func = null; - - /** - * Optional function for converting code span content to HTML. - * @var callable - */ - public $code_span_content_func = null; - - /** - * Class attribute to toggle "enhanced ordered list" behaviour - * setting this to true will allow ordered lists to start from the index - * number that is defined first. - * - * For example: - * 2. List item two - * 3. List item three - * - * Becomes: - * <ol start="2"> - * <li>List item two</li> - * <li>List item three</li> - * </ol> - * - * @var bool - */ - public $enhanced_ordered_list = false; - - /** - * Parser implementation - */ - - /** - * Regex to match balanced [brackets]. - * Needed to insert a maximum bracked depth while converting to PHP. - * @var int - */ - protected $nested_brackets_depth = 6; - protected $nested_brackets_re; - - protected $nested_url_parenthesis_depth = 4; - protected $nested_url_parenthesis_re; - - /** - * Table of hash values for escaped characters: - * @var string - */ - protected $escape_chars = '\`*_{}[]()>#+-.!'; - protected $escape_chars_re; - - /** - * Constructor function. Initialize appropriate member variables. - * @return void - */ - public function __construct() { - $this->_initDetab(); - $this->prepareItalicsAndBold(); - - $this->nested_brackets_re = - str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). - str_repeat('\])*', $this->nested_brackets_depth); - - $this->nested_url_parenthesis_re = - str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). - str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); - - $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; - - // Sort document, block, and span gamut in ascendent priority order. - asort($this->document_gamut); - asort($this->block_gamut); - asort($this->span_gamut); - } - - - /** - * Internal hashes used during transformation. - * @var array - */ - protected $urls = array(); - protected $titles = array(); - protected $html_hashes = array(); - - /** - * Status flag to avoid invalid nesting. - * @var boolean - */ - protected $in_anchor = false; - - /** - * Status flag to avoid invalid nesting. - * @var boolean - */ - protected $in_emphasis_processing = false; - - /** - * Called before the transformation process starts to setup parser states. - * @return void - */ - protected function setup() { - // Clear global hashes. - $this->urls = $this->predef_urls; - $this->titles = $this->predef_titles; - $this->html_hashes = array(); - $this->in_anchor = false; - $this->in_emphasis_processing = false; - } - - /** - * Called after the transformation process to clear any variable which may - * be taking up memory unnecessarly. - * @return void - */ - protected function teardown() { - $this->urls = array(); - $this->titles = array(); - $this->html_hashes = array(); - } - - /** - * Main function. Performs some preprocessing on the input text and pass - * it through the document gamut. - * - * @api - * - * @param string $text - * @return string - */ - public function transform($text) { - $this->setup(); - - # Remove UTF-8 BOM and marker character in input, if present. - $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); - - # Standardize line endings: - # DOS to Unix and Mac to Unix - $text = preg_replace('{\r\n?}', "\n", $text); - - # Make sure $text ends with a couple of newlines: - $text .= "\n\n"; - - # Convert all tabs to spaces. - $text = $this->detab($text); - - # Turn block-level HTML blocks into hash entries - $text = $this->hashHTMLBlocks($text); - - # Strip any lines consisting only of spaces and tabs. - # This makes subsequent regexen easier to write, because we can - # match consecutive blank lines with /\n+/ instead of something - # contorted like /[ ]*\n+/ . - $text = preg_replace('/^[ ]+$/m', '', $text); - - # Run document gamut methods. - foreach ($this->document_gamut as $method => $priority) { - $text = $this->$method($text); - } - - $this->teardown(); - - return $text . "\n"; - } - - /** - * Define the document gamut - * @var array - */ - protected $document_gamut = array( - // Strip link definitions, store in hashes. - "stripLinkDefinitions" => 20, - "runBasicBlockGamut" => 30, - ); - - /** - * Strips link definitions from text, stores the URLs and titles in - * hash references - * @param string $text - * @return string - */ - protected function stripLinkDefinitions($text) { - - $less_than_tab = $this->tab_width - 1; - - // Link defs are in the form: ^[id]: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 - [ ]* - \n? # maybe *one* newline - [ ]* - (?: - <(.+?)> # url = $2 - | - (\S+?) # url = $3 - ) - [ ]* - \n? # maybe one newline - [ ]* - (?: - (?<=\s) # lookbehind for whitespace - ["(] - (.*?) # title = $4 - [")] - [ ]* - )? # title is optional - (?:\n+|\Z) - }xm', - array($this, '_stripLinkDefinitions_callback'), - $text - ); - return $text; - } - - /** - * The callback to strip link definitions - * @param array $matches - * @return string - */ - protected function _stripLinkDefinitions_callback($matches) { - $link_id = strtolower($matches[1]); - $url = $matches[2] == '' ? $matches[3] : $matches[2]; - $this->urls[$link_id] = $url; - $this->titles[$link_id] =& $matches[4]; - return ''; // String that will replace the block - } - - /** - * Hashify HTML blocks - * @param string $text - * @return string - */ - protected function hashHTMLBlocks($text) { - if ($this->no_markup) { - return $text; - } - - $less_than_tab = $this->tab_width - 1; - - /** - * Hashify HTML blocks: - * - * We only want to do this for block-level HTML tags, such as headers, - * lists, and tables. That's because we still want to wrap <p>s around - * "paragraphs" that are wrapped in non-block-level tags, such as - * anchors, phrase emphasis, and spans. The list of tags we're looking - * for is hard-coded: - * - * * List "a" is made of tags which can be both inline or block-level. - * These will be treated block-level when the start tag is alone on - * its line, otherwise they're not matched here and will be taken as - * inline later. - * * List "b" is made of tags which are always block-level; - */ - $block_tags_a_re = 'ins|del'; - $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. - 'script|noscript|style|form|fieldset|iframe|math|svg|'. - 'article|section|nav|aside|hgroup|header|footer|'. - 'figure'; - - // Regular expression for the content of a block tag. - $nested_tags_level = 4; - $attr = ' - (?> # optional tag attributes - \s # starts with whitespace - (?> - [^>"/]+ # text outside quotes - | - /+(?!>) # slash not followed by ">" - | - "[^"]*" # text inside double quotes (tolerate ">") - | - \'[^\']*\' # text inside single quotes (tolerate ">") - )* - )? - '; - $content = - str_repeat(' - (?> - [^<]+ # content without tag - | - <\2 # nested opening tag - '.$attr.' # attributes - (?> - /> - | - >', $nested_tags_level). // end of opening tag - '.*?'. // last level nested tag content - str_repeat(' - </\2\s*> # closing nested tag - ) - | - <(?!/\2\s*> # other tags with a different name - ) - )*', - $nested_tags_level); - $content2 = str_replace('\2', '\3', $content); - - /** - * First, look for nested blocks, e.g.: - * <div> - * <div> - * tags for inner block must be indented. - * </div> - * </div> - * - * The outermost tags must start at the left margin for this to match, - * and the inner nested divs must be indented. - * We need to do this before the next, more liberal match, because the - * next match will start at the first `<div>` and stop at the - * first `</div>`. - */ - $text = preg_replace_callback('{(?> - (?> - (?<=\n) # Starting on its own line - | # or - \A\n? # the at beginning of the doc - ) - ( # save in $1 - - # Match from `\n<tag>` to `</tag>\n`, handling nested tags - # in between. - - [ ]{0,'.$less_than_tab.'} - <('.$block_tags_b_re.')# start tag = $2 - '.$attr.'> # attributes followed by > and \n - '.$content.' # content, support nesting - </\2> # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special version for tags of group a. - - [ ]{0,'.$less_than_tab.'} - <('.$block_tags_a_re.')# start tag = $3 - '.$attr.'>[ ]*\n # attributes followed by > - '.$content2.' # content, support nesting - </\3> # the matching end tag - [ ]* # trailing spaces/tabs - (?=\n+|\Z) # followed by a newline or end of document - - | # Special case just for <hr />. It was easier to make a special - # case than to make the other regex more complicated. - - [ ]{0,'.$less_than_tab.'} - <(hr) # start tag = $2 - '.$attr.' # attributes - /?> # the matching end tag - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - | # Special case for standalone HTML comments: - - [ ]{0,'.$less_than_tab.'} - (?s: - <!-- .*? --> - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - | # PHP and ASP-style processor instructions (<? and <%) - - [ ]{0,'.$less_than_tab.'} - (?s: - <([?%]) # $2 - .*? - \2> - ) - [ ]* - (?=\n{2,}|\Z) # followed by a blank line or end of document - - ) - )}Sxmi', - array($this, '_hashHTMLBlocks_callback'), - $text - ); - - return $text; - } - - /** - * The callback for hashing HTML blocks - * @param string $matches - * @return string - */ - protected function _hashHTMLBlocks_callback($matches) { - $text = $matches[1]; - $key = $this->hashBlock($text); - return "\n\n$key\n\n"; - } - - /** - * Called whenever a tag must be hashed when a function insert an atomic - * element in the text stream. Passing $text to through this function gives - * a unique text-token which will be reverted back when calling unhash. - * - * The $boundary argument specify what character should be used to surround - * the token. By convension, "B" is used for block elements that needs not - * to be wrapped into paragraph tags at the end, ":" is used for elements - * that are word separators and "X" is used in the general case. - * - * @param string $text - * @param string $boundary - * @return string - */ - protected function hashPart($text, $boundary = 'X') { - // Swap back any tag hash found in $text so we do not have to `unhash` - // multiple times at the end. - $text = $this->unhash($text); - - // Then hash the block. - static $i = 0; - $key = "$boundary\x1A" . ++$i . $boundary; - $this->html_hashes[$key] = $text; - return $key; // String that will replace the tag. - } - - /** - * Shortcut function for hashPart with block-level boundaries. - * @param string $text - * @return string - */ - protected function hashBlock($text) { - return $this->hashPart($text, 'B'); - } - - /** - * Define the block gamut - these are all the transformations that form - * block-level tags like paragraphs, headers, and list items. - * @var array - */ - protected $block_gamut = array( - "doHeaders" => 10, - "doHorizontalRules" => 20, - "doLists" => 40, - "doCodeBlocks" => 50, - "doBlockQuotes" => 60, - ); - - /** - * Run block gamut tranformations. - * - * We need to escape raw HTML in Markdown source before doing anything - * else. This need to be done for each block, and not only at the - * begining in the Markdown function since hashed blocks can be part of - * list items and could have been indented. Indented blocks would have - * been seen as a code block in a previous pass of hashHTMLBlocks. - * - * @param string $text - * @return string - */ - protected function runBlockGamut($text) { - $text = $this->hashHTMLBlocks($text); - return $this->runBasicBlockGamut($text); - } - - /** - * Run block gamut tranformations, without hashing HTML blocks. This is - * useful when HTML blocks are known to be already hashed, like in the first - * whole-document pass. - * - * @param string $text - * @return string - */ - protected function runBasicBlockGamut($text) { - - foreach ($this->block_gamut as $method => $priority) { - $text = $this->$method($text); - } - - // Finally form paragraph and restore hashed blocks. - $text = $this->formParagraphs($text); - - return $text; - } - - /** - * Convert horizontal rules - * @param string $text - * @return string - */ - protected function doHorizontalRules($text) { - return preg_replace( - '{ - ^[ ]{0,3} # Leading space - ([-*_]) # $1: First marker - (?> # Repeated marker group - [ ]{0,2} # Zero, one, or two spaces. - \1 # Marker character - ){2,} # Group repeated at least twice - [ ]* # Tailing spaces - $ # End of line. - }mx', - "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n", - $text - ); - } - - /** - * These are all the transformations that occur *within* block-level - * tags like paragraphs, headers, and list items. - * @var array - */ - protected $span_gamut = array( - // Process character escapes, code spans, and inline HTML - // in one shot. - "parseSpan" => -30, - // Process anchor and image tags. Images must come first, - // because ![foo][f] looks like an anchor. - "doImages" => 10, - "doAnchors" => 20, - // Make links out of things like `<https://example.com/>` - // Must come after doAnchors, because you can use < and > - // delimiters in inline links like [this](<url>). - "doAutoLinks" => 30, - "encodeAmpsAndAngles" => 40, - "doItalicsAndBold" => 50, - "doHardBreaks" => 60, - ); - - /** - * Run span gamut transformations - * @param string $text - * @return string - */ - protected function runSpanGamut($text) { - foreach ($this->span_gamut as $method => $priority) { - $text = $this->$method($text); - } - - return $text; - } - - /** - * Do hard breaks - * @param string $text - * @return string - */ - protected function doHardBreaks($text) { - if ($this->hard_wrap) { - return preg_replace_callback('/ *\n/', - array($this, '_doHardBreaks_callback'), $text); - } else { - return preg_replace_callback('/ {2,}\n/', - array($this, '_doHardBreaks_callback'), $text); - } - } - - /** - * Trigger part hashing for the hard break (callback method) - * @param array $matches - * @return string - */ - protected function _doHardBreaks_callback($matches) { - return $this->hashPart("<br$this->empty_element_suffix\n"); - } - - /** - * Turn Markdown link shortcuts into XHTML <a> tags. - * @param string $text - * @return string - */ - protected function doAnchors($text) { - if ($this->in_anchor) { - return $text; - } - $this->in_anchor = true; - - // First, handle reference-style links: [link text] [id] - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ('.$this->nested_brackets_re.') # link text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - ) - }xs', - array($this, '_doAnchors_reference_callback'), $text); - - // Next, inline-style links: [link text](url "optional title") - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ('.$this->nested_brackets_re.') # link text = $2 - \] - \( # literal paren - [ \n]* - (?: - <(.+?)> # href = $3 - | - ('.$this->nested_url_parenthesis_re.') # href = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # Title = $7 - \6 # matching quote - [ \n]* # ignore any spaces/tabs between closing quote and ) - )? # title is optional - \) - ) - }xs', - array($this, '_doAnchors_inline_callback'), $text); - - // Last, handle reference-style shortcuts: [link text] - // These must come last in case you've also got [link text][1] - // or [link text](/foo) - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ([^\[\]]+) # link text = $2; can\'t contain [ or ] - \] - ) - }xs', - array($this, '_doAnchors_reference_callback'), $text); - - $this->in_anchor = false; - return $text; - } - - /** - * Callback method to parse referenced anchors - * @param string $matches - * @return string - */ - protected function _doAnchors_reference_callback($matches) { - $whole_match = $matches[1]; - $link_text = $matches[2]; - $link_id =& $matches[3]; - - if ($link_id == "") { - // for shortcut links like [this][] or [this]. - $link_id = $link_text; - } - - // lower-case and turn embedded newlines into spaces - $link_id = strtolower($link_id); - $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); - - if (isset($this->urls[$link_id])) { - $url = $this->urls[$link_id]; - $url = $this->encodeURLAttribute($url); - - $result = "<a href=\"$url\""; - if ( isset( $this->titles[$link_id] ) ) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text</a>"; - $result = $this->hashPart($result); - } else { - $result = $whole_match; - } - return $result; - } - - /** - * Callback method to parse inline anchors - * @param string $matches - * @return string - */ - protected function _doAnchors_inline_callback($matches) { - $whole_match = $matches[1]; - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - // If the URL was of the form <s p a c e s> it got caught by the HTML - // tag parser and hashed. Need to reverse the process before using - // the URL. - $unhashed = $this->unhash($url); - if ($unhashed != $url) - $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); - - $url = $this->encodeURLAttribute($url); - - $result = "<a href=\"$url\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text</a>"; - - return $this->hashPart($result); - } - - /** - * Turn Markdown image shortcuts into <img> tags. - * @param string $text - * @return string - */ - protected function doImages($text) { - // First, handle reference-style labeled images: ![alt text][id] - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - ('.$this->nested_brackets_re.') # alt text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - - ) - }xs', - array($this, '_doImages_reference_callback'), $text); - - // Next, handle inline images: ![alt text](url "optional title") - // Don't forget: encode * and _ - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - ('.$this->nested_brackets_re.') # alt text = $2 - \] - \s? # One optional whitespace character - \( # literal paren - [ \n]* - (?: - <(\S*)> # src url = $3 - | - ('.$this->nested_url_parenthesis_re.') # src url = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # title = $7 - \6 # matching quote - [ \n]* - )? # title is optional - \) - ) - }xs', - array($this, '_doImages_inline_callback'), $text); - - return $text; - } - - /** - * Callback to parse references image tags - * @param array $matches - * @return string - */ - protected function _doImages_reference_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower($matches[3]); - - if ($link_id == "") { - $link_id = strtolower($alt_text); // for shortcut links like ![this][]. - } - - $alt_text = $this->encodeAttribute($alt_text); - if (isset($this->urls[$link_id])) { - $url = $this->encodeURLAttribute($this->urls[$link_id]); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($this->titles[$link_id])) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - $result .= $this->empty_element_suffix; - $result = $this->hashPart($result); - } else { - // If there's no such link ID, leave intact: - $result = $whole_match; - } - - return $result; - } - - /** - * Callback to parse inline image tags - * @param array $matches - * @return string - */ - protected function _doImages_inline_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeURLAttribute($url); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; // $title already quoted - } - $result .= $this->empty_element_suffix; - - return $this->hashPart($result); - } - - /** - * Parse Markdown heading elements to HTML - * @param string $text - * @return string - */ - protected function doHeaders($text) { - /** - * Setext-style headers: - * Header 1 - * ======== - * - * Header 2 - * -------- - */ - $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', - array($this, '_doHeaders_callback_setext'), $text); - - /** - * atx-style headers: - * # Header 1 - * ## Header 2 - * ## Header 2 with closing hashes ## - * ... - * ###### Header 6 - */ - $text = preg_replace_callback('{ - ^(\#{1,6}) # $1 = string of #\'s - [ ]* - (.+?) # $2 = Header text - [ ]* - \#* # optional closing #\'s (not counted) - \n+ - }xm', - array($this, '_doHeaders_callback_atx'), $text); - - return $text; - } - - /** - * Setext header parsing callback - * @param array $matches - * @return string - */ - protected function _doHeaders_callback_setext($matches) { - // Terrible hack to check we haven't found an empty list item. - if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) { - return $matches[0]; - } - - $level = $matches[2]{0} == '=' ? 1 : 2; - - // ID attribute generation - $idAtt = $this->_generateIdFromHeaderValue($matches[1]); - - $block = "<h$level$idAtt>".$this->runSpanGamut($matches[1])."</h$level>"; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - /** - * ATX header parsing callback - * @param array $matches - * @return string - */ - protected function _doHeaders_callback_atx($matches) { - // ID attribute generation - $idAtt = $this->_generateIdFromHeaderValue($matches[2]); - - $level = strlen($matches[1]); - $block = "<h$level$idAtt>".$this->runSpanGamut($matches[2])."</h$level>"; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - /** - * If a header_id_func property is set, we can use it to automatically - * generate an id attribute. - * - * This method returns a string in the form id="foo", or an empty string - * otherwise. - * @param string $headerValue - * @return string - */ - protected function _generateIdFromHeaderValue($headerValue) { - if (!is_callable($this->header_id_func)) { - return ""; - } - - $idValue = call_user_func($this->header_id_func, $headerValue); - if (!$idValue) { - return ""; - } - - return ' id="' . $this->encodeAttribute($idValue) . '"'; - } - - /** - * Form HTML ordered (numbered) and unordered (bulleted) lists. - * @param string $text - * @return string - */ - protected function doLists($text) { - $less_than_tab = $this->tab_width - 1; - - // Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - - $markers_relist = array( - $marker_ul_re => $marker_ol_re, - $marker_ol_re => $marker_ul_re, - ); - - foreach ($markers_relist as $marker_re => $other_marker_re) { - // Re-usable pattern to match any entirel ul or ol list: - $whole_list_re = ' - ( # $1 = whole list - ( # $2 - ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces - ('.$marker_re.') # $4 = first list item marker - [ ]+ - ) - (?s:.+?) - ( # $5 - \z - | - \n{2,} - (?=\S) - (?! # Negative lookahead for another list item marker - [ ]* - '.$marker_re.'[ ]+ - ) - | - (?= # Lookahead for another kind of list - \n - \3 # Must have the same indentation - '.$other_marker_re.'[ ]+ - ) - ) - ) - '; // mx - - // We use a different prefix before nested lists than top-level lists. - //See extended comment in _ProcessListItems(). - - if ($this->list_level) { - $text = preg_replace_callback('{ - ^ - '.$whole_list_re.' - }mx', - array($this, '_doLists_callback'), $text); - } else { - $text = preg_replace_callback('{ - (?:(?<=\n)\n|\A\n?) # Must eat the newline - '.$whole_list_re.' - }mx', - array($this, '_doLists_callback'), $text); - } - } - - return $text; - } - - /** - * List parsing callback - * @param array $matches - * @return string - */ - protected function _doLists_callback($matches) { - // Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; - $marker_ol_start_re = '[0-9]+'; - - $list = $matches[1]; - $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; - - $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); - - $list .= "\n"; - $result = $this->processListItems($list, $marker_any_re); - - $ol_start = 1; - if ($this->enhanced_ordered_list) { - // Get the start number for ordered list. - if ($list_type == 'ol') { - $ol_start_array = array(); - $ol_start_check = preg_match("/$marker_ol_start_re/", $matches[4], $ol_start_array); - if ($ol_start_check){ - $ol_start = $ol_start_array[0]; - } - } - } - - if ($ol_start > 1 && $list_type == 'ol'){ - $result = $this->hashBlock("<$list_type start=\"$ol_start\">\n" . $result . "</$list_type>"); - } else { - $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>"); - } - return "\n". $result ."\n\n"; - } - - /** - * Nesting tracker for list levels - * @var integer - */ - protected $list_level = 0; - - /** - * Process the contents of a single ordered or unordered list, splitting it - * into individual list items. - * @param string $list_str - * @param string $marker_any_re - * @return string - */ - protected function processListItems($list_str, $marker_any_re) { - /** - * The $this->list_level global keeps track of when we're inside a list. - * Each time we enter a list, we increment it; when we leave a list, - * we decrement. If it's zero, we're not in a list anymore. - * - * We do this because when we're not inside a list, we want to treat - * something like this: - * - * I recommend upgrading to version - * 8. Oops, now this line is treated - * as a sub-list. - * - * As a single paragraph, despite the fact that the second line starts - * with a digit-period-space sequence. - * - * Whereas when we're inside a list (or sub-list), that line will be - * treated as the start of a sub-list. What a kludge, huh? This is - * an aspect of Markdown's syntax that's hard to parse perfectly - * without resorting to mind-reading. Perhaps the solution is to - * change the syntax rules such that sub-lists must start with a - * starting cardinal number; e.g. "1." or "a.". - */ - $this->list_level++; - - // Trim trailing blank lines: - $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); - - $list_str = preg_replace_callback('{ - (\n)? # leading line = $1 - (^[ ]*) # leading whitespace = $2 - ('.$marker_any_re.' # list marker and space = $3 - (?:[ ]+|(?=\n)) # space only required if item is not empty - ) - ((?s:.*?)) # list item text = $4 - (?:(\n+(?=\n))|\n) # tailing blank line = $5 - (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) - }xm', - array($this, '_processListItems_callback'), $list_str); - - $this->list_level--; - return $list_str; - } - - /** - * List item parsing callback - * @param array $matches - * @return string - */ - protected function _processListItems_callback($matches) { - $item = $matches[4]; - $leading_line =& $matches[1]; - $leading_space =& $matches[2]; - $marker_space = $matches[3]; - $tailing_blank_line =& $matches[5]; - - if ($leading_line || $tailing_blank_line || - preg_match('/\n{2,}/', $item)) - { - // Replace marker with the appropriate whitespace indentation - $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; - $item = $this->runBlockGamut($this->outdent($item)."\n"); - } else { - // Recursion for sub-lists: - $item = $this->doLists($this->outdent($item)); - $item = $this->formParagraphs($item, false); - } - - return "<li>" . $item . "</li>\n"; - } - - /** - * Process Markdown `<pre><code>` blocks. - * @param string $text - * @return string - */ - protected function doCodeBlocks($text) { - $text = preg_replace_callback('{ - (?:\n\n|\A\n?) - ( # $1 = the code block -- one or more lines, starting with a space/tab - (?> - [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces - .*\n+ - )+ - ) - ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc - }xm', - array($this, '_doCodeBlocks_callback'), $text); - - return $text; - } - - /** - * Code block parsing callback - * @param array $matches - * @return string - */ - protected function _doCodeBlocks_callback($matches) { - $codeblock = $matches[1]; - - $codeblock = $this->outdent($codeblock); - if ($this->code_block_content_func) { - $codeblock = call_user_func($this->code_block_content_func, $codeblock, ""); - } else { - $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); - } - - # trim leading newlines and trailing newlines - $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock); - - $codeblock = "<pre><code>$codeblock\n</code></pre>"; - return "\n\n" . $this->hashBlock($codeblock) . "\n\n"; - } - - /** - * Create a code span markup for $code. Called from handleSpanToken. - * @param string $code - * @return string - */ - protected function makeCodeSpan($code) { - if ($this->code_span_content_func) { - $code = call_user_func($this->code_span_content_func, $code); - } else { - $code = htmlspecialchars(trim($code), ENT_NOQUOTES); - } - return $this->hashPart("<code>$code</code>"); - } - - /** - * Define the emphasis operators with their regex matches - * @var array - */ - protected $em_relist = array( - '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?![\.,:;]?\s)', - '*' => '(?<![\s*])\*(?!\*)', - '_' => '(?<![\s_])_(?!_)', - ); - - /** - * Define the strong operators with their regex matches - * @var array - */ - protected $strong_relist = array( - '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?![\.,:;]?\s)', - '**' => '(?<![\s*])\*\*(?!\*)', - '__' => '(?<![\s_])__(?!_)', - ); - - /** - * Define the emphasis + strong operators with their regex matches - * @var array - */ - protected $em_strong_relist = array( - '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?![\.,:;]?\s)', - '***' => '(?<![\s*])\*\*\*(?!\*)', - '___' => '(?<![\s_])___(?!_)', - ); - - /** - * Container for prepared regular expressions - * @var array - */ - protected $em_strong_prepared_relist; - - /** - * Prepare regular expressions for searching emphasis tokens in any - * context. - * @return void - */ - protected function prepareItalicsAndBold() { - foreach ($this->em_relist as $em => $em_re) { - foreach ($this->strong_relist as $strong => $strong_re) { - // Construct list of allowed token expressions. - $token_relist = array(); - if (isset($this->em_strong_relist["$em$strong"])) { - $token_relist[] = $this->em_strong_relist["$em$strong"]; - } - $token_relist[] = $em_re; - $token_relist[] = $strong_re; - - // Construct master expression from list. - $token_re = '{(' . implode('|', $token_relist) . ')}'; - $this->em_strong_prepared_relist["$em$strong"] = $token_re; - } - } - } - - /** - * Convert Markdown italics (emphasis) and bold (strong) to HTML - * @param string $text - * @return string - */ - protected function doItalicsAndBold($text) { - if ($this->in_emphasis_processing) { - return $text; // avoid reentrency - } - $this->in_emphasis_processing = true; - - $token_stack = array(''); - $text_stack = array(''); - $em = ''; - $strong = ''; - $tree_char_em = false; - - while (1) { - // Get prepared regular expression for seraching emphasis tokens - // in current context. - $token_re = $this->em_strong_prepared_relist["$em$strong"]; - - // Each loop iteration search for the next emphasis token. - // Each token is then passed to handleSpanToken. - $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); - $text_stack[0] .= $parts[0]; - $token =& $parts[1]; - $text =& $parts[2]; - - if (empty($token)) { - // Reached end of text span: empty stack without emitting. - // any more emphasis. - while ($token_stack[0]) { - $text_stack[1] .= array_shift($token_stack); - $text_stack[0] .= array_shift($text_stack); - } - break; - } - - $token_len = strlen($token); - if ($tree_char_em) { - // Reached closing marker while inside a three-char emphasis. - if ($token_len == 3) { - // Three-char closing marker, close em and strong. - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<strong><em>$span</em></strong>"; - $text_stack[0] .= $this->hashPart($span); - $em = ''; - $strong = ''; - } else { - // Other closing marker: close one em or strong and - // change current token state to match the other - $token_stack[0] = str_repeat($token{0}, 3-$token_len); - $tag = $token_len == 2 ? "strong" : "em"; - $span = $text_stack[0]; - $span = $this->runSpanGamut($span); - $span = "<$tag>$span</$tag>"; - $text_stack[0] = $this->hashPart($span); - $$tag = ''; // $$tag stands for $em or $strong - } - $tree_char_em = false; - } else if ($token_len == 3) { - if ($em) { - // Reached closing marker for both em and strong. - // Closing strong marker: - for ($i = 0; $i < 2; ++$i) { - $shifted_token = array_shift($token_stack); - $tag = strlen($shifted_token) == 2 ? "strong" : "em"; - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<$tag>$span</$tag>"; - $text_stack[0] .= $this->hashPart($span); - $$tag = ''; // $$tag stands for $em or $strong - } - } else { - // Reached opening three-char emphasis marker. Push on token - // stack; will be handled by the special condition above. - $em = $token{0}; - $strong = "$em$em"; - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $tree_char_em = true; - } - } else if ($token_len == 2) { - if ($strong) { - // Unwind any dangling emphasis marker: - if (strlen($token_stack[0]) == 1) { - $text_stack[1] .= array_shift($token_stack); - $text_stack[0] .= array_shift($text_stack); - $em = ''; - } - // Closing strong marker: - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<strong>$span</strong>"; - $text_stack[0] .= $this->hashPart($span); - $strong = ''; - } else { - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $strong = $token; - } - } else { - // Here $token_len == 1 - if ($em) { - if (strlen($token_stack[0]) == 1) { - // Closing emphasis marker: - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<em>$span</em>"; - $text_stack[0] .= $this->hashPart($span); - $em = ''; - } else { - $text_stack[0] .= $token; - } - } else { - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $em = $token; - } - } - } - $this->in_emphasis_processing = false; - return $text_stack[0]; - } - - /** - * Parse Markdown blockquotes to HTML - * @param string $text - * @return string - */ - protected function doBlockQuotes($text) { - $text = preg_replace_callback('/ - ( # Wrap whole match in $1 - (?> - ^[ ]*>[ ]? # ">" at the start of a line - .+\n # rest of the first line - (.+\n)* # subsequent consecutive lines - \n* # blanks - )+ - ) - /xm', - array($this, '_doBlockQuotes_callback'), $text); - - return $text; - } - - /** - * Blockquote parsing callback - * @param array $matches - * @return string - */ - protected function _doBlockQuotes_callback($matches) { - $bq = $matches[1]; - // trim one level of quoting - trim whitespace-only lines - $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); - $bq = $this->runBlockGamut($bq); // recurse - - $bq = preg_replace('/^/m', " ", $bq); - // These leading spaces cause problem with <pre> content, - // so we need to fix that: - $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx', - array($this, '_doBlockQuotes_callback2'), $bq); - - return "\n" . $this->hashBlock("<blockquote>\n$bq\n</blockquote>") . "\n\n"; - } - - /** - * Blockquote parsing callback - * @param array $matches - * @return string - */ - protected function _doBlockQuotes_callback2($matches) { - $pre = $matches[1]; - $pre = preg_replace('/^ /m', '', $pre); - return $pre; - } - - /** - * Parse paragraphs - * - * @param string $text String to process in paragraphs - * @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags - * @return string - */ - protected function formParagraphs($text, $wrap_in_p = true) { - // Strip leading and trailing lines: - $text = preg_replace('/\A\n+|\n+\z/', '', $text); - - $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); - - // Wrap <p> tags and unhashify HTML blocks - foreach ($grafs as $key => $value) { - if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { - // Is a paragraph. - $value = $this->runSpanGamut($value); - if ($wrap_in_p) { - $value = preg_replace('/^([ ]*)/', "<p>", $value); - $value .= "</p>"; - } - $grafs[$key] = $this->unhash($value); - } else { - // Is a block. - // Modify elements of @grafs in-place... - $graf = $value; - $block = $this->html_hashes[$graf]; - $graf = $block; -// if (preg_match('{ -// \A -// ( # $1 = <div> tag -// <div \s+ -// [^>]* -// \b -// markdown\s*=\s* ([\'"]) # $2 = attr quote char -// 1 -// \2 -// [^>]* -// > -// ) -// ( # $3 = contents -// .* -// ) -// (</div>) # $4 = closing tag -// \z -// }xs', $block, $matches)) -// { -// list(, $div_open, , $div_content, $div_close) = $matches; -// -// // We can't call Markdown(), because that resets the hash; -// // that initialization code should be pulled into its own sub, though. -// $div_content = $this->hashHTMLBlocks($div_content); -// -// // Run document gamut methods on the content. -// foreach ($this->document_gamut as $method => $priority) { -// $div_content = $this->$method($div_content); -// } -// -// $div_open = preg_replace( -// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); -// -// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; -// } - $grafs[$key] = $graf; - } - } - - return implode("\n\n", $grafs); - } - - /** - * Encode text for a double-quoted HTML attribute. This function - * is *not* suitable for attributes enclosed in single quotes. - * @param string $text - * @return string - */ - protected function encodeAttribute($text) { - $text = $this->encodeAmpsAndAngles($text); - $text = str_replace('"', '&quot;', $text); - return $text; - } - - /** - * Encode text for a double-quoted HTML attribute containing a URL, - * applying the URL filter if set. Also generates the textual - * representation for the URL (removing mailto: or tel:) storing it in $text. - * This function is *not* suitable for attributes enclosed in single quotes. - * - * @param string $url - * @param string &$text Passed by reference - * @return string URL - */ - protected function encodeURLAttribute($url, &$text = null) { - if ($this->url_filter_func) { - $url = call_user_func($this->url_filter_func, $url); - } - - if (preg_match('{^mailto:}i', $url)) { - $url = $this->encodeEntityObfuscatedAttribute($url, $text, 7); - } else if (preg_match('{^tel:}i', $url)) { - $url = $this->encodeAttribute($url); - $text = substr($url, 4); - } else { - $url = $this->encodeAttribute($url); - $text = $url; - } - - return $url; - } - - /** - * Smart processing for ampersands and angle brackets that need to - * be encoded. Valid character entities are left alone unless the - * no-entities mode is set. - * @param string $text - * @return string - */ - protected function encodeAmpsAndAngles($text) { - if ($this->no_entities) { - $text = str_replace('&', '&amp;', $text); - } else { - // Ampersand-encoding based entirely on Nat Irons's Amputator - // MT plugin: <http://bumppo.net/projects/amputator/> - $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', - '&amp;', $text); - } - // Encode remaining <'s - $text = str_replace('<', '&lt;', $text); - - return $text; - } - - /** - * Parse Markdown automatic links to anchor HTML tags - * @param string $text - * @return string - */ - protected function doAutoLinks($text) { - $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i', - array($this, '_doAutoLinks_url_callback'), $text); - - // Email addresses: <address@domain.foo> - $text = preg_replace_callback('{ - < - (?:mailto:)? - ( - (?: - [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ - | - ".*?" - ) - \@ - (?: - [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ - | - \[[\d.a-fA-F:]+\] # IPv4 & IPv6 - ) - ) - > - }xi', - array($this, '_doAutoLinks_email_callback'), $text); - - return $text; - } - - /** - * Parse URL callback - * @param array $matches - * @return string - */ - protected function _doAutoLinks_url_callback($matches) { - $url = $this->encodeURLAttribute($matches[1], $text); - $link = "<a href=\"$url\">$text</a>"; - return $this->hashPart($link); - } - - /** - * Parse email address callback - * @param array $matches - * @return string - */ - protected function _doAutoLinks_email_callback($matches) { - $addr = $matches[1]; - $url = $this->encodeURLAttribute("mailto:$addr", $text); - $link = "<a href=\"$url\">$text</a>"; - return $this->hashPart($link); - } - - /** - * Input: some text to obfuscate, e.g. "mailto:foo@example.com" - * - * Output: the same text but with most characters encoded as either a - * decimal or hex entity, in the hopes of foiling most address - * harvesting spam bots. E.g.: - * - * &#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111; - * &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111; - * &#x6d; - * - * Note: the additional output $tail is assigned the same value as the - * ouput, minus the number of characters specified by $head_length. - * - * Based by a filter by Matthew Wickline, posted to BBEdit-Talk. - * With some optimizations by Milian Wolff. Forced encoding of HTML - * attribute special characters by Allan Odgaard. - * - * @param string $text - * @param string &$tail - * @param integer $head_length - * @return string - */ - protected function encodeEntityObfuscatedAttribute($text, &$tail = null, $head_length = 0) { - if ($text == "") { - return $tail = ""; - } - - $chars = preg_split('/(?<!^)(?!$)/', $text); - $seed = (int)abs(crc32($text) / strlen($text)); // Deterministic seed. - - foreach ($chars as $key => $char) { - $ord = ord($char); - // Ignore non-ascii chars. - if ($ord < 128) { - $r = ($seed * (1 + $key)) % 100; // Pseudo-random function. - // roughly 10% raw, 45% hex, 45% dec - // '@' *must* be encoded. I insist. - // '"' and '>' have to be encoded inside the attribute - if ($r > 90 && strpos('@"&>', $char) === false) { - /* do nothing */ - } else if ($r < 45) { - $chars[$key] = '&#x'.dechex($ord).';'; - } else { - $chars[$key] = '&#'.$ord.';'; - } - } - } - - $text = implode('', $chars); - $tail = $head_length ? implode('', array_slice($chars, $head_length)) : $text; - - return $text; - } - - /** - * Take the string $str and parse it into tokens, hashing embeded HTML, - * escaped characters and handling code spans. - * @param string $str - * @return string - */ - protected function parseSpan($str) { - $output = ''; - - $span_re = '{ - ( - \\\\'.$this->escape_chars_re.' - | - (?<![`\\\\]) - `+ # code span marker - '.( $this->no_markup ? '' : ' - | - <!-- .*? --> # comment - | - <\?.*?\?> | <%.*?%> # processing instruction - | - <[!$]?[-a-zA-Z0-9:_]+ # regular tags - (?> - \s - (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* - )? - > - | - <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag - | - </[-a-zA-Z0-9:_]+\s*> # closing tag - ').' - ) - }xs'; - - while (1) { - // Each loop iteration seach for either the next tag, the next - // openning code span marker, or the next escaped character. - // Each token is then passed to handleSpanToken. - $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); - - // Create token from text preceding tag. - if ($parts[0] != "") { - $output .= $parts[0]; - } - - // Check if we reach the end. - if (isset($parts[1])) { - $output .= $this->handleSpanToken($parts[1], $parts[2]); - $str = $parts[2]; - } else { - break; - } - } - - return $output; - } - - /** - * Handle $token provided by parseSpan by determining its nature and - * returning the corresponding value that should replace it. - * @param string $token - * @param string &$str - * @return string - */ - protected function handleSpanToken($token, &$str) { - switch ($token{0}) { - case "\\": - return $this->hashPart("&#". ord($token{1}). ";"); - case "`": - // Search for end marker in remaining text. - if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', - $str, $matches)) - { - $str = $matches[2]; - $codespan = $this->makeCodeSpan($matches[1]); - return $this->hashPart($codespan); - } - return $token; // Return as text since no ending marker found. - default: - return $this->hashPart($token); - } - } - - /** - * Remove one level of line-leading tabs or spaces - * @param string $text - * @return string - */ - protected function outdent($text) { - return preg_replace('/^(\t|[ ]{1,' . $this->tab_width . '})/m', '', $text); - } - - - /** - * String length function for detab. `_initDetab` will create a function to - * handle UTF-8 if the default function does not exist. - * @var string - */ - protected $utf8_strlen = 'mb_strlen'; - - /** - * Replace tabs with the appropriate amount of spaces. - * - * For each line we separate the line in blocks delemited by tab characters. - * Then we reconstruct every line by adding the appropriate number of space - * between each blocks. - * - * @param string $text - * @return string - */ - protected function detab($text) { - $text = preg_replace_callback('/^.*\t.*$/m', - array($this, '_detab_callback'), $text); - - return $text; - } - - /** - * Replace tabs callback - * @param string $matches - * @return string - */ - protected function _detab_callback($matches) { - $line = $matches[0]; - $strlen = $this->utf8_strlen; // strlen function for UTF-8. - - // Split in blocks. - $blocks = explode("\t", $line); - // Add each blocks to the line. - $line = $blocks[0]; - unset($blocks[0]); // Do not add first block twice. - foreach ($blocks as $block) { - // Calculate amount of space, insert spaces, insert block. - $amount = $this->tab_width - - $strlen($line, 'UTF-8') % $this->tab_width; - $line .= str_repeat(" ", $amount) . $block; - } - return $line; - } - - /** - * Check for the availability of the function in the `utf8_strlen` property - * (initially `mb_strlen`). If the function is not available, create a - * function that will loosely count the number of UTF-8 characters with a - * regular expression. - * @return void - */ - protected function _initDetab() { - - if (function_exists($this->utf8_strlen)) { - return; - } - - $this->utf8_strlen = function($text) { - return preg_match_all('/[\x00-\xBF]|[\xC0-\xFF][\x80-\xBF]*/', $text, $m); - }; - } - - /** - * Swap back in all the tags hashed by _HashHTMLBlocks. - * @param string $text - * @return string - */ - protected function unhash($text) { - return preg_replace_callback('/(.)\x1A[0-9]+\1/', - array($this, '_unhash_callback'), $text); - } - - /** - * Unhashing callback - * @param array $matches - * @return string - */ - protected function _unhash_callback($matches) { - return $this->html_hashes[$matches[0]]; - } -} - -class MarkdownExtraParser extends MarkdownParser { - /** - * Configuration variables - */ - - /** - * Prefix for footnote ids. - * @var string - */ - public $fn_id_prefix = ""; - - /** - * Optional title attribute for footnote links and backlinks. - * @var string - */ - public $fn_link_title = ""; - public $fn_backlink_title = ""; - - /** - * Optional class attribute for footnote links and backlinks. - * @var string - */ - public $fn_link_class = "footnote-ref"; - public $fn_backlink_class = "footnote-backref"; - - /** - * Content to be displayed within footnote backlinks. The default is '↩'; - * the U+FE0E on the end is a Unicode variant selector used to prevent iOS - * from displaying the arrow character as an emoji. - * @var string - */ - public $fn_backlink_html = '&#8617;&#xFE0E;'; - - /** - * Class name for table cell alignment (%% replaced left/center/right) - * For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center' - * If empty, the align attribute is used instead of a class name. - * @var string - */ - public $table_align_class_tmpl = ''; - - /** - * Optional class prefix for fenced code block. - * @var string - */ - public $code_class_prefix = ""; - - /** - * Class attribute for code blocks goes on the `code` tag; - * setting this to true will put attributes on the `pre` tag instead. - * @var boolean - */ - public $code_attr_on_pre = false; - - /** - * Predefined abbreviations. - * @var array - */ - public $predef_abbr = array(); - - /** - * Only convert atx-style headers if there's a space between the header and # - * @var boolean - */ - public $hashtag_protection = false; - - /** - * Parser implementation - */ - - /** - * Constructor function. Initialize the parser object. - * @return void - */ - public function __construct() { - // Add extra escapable characters before parent constructor - // initialize the table. - $this->escape_chars .= ':|'; - - // Insert extra document, block, and span transformations. - // Parent constructor will do the sorting. - $this->document_gamut += array( - "doFencedCodeBlocks" => 5, - "stripFootnotes" => 15, - "stripAbbreviations" => 25, - "appendFootnotes" => 50, - ); - $this->block_gamut += array( - "doFencedCodeBlocks" => 5, - "doTables" => 15, - "doDefLists" => 45, - ); - $this->span_gamut += array( - "doFootnotes" => 5, - "doAbbreviations" => 70, - ); - - $this->enhanced_ordered_list = true; - parent::__construct(); - } - - - /** - * Extra variables used during extra transformations. - * @var array - */ - protected $footnotes = array(); - protected $footnotes_ordered = array(); - protected $footnotes_ref_count = array(); - protected $footnotes_numbers = array(); - protected $abbr_desciptions = array(); - /** @var string */ - protected $abbr_word_re = ''; - - /** - * Give the current footnote number. - * @var integer - */ - protected $footnote_counter = 1; - - /** - * Setting up Extra-specific variables. - */ - protected function setup() { - parent::setup(); - - $this->footnotes = array(); - $this->footnotes_ordered = array(); - $this->footnotes_ref_count = array(); - $this->footnotes_numbers = array(); - $this->abbr_desciptions = array(); - $this->abbr_word_re = ''; - $this->footnote_counter = 1; - - foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { - if ($this->abbr_word_re) - $this->abbr_word_re .= '|'; - $this->abbr_word_re .= preg_quote($abbr_word); - $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); - } - } - - /** - * Clearing Extra-specific variables. - */ - protected function teardown() { - $this->footnotes = array(); - $this->footnotes_ordered = array(); - $this->footnotes_ref_count = array(); - $this->footnotes_numbers = array(); - $this->abbr_desciptions = array(); - $this->abbr_word_re = ''; - - parent::teardown(); - } - - - /** - * Extra attribute parser - */ - - /** - * Expression to use to catch attributes (includes the braces) - * @var string - */ - protected $id_class_attr_catch_re = '\{((?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}'; - - /** - * Expression to use when parsing in a context when no capture is desired - * @var string - */ - protected $id_class_attr_nocatch_re = '\{(?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}'; - - /** - * Parse attributes caught by the $this->id_class_attr_catch_re expression - * and return the HTML-formatted list of attributes. - * - * Currently supported attributes are .class and #id. - * - * In addition, this method also supports supplying a default Id value, - * which will be used to populate the id attribute in case it was not - * overridden. - * @param string $tag_name - * @param string $attr - * @param mixed $defaultIdValue - * @param array $classes - * @return string - */ - protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = array()) { - if (empty($attr) && !$defaultIdValue && empty($classes)) return ""; - - // Split on components - preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches); - $elements = $matches[0]; - - // Handle classes and IDs (only first ID taken into account) - $attributes = array(); - $id = false; - foreach ($elements as $element) { - if ($element{0} == '.') { - $classes[] = substr($element, 1); - } else if ($element{0} == '#') { - if ($id === false) $id = substr($element, 1); - } else if (strpos($element, '=') > 0) { - $parts = explode('=', $element, 2); - $attributes[] = $parts[0] . '="' . $parts[1] . '"'; - } - } - - if (!$id) $id = $defaultIdValue; - - // Compose attributes as string - $attr_str = ""; - if (!empty($id)) { - $attr_str .= ' id="'.$this->encodeAttribute($id) .'"'; - } - if (!empty($classes)) { - $attr_str .= ' class="'. implode(" ", $classes) . '"'; - } - if (!$this->no_markup && !empty($attributes)) { - $attr_str .= ' '.implode(" ", $attributes); - } - return $attr_str; - } - - /** - * Strips link definitions from text, stores the URLs and titles in - * hash references. - * @param string $text - * @return string - */ - protected function stripLinkDefinitions($text) { - $less_than_tab = $this->tab_width - 1; - - // Link defs are in the form: ^[id]: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 - [ ]* - \n? # maybe *one* newline - [ ]* - (?: - <(.+?)> # url = $2 - | - (\S+?) # url = $3 - ) - [ ]* - \n? # maybe one newline - [ ]* - (?: - (?<=\s) # lookbehind for whitespace - ["(] - (.*?) # title = $4 - [")] - [ ]* - )? # title is optional - (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr - (?:\n+|\Z) - }xm', - array($this, '_stripLinkDefinitions_callback'), - $text); - return $text; - } - - /** - * Strip link definition callback - * @param array $matches - * @return string - */ - protected function _stripLinkDefinitions_callback($matches) { - $link_id = strtolower($matches[1]); - $url = $matches[2] == '' ? $matches[3] : $matches[2]; - $this->urls[$link_id] = $url; - $this->titles[$link_id] =& $matches[4]; - $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]); - return ''; // String that will replace the block - } - - - /** - * HTML block parser - */ - - /** - * Tags that are always treated as block tags - * @var string - */ - protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure'; - - /** - * Tags treated as block tags only if the opening tag is alone on its line - * @var string - */ - protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; - - /** - * Tags where markdown="1" default to span mode: - * @var string - */ - protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; - - /** - * Tags which must not have their contents modified, no matter where - * they appear - * @var string - */ - protected $clean_tags_re = 'script|style|math|svg'; - - /** - * Tags that do not need to be closed. - * @var string - */ - protected $auto_close_tags_re = 'hr|img|param|source|track'; - - /** - * Hashify HTML Blocks and "clean tags". - * - * We only want to do this for block-level HTML tags, such as headers, - * lists, and tables. That's because we still want to wrap <p>s around - * "paragraphs" that are wrapped in non-block-level tags, such as anchors, - * phrase emphasis, and spans. The list of tags we're looking for is - * hard-coded. - * - * This works by calling _HashHTMLBlocks_InMarkdown, which then calls - * _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" - * attribute is found within a tag, _HashHTMLBlocks_InHTML calls back - * _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. - * These two functions are calling each other. It's recursive! - * @param string $text - * @return string - */ - protected function hashHTMLBlocks($text) { - if ($this->no_markup) { - return $text; - } - - // Call the HTML-in-Markdown hasher. - list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); - - return $text; - } - - /** - * Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. - * - * * $indent is the number of space to be ignored when checking for code - * blocks. This is important because if we don't take the indent into - * account, something like this (which looks right) won't work as expected: - * - * <div> - * <div markdown="1"> - * Hello World. <-- Is this a Markdown code block or text? - * </div> <-- Is this a Markdown code block or a real tag? - * <div> - * - * If you don't like this, just don't indent the tag on which - * you apply the markdown="1" attribute. - * - * * If $enclosing_tag_re is not empty, stops at the first unmatched closing - * tag with that name. Nested tags supported. - * - * * If $span is true, text inside must treated as span. So any double - * newline will be replaced by a single newline so that it does not create - * paragraphs. - * - * Returns an array of that form: ( processed text , remaining text ) - * - * @param string $text - * @param integer $indent - * @param string $enclosing_tag_re - * @param boolean $span - * @return array - */ - protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0, - $enclosing_tag_re = '', $span = false) - { - - if ($text === '') return array('', ''); - - // Regex to check for the presense of newlines around a block tag. - $newline_before_re = '/(?:^\n?|\n\n)*$/'; - $newline_after_re = - '{ - ^ # Start of text following the tag. - (?>[ ]*<!--.*?-->)? # Optional comment. - [ ]*\n # Must be followed by newline. - }xs'; - - // Regex to match any tag. - $block_tag_re = - '{ - ( # $2: Capture whole tag. - </? # Any opening or closing tag. - (?> # Tag name. - ' . $this->block_tags_re . ' | - ' . $this->context_block_tags_re . ' | - ' . $this->clean_tags_re . ' | - (?!\s)'.$enclosing_tag_re . ' - ) - (?: - (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. - (?> - ".*?" | # Double quotes (can contain `>`) - \'.*?\' | # Single quotes (can contain `>`) - .+? # Anything but quotes and `>`. - )*? - )? - > # End of tag. - | - <!-- .*? --> # HTML Comment - | - <\?.*?\?> | <%.*?%> # Processing instruction - | - <!\[CDATA\[.*?\]\]> # CData Block - ' . ( !$span ? ' # If not in span. - | - # Indented code block - (?: ^[ ]*\n | ^ | \n[ ]*\n ) - [ ]{' . ($indent + 4) . '}[^\n]* \n - (?> - (?: [ ]{' . ($indent + 4) . '}[^\n]* | [ ]* ) \n - )* - | - # Fenced code block marker - (?<= ^ | \n ) - [ ]{0,' . ($indent + 3) . '}(?:~{3,}|`{3,}) - [ ]* - (?: \.?[-_:a-zA-Z0-9]+ )? # standalone class name - [ ]* - (?: ' . $this->id_class_attr_nocatch_re . ' )? # extra attributes - [ ]* - (?= \n ) - ' : '' ) . ' # End (if not is span). - | - # Code span marker - # Note, this regex needs to go after backtick fenced - # code blocks but it should also be kept outside of the - # "if not in span" condition adding backticks to the parser - `+ - ) - }xs'; - - - $depth = 0; // Current depth inside the tag tree. - $parsed = ""; // Parsed text that will be returned. - - // Loop through every tag until we find the closing tag of the parent - // or loop until reaching the end of text if no parent tag specified. - do { - // Split the text using the first $tag_match pattern found. - // Text before pattern will be first in the array, text after - // pattern will be at the end, and between will be any catches made - // by the pattern. - $parts = preg_split($block_tag_re, $text, 2, - PREG_SPLIT_DELIM_CAPTURE); - - // If in Markdown span mode, add a empty-string span-level hash - // after each newline to prevent triggering any block element. - if ($span) { - $void = $this->hashPart("", ':'); - $newline = "\n$void"; - $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; - } - - $parsed .= $parts[0]; // Text before current tag. - - // If end of $text has been reached. Stop loop. - if (count($parts) < 3) { - $text = ""; - break; - } - - $tag = $parts[1]; // Tag to handle. - $text = $parts[2]; // Remaining text after current tag. - $tag_re = preg_quote($tag); // For use in a regular expression. - - // Check for: Fenced code block marker. - // Note: need to recheck the whole tag to disambiguate backtick - // fences from code spans - if (preg_match('{^\n?([ ]{0,' . ($indent + 3) . '})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+)?[ ]*(?:' . $this->id_class_attr_nocatch_re . ')?[ ]*\n?$}', $tag, $capture)) { - // Fenced code block marker: find matching end marker. - $fence_indent = strlen($capture[1]); // use captured indent in re - $fence_re = $capture[2]; // use captured fence in re - if (preg_match('{^(?>.*\n)*?[ ]{' . ($fence_indent) . '}' . $fence_re . '[ ]*(?:\n|$)}', $text, - $matches)) - { - // End marker found: pass text unchanged until marker. - $parsed .= $tag . $matches[0]; - $text = substr($text, strlen($matches[0])); - } - else { - // No end marker: just skip it. - $parsed .= $tag; - } - } - // Check for: Indented code block. - else if ($tag{0} == "\n" || $tag{0} == " ") { - // Indented code block: pass it unchanged, will be handled - // later. - $parsed .= $tag; - } - // Check for: Code span marker - // Note: need to check this after backtick fenced code blocks - else if ($tag{0} == "`") { - // Find corresponding end marker. - $tag_re = preg_quote($tag); - if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)' . $tag_re . '(?!`)}', - $text, $matches)) - { - // End marker found: pass text unchanged until marker. - $parsed .= $tag . $matches[0]; - $text = substr($text, strlen($matches[0])); - } - else { - // Unmatched marker: just skip it. - $parsed .= $tag; - } - } - // Check for: Opening Block level tag or - // Opening Context Block tag (like ins and del) - // used as a block tag (tag is alone on it's line). - else if (preg_match('{^<(?:' . $this->block_tags_re . ')\b}', $tag) || - ( preg_match('{^<(?:' . $this->context_block_tags_re . ')\b}', $tag) && - preg_match($newline_before_re, $parsed) && - preg_match($newline_after_re, $text) ) - ) - { - // Need to parse tag and following text using the HTML parser. - list($block_text, $text) = - $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); - - // Make sure it stays outside of any paragraph by adding newlines. - $parsed .= "\n\n$block_text\n\n"; - } - // Check for: Clean tag (like script, math) - // HTML Comments, processing instructions. - else if (preg_match('{^<(?:' . $this->clean_tags_re . ')\b}', $tag) || - $tag{1} == '!' || $tag{1} == '?') - { - // Need to parse tag and following text using the HTML parser. - // (don't check for markdown attribute) - list($block_text, $text) = - $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); - - $parsed .= $block_text; - } - // Check for: Tag with same name as enclosing tag. - else if ($enclosing_tag_re !== '' && - // Same name as enclosing tag. - preg_match('{^</?(?:' . $enclosing_tag_re . ')\b}', $tag)) - { - // Increase/decrease nested tag count. - if ($tag{1} == '/') $depth--; - else if ($tag{strlen($tag)-2} != '/') $depth++; - - if ($depth < 0) { - // Going out of parent element. Clean up and break so we - // return to the calling function. - $text = $tag . $text; - break; - } - - $parsed .= $tag; - } - else { - $parsed .= $tag; - } - } while ($depth >= 0); - - return array($parsed, $text); - } - - /** - * Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. - * - * * Calls $hash_method to convert any blocks. - * * Stops when the first opening tag closes. - * * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. - * (it is not inside clean tags) - * - * Returns an array of that form: ( processed text , remaining text ) - * @param string $text - * @param string $hash_method - * @param string $md_attr - * @return array - */ - protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { - if ($text === '') return array('', ''); - - // Regex to match `markdown` attribute inside of a tag. - $markdown_attr_re = ' - { - \s* # Eat whitespace before the `markdown` attribute - markdown - \s*=\s* - (?> - (["\']) # $1: quote delimiter - (.*?) # $2: attribute value - \1 # matching delimiter - | - ([^\s>]*) # $3: unquoted attribute value - ) - () # $4: make $3 always defined (avoid warnings) - }xs'; - - // Regex to match any tag. - $tag_re = '{ - ( # $2: Capture whole tag. - </? # Any opening or closing tag. - [\w:$]+ # Tag name. - (?: - (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. - (?> - ".*?" | # Double quotes (can contain `>`) - \'.*?\' | # Single quotes (can contain `>`) - .+? # Anything but quotes and `>`. - )*? - )? - > # End of tag. - | - <!-- .*? --> # HTML Comment - | - <\?.*?\?> | <%.*?%> # Processing instruction - | - <!\[CDATA\[.*?\]\]> # CData Block - ) - }xs'; - - $original_text = $text; // Save original text in case of faliure. - - $depth = 0; // Current depth inside the tag tree. - $block_text = ""; // Temporary text holder for current text. - $parsed = ""; // Parsed text that will be returned. - - // Get the name of the starting tag. - // (This pattern makes $base_tag_name_re safe without quoting.) - if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) - $base_tag_name_re = $matches[1]; - - // Loop through every tag until we find the corresponding closing tag. - do { - // Split the text using the first $tag_match pattern found. - // Text before pattern will be first in the array, text after - // pattern will be at the end, and between will be any catches made - // by the pattern. - $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); - - if (count($parts) < 3) { - // End of $text reached with unbalenced tag(s). - // In that case, we return original text unchanged and pass the - // first character as filtered to prevent an infinite loop in the - // parent function. - return array($original_text{0}, substr($original_text, 1)); - } - - $block_text .= $parts[0]; // Text before current tag. - $tag = $parts[1]; // Tag to handle. - $text = $parts[2]; // Remaining text after current tag. - - // Check for: Auto-close tag (like <hr/>) - // Comments and Processing Instructions. - if (preg_match('{^</?(?:' . $this->auto_close_tags_re . ')\b}', $tag) || - $tag{1} == '!' || $tag{1} == '?') - { - // Just add the tag to the block as if it was text. - $block_text .= $tag; - } - else { - // Increase/decrease nested tag count. Only do so if - // the tag's name match base tag's. - if (preg_match('{^</?' . $base_tag_name_re . '\b}', $tag)) { - if ($tag{1} == '/') $depth--; - else if ($tag{strlen($tag)-2} != '/') $depth++; - } - - // Check for `markdown="1"` attribute and handle it. - if ($md_attr && - preg_match($markdown_attr_re, $tag, $attr_m) && - preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3])) - { - // Remove `markdown` attribute from opening tag. - $tag = preg_replace($markdown_attr_re, '', $tag); - - // Check if text inside this tag must be parsed in span mode. - $this->mode = $attr_m[2] . $attr_m[3]; - $span_mode = $this->mode == 'span' || $this->mode != 'block' && - preg_match('{^<(?:' . $this->contain_span_tags_re . ')\b}', $tag); - - // Calculate indent before tag. - if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { - $strlen = $this->utf8_strlen; - $indent = $strlen($matches[1], 'UTF-8'); - } else { - $indent = 0; - } - - // End preceding block with this tag. - $block_text .= $tag; - $parsed .= $this->$hash_method($block_text); - - // Get enclosing tag name for the ParseMarkdown function. - // (This pattern makes $tag_name_re safe without quoting.) - preg_match('/^<([\w:$]*)\b/', $tag, $matches); - $tag_name_re = $matches[1]; - - // Parse the content using the HTML-in-Markdown parser. - list ($block_text, $text) - = $this->_hashHTMLBlocks_inMarkdown($text, $indent, - $tag_name_re, $span_mode); - - // Outdent markdown text. - if ($indent > 0) { - $block_text = preg_replace("/^[ ]{1,$indent}/m", "", - $block_text); - } - - // Append tag content to parsed text. - if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; - else $parsed .= "$block_text"; - - // Start over with a new block. - $block_text = ""; - } - else $block_text .= $tag; - } - - } while ($depth > 0); - - // Hash last block text that wasn't processed inside the loop. - $parsed .= $this->$hash_method($block_text); - - return array($parsed, $text); - } - - /** - * Called whenever a tag must be hashed when a function inserts a "clean" tag - * in $text, it passes through this function and is automaticaly escaped, - * blocking invalid nested overlap. - * @param string $text - * @return string - */ - protected function hashClean($text) { - return $this->hashPart($text, 'C'); - } - - /** - * Turn Markdown link shortcuts into XHTML <a> tags. - * @param string $text - * @return string - */ - protected function doAnchors($text) { - if ($this->in_anchor) { - return $text; - } - $this->in_anchor = true; - - // First, handle reference-style links: [link text] [id] - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - (' . $this->nested_brackets_re . ') # link text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - ) - }xs', - array($this, '_doAnchors_reference_callback'), $text); - - // Next, inline-style links: [link text](url "optional title") - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - (' . $this->nested_brackets_re . ') # link text = $2 - \] - \( # literal paren - [ \n]* - (?: - <(.+?)> # href = $3 - | - (' . $this->nested_url_parenthesis_re . ') # href = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # Title = $7 - \6 # matching quote - [ \n]* # ignore any spaces/tabs between closing quote and ) - )? # title is optional - \) - (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes - ) - }xs', - array($this, '_doAnchors_inline_callback'), $text); - - // Last, handle reference-style shortcuts: [link text] - // These must come last in case you've also got [link text][1] - // or [link text](/foo) - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - \[ - ([^\[\]]+) # link text = $2; can\'t contain [ or ] - \] - ) - }xs', - array($this, '_doAnchors_reference_callback'), $text); - - $this->in_anchor = false; - return $text; - } - - /** - * Callback for reference anchors - * @param array $matches - * @return string - */ - protected function _doAnchors_reference_callback($matches) { - $whole_match = $matches[1]; - $link_text = $matches[2]; - $link_id =& $matches[3]; - - if ($link_id == "") { - // for shortcut links like [this][] or [this]. - $link_id = $link_text; - } - - // lower-case and turn embedded newlines into spaces - $link_id = strtolower($link_id); - $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); - - if (isset($this->urls[$link_id])) { - $url = $this->urls[$link_id]; - $url = $this->encodeURLAttribute($url); - - $result = "<a href=\"$url\""; - if ( isset( $this->titles[$link_id] ) ) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - if (isset($this->ref_attr[$link_id])) - $result .= $this->ref_attr[$link_id]; - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text</a>"; - $result = $this->hashPart($result); - } - else { - $result = $whole_match; - } - return $result; - } - - /** - * Callback for inline anchors - * @param array $matches - * @return string - */ - protected function _doAnchors_inline_callback($matches) { - $whole_match = $matches[1]; - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); - - // if the URL was of the form <s p a c e s> it got caught by the HTML - // tag parser and hashed. Need to reverse the process before using the URL. - $unhashed = $this->unhash($url); - if ($unhashed != $url) - $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); - - $url = $this->encodeURLAttribute($url); - - $result = "<a href=\"$url\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - $result .= $attr; - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text</a>"; - - return $this->hashPart($result); - } - - /** - * Turn Markdown image shortcuts into <img> tags. - * @param string $text - * @return string - */ - protected function doImages($text) { - // First, handle reference-style labeled images: ![alt text][id] - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - (' . $this->nested_brackets_re . ') # alt text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - - ) - }xs', - array($this, '_doImages_reference_callback'), $text); - - // Next, handle inline images: ![alt text](url "optional title") - // Don't forget: encode * and _ - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - (' . $this->nested_brackets_re . ') # alt text = $2 - \] - \s? # One optional whitespace character - \( # literal paren - [ \n]* - (?: - <(\S*)> # src url = $3 - | - (' . $this->nested_url_parenthesis_re . ') # src url = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # title = $7 - \6 # matching quote - [ \n]* - )? # title is optional - \) - (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes - ) - }xs', - array($this, '_doImages_inline_callback'), $text); - - return $text; - } - - /** - * Callback for referenced images - * @param array $matches - * @return string - */ - protected function _doImages_reference_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower($matches[3]); - - if ($link_id == "") { - $link_id = strtolower($alt_text); // for shortcut links like ![this][]. - } - - $alt_text = $this->encodeAttribute($alt_text); - if (isset($this->urls[$link_id])) { - $url = $this->encodeURLAttribute($this->urls[$link_id]); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($this->titles[$link_id])) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - if (isset($this->ref_attr[$link_id])) - $result .= $this->ref_attr[$link_id]; - $result .= $this->empty_element_suffix; - $result = $this->hashPart($result); - } - else { - // If there's no such link ID, leave intact: - $result = $whole_match; - } - - return $result; - } - - /** - * Callback for inline images - * @param array $matches - * @return string - */ - protected function _doImages_inline_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); - - $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeURLAttribute($url); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; // $title already quoted - } - $result .= $attr; - $result .= $this->empty_element_suffix; - - return $this->hashPart($result); - } - - /** - * Process markdown headers. Redefined to add ID and class attribute support. - * @param string $text - * @return string - */ - protected function doHeaders($text) { - // Setext-style headers: - // Header 1 {#header1} - // ======== - // - // Header 2 {#header2 .class1 .class2} - // -------- - // - $text = preg_replace_callback( - '{ - (^.+?) # $1: Header text - (?:[ ]+ ' . $this->id_class_attr_catch_re . ' )? # $3 = id/class attributes - [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer - }mx', - array($this, '_doHeaders_callback_setext'), $text); - - // atx-style headers: - // # Header 1 {#header1} - // ## Header 2 {#header2} - // ## Header 2 with closing hashes ## {#header3.class1.class2} - // ... - // ###### Header 6 {.class2} - // - $text = preg_replace_callback('{ - ^(\#{1,6}) # $1 = string of #\'s - [ ]'.($this->hashtag_protection ? '+' : '*').' - (.+?) # $2 = Header text - [ ]* - \#* # optional closing #\'s (not counted) - (?:[ ]+ ' . $this->id_class_attr_catch_re . ' )? # $3 = id/class attributes - [ ]* - \n+ - }xm', - array($this, '_doHeaders_callback_atx'), $text); - - return $text; - } - - /** - * Callback for setext headers - * @param array $matches - * @return string - */ - protected function _doHeaders_callback_setext($matches) { - if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) { - return $matches[0]; - } - - $level = $matches[3]{0} == '=' ? 1 : 2; - - $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null; - - $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId); - $block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>"; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - /** - * Callback for atx headers - * @param array $matches - * @return string - */ - protected function _doHeaders_callback_atx($matches) { - $level = strlen($matches[1]); - - $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null; - $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId); - $block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>"; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - /** - * Form HTML tables. - * @param string $text - * @return string - */ - protected function doTables($text) { - $less_than_tab = $this->tab_width - 1; - // Find tables with leading pipe. - // - // | Header 1 | Header 2 - // | -------- | -------- - // | Cell 1 | Cell 2 - // | Cell 3 | Cell 4 - $text = preg_replace_callback(' - { - ^ # Start of a line - [ ]{0,' . $less_than_tab . '} # Allowed whitespace. - [|] # Optional leading pipe (present) - (.+) \n # $1: Header row (at least one pipe) - - [ ]{0,' . $less_than_tab . '} # Allowed whitespace. - [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline - - ( # $3: Cells - (?> - [ ]* # Allowed whitespace. - [|] .* \n # Row content. - )* - ) - (?=\n|\Z) # Stop at final double newline. - }xm', - array($this, '_doTable_leadingPipe_callback'), $text); - - // Find tables without leading pipe. - // - // Header 1 | Header 2 - // -------- | -------- - // Cell 1 | Cell 2 - // Cell 3 | Cell 4 - $text = preg_replace_callback(' - { - ^ # Start of a line - [ ]{0,' . $less_than_tab . '} # Allowed whitespace. - (\S.*[|].*) \n # $1: Header row (at least one pipe) - - [ ]{0,' . $less_than_tab . '} # Allowed whitespace. - ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline - - ( # $3: Cells - (?> - .* [|] .* \n # Row content - )* - ) - (?=\n|\Z) # Stop at final double newline. - }xm', - array($this, '_DoTable_callback'), $text); - - return $text; - } - - /** - * Callback for removing the leading pipe for each row - * @param array $matches - * @return string - */ - protected function _doTable_leadingPipe_callback($matches) { - $head = $matches[1]; - $underline = $matches[2]; - $content = $matches[3]; - - $content = preg_replace('/^ *[|]/m', '', $content); - - return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); - } - - /** - * Make the align attribute in a table - * @param string $alignname - * @return string - */ - protected function _doTable_makeAlignAttr($alignname) - { - if (empty($this->table_align_class_tmpl)) { - return " align=\"$alignname\""; - } - - $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl); - return " class=\"$classname\""; - } - - /** - * Calback for processing tables - * @param array $matches - * @return string - */ - protected function _doTable_callback($matches) { - $head = $matches[1]; - $underline = $matches[2]; - $content = $matches[3]; - - // Remove any tailing pipes for each line. - $head = preg_replace('/[|] *$/m', '', $head); - $underline = preg_replace('/[|] *$/m', '', $underline); - $content = preg_replace('/[|] *$/m', '', $content); - - // Reading alignement from header underline. - $separators = preg_split('/ *[|] */', $underline); - foreach ($separators as $n => $s) { - if (preg_match('/^ *-+: *$/', $s)) - $attr[$n] = $this->_doTable_makeAlignAttr('right'); - else if (preg_match('/^ *:-+: *$/', $s)) - $attr[$n] = $this->_doTable_makeAlignAttr('center'); - else if (preg_match('/^ *:-+ *$/', $s)) - $attr[$n] = $this->_doTable_makeAlignAttr('left'); - else - $attr[$n] = ''; - } - - // Parsing span elements, including code spans, character escapes, - // and inline HTML tags, so that pipes inside those gets ignored. - $head = $this->parseSpan($head); - $headers = preg_split('/ *[|] */', $head); - $col_count = count($headers); - $attr = array_pad($attr, $col_count, ''); - - // Write column headers. - $text = "<table>\n"; - $text .= "<thead>\n"; - $text .= "<tr>\n"; - foreach ($headers as $n => $header) - $text .= " <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n"; - $text .= "</tr>\n"; - $text .= "</thead>\n"; - - // Split content by row. - $rows = explode("\n", trim($content, "\n")); - - $text .= "<tbody>\n"; - foreach ($rows as $row) { - // Parsing span elements, including code spans, character escapes, - // and inline HTML tags, so that pipes inside those gets ignored. - $row = $this->parseSpan($row); - - // Split row by cell. - $row_cells = preg_split('/ *[|] */', $row, $col_count); - $row_cells = array_pad($row_cells, $col_count, ''); - - $text .= "<tr>\n"; - foreach ($row_cells as $n => $cell) - $text .= " <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n"; - $text .= "</tr>\n"; - } - $text .= "</tbody>\n"; - $text .= "</table>"; - - return $this->hashBlock($text) . "\n"; - } - - /** - * Form HTML definition lists. - * @param string $text - * @return string - */ - protected function doDefLists($text) { - $less_than_tab = $this->tab_width - 1; - - // Re-usable pattern to match any entire dl list: - $whole_list_re = '(?> - ( # $1 = whole list - ( # $2 - [ ]{0,' . $less_than_tab . '} - ((?>.*\S.*\n)+) # $3 = defined term - \n? - [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition - ) - (?s:.+?) - ( # $4 - \z - | - \n{2,} - (?=\S) - (?! # Negative lookahead for another term - [ ]{0,' . $less_than_tab . '} - (?: \S.*\n )+? # defined term - \n? - [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition - ) - (?! # Negative lookahead for another definition - [ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition - ) - ) - ) - )'; // mx - - $text = preg_replace_callback('{ - (?>\A\n?|(?<=\n\n)) - ' . $whole_list_re . ' - }mx', - array($this, '_doDefLists_callback'), $text); - - return $text; - } - - /** - * Callback for processing definition lists - * @param array $matches - * @return string - */ - protected function _doDefLists_callback($matches) { - // Re-usable patterns to match list item bullets and number markers: - $list = $matches[1]; - - // Turn double returns into triple returns, so that we can make a - // paragraph for the last item in a list, if necessary: - $result = trim($this->processDefListItems($list)); - $result = "<dl>\n" . $result . "\n</dl>"; - return $this->hashBlock($result) . "\n\n"; - } - - /** - * Process the contents of a single definition list, splitting it - * into individual term and definition list items. - * @param string $list_str - * @return string - */ - protected function processDefListItems($list_str) { - - $less_than_tab = $this->tab_width - 1; - - // Trim trailing blank lines: - $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); - - // Process definition terms. - $list_str = preg_replace_callback('{ - (?>\A\n?|\n\n+) # leading line - ( # definition terms = $1 - [ ]{0,' . $less_than_tab . '} # leading whitespace - (?!\:[ ]|[ ]) # negative lookahead for a definition - # mark (colon) or more whitespace. - (?> \S.* \n)+? # actual term (not whitespace). - ) - (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed - # with a definition mark. - }xm', - array($this, '_processDefListItems_callback_dt'), $list_str); - - // Process actual definitions. - $list_str = preg_replace_callback('{ - \n(\n+)? # leading line = $1 - ( # marker space = $2 - [ ]{0,' . $less_than_tab . '} # whitespace before colon - \:[ ]+ # definition mark (colon) - ) - ((?s:.+?)) # definition text = $3 - (?= \n+ # stop at next definition mark, - (?: # next term or end of text - [ ]{0,' . $less_than_tab . '} \:[ ] | - <dt> | \z - ) - ) - }xm', - array($this, '_processDefListItems_callback_dd'), $list_str); - - return $list_str; - } - - /** - * Callback for <dt> elements in definition lists - * @param array $matches - * @return string - */ - protected function _processDefListItems_callback_dt($matches) { - $terms = explode("\n", trim($matches[1])); - $text = ''; - foreach ($terms as $term) { - $term = $this->runSpanGamut(trim($term)); - $text .= "\n<dt>" . $term . "</dt>"; - } - return $text . "\n"; - } - - /** - * Callback for <dd> elements in definition lists - * @param array $matches - * @return string - */ - protected function _processDefListItems_callback_dd($matches) { - $leading_line = $matches[1]; - $marker_space = $matches[2]; - $def = $matches[3]; - - if ($leading_line || preg_match('/\n{2,}/', $def)) { - // Replace marker with the appropriate whitespace indentation - $def = str_repeat(' ', strlen($marker_space)) . $def; - $def = $this->runBlockGamut($this->outdent($def . "\n\n")); - $def = "\n". $def ."\n"; - } - else { - $def = rtrim($def); - $def = $this->runSpanGamut($this->outdent($def)); - } - - return "\n<dd>" . $def . "</dd>\n"; - } - - /** - * Adding the fenced code block syntax to regular Markdown: - * - * ~~~ - * Code block - * ~~~ - * - * @param string $text - * @return string - */ - protected function doFencedCodeBlocks($text) { - - $less_than_tab = $this->tab_width; - - $text = preg_replace_callback('{ - (?:\n|\A) - # 1: Opening marker - ( - (?:~{3,}|`{3,}) # 3 or more tildes/backticks. - ) - [ ]* - (?: - \.?([-_:a-zA-Z0-9]+) # 2: standalone class name - )? - [ ]* - (?: - ' . $this->id_class_attr_catch_re . ' # 3: Extra attributes - )? - [ ]* \n # Whitespace and newline following marker. - - # 4: Content - ( - (?> - (?!\1 [ ]* \n) # Not a closing marker. - .*\n+ - )+ - ) - - # Closing marker. - \1 [ ]* (?= \n ) - }xm', - array($this, '_doFencedCodeBlocks_callback'), $text); - - return $text; - } - - /** - * Callback to process fenced code blocks - * @param array $matches - * @return string - */ - protected function _doFencedCodeBlocks_callback($matches) { - $classname =& $matches[2]; - $attrs =& $matches[3]; - $codeblock = $matches[4]; - - if ($this->code_block_content_func) { - $codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname); - } else { - $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); - } - - $codeblock = preg_replace_callback('/^\n+/', - array($this, '_doFencedCodeBlocks_newlines'), $codeblock); - - $classes = array(); - if ($classname != "") { - if ($classname{0} == '.') - $classname = substr($classname, 1); - $classes[] = $this->code_class_prefix . $classname; - } - $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes); - $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; - $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; - $codeblock = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>"; - - return "\n\n".$this->hashBlock($codeblock)."\n\n"; - } - - /** - * Replace new lines in fenced code blocks - * @param array $matches - * @return string - */ - protected function _doFencedCodeBlocks_newlines($matches) { - return str_repeat("<br$this->empty_element_suffix", - strlen($matches[0])); - } - - /** - * Redefining emphasis markers so that emphasis by underscore does not - * work in the middle of a word. - * @var array - */ - protected $em_relist = array( - '' => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)', - '*' => '(?<![\s*])\*(?!\*)', - '_' => '(?<![\s_])_(?![a-zA-Z0-9_])', - ); - protected $strong_relist = array( - '' => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)', - '**' => '(?<![\s*])\*\*(?!\*)', - '__' => '(?<![\s_])__(?![a-zA-Z0-9_])', - ); - protected $em_strong_relist = array( - '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)', - '***' => '(?<![\s*])\*\*\*(?!\*)', - '___' => '(?<![\s_])___(?![a-zA-Z0-9_])', - ); - - /** - * Parse text into paragraphs - * @param string $text String to process in paragraphs - * @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags - * @return string HTML output - */ - protected function formParagraphs($text, $wrap_in_p = true) { - // Strip leading and trailing lines: - $text = preg_replace('/\A\n+|\n+\z/', '', $text); - - $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); - - // Wrap <p> tags and unhashify HTML blocks - foreach ($grafs as $key => $value) { - $value = trim($this->runSpanGamut($value)); - - // Check if this should be enclosed in a paragraph. - // Clean tag hashes & block tag hashes are left alone. - $is_p = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); - - if ($is_p) { - $value = "<p>$value</p>"; - } - $grafs[$key] = $value; - } - - // Join grafs in one text, then unhash HTML tags. - $text = implode("\n\n", $grafs); - - // Finish by removing any tag hashes still present in $text. - $text = $this->unhash($text); - - return $text; - } - - - /** - * Footnotes - Strips link definitions from text, stores the URLs and - * titles in hash references. - * @param string $text - * @return string - */ - protected function stripFootnotes($text) { - $less_than_tab = $this->tab_width - 1; - - // Link defs are in the form: [^id]: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,' . $less_than_tab . '}\[\^(.+?)\][ ]?: # note_id = $1 - [ ]* - \n? # maybe *one* newline - ( # text = $2 (no blank lines allowed) - (?: - .+ # actual text - | - \n # newlines but - (?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker. - (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed - # by non-indented content - )* - ) - }xm', - array($this, '_stripFootnotes_callback'), - $text); - return $text; - } - - /** - * Callback for stripping footnotes - * @param array $matches - * @return string - */ - protected function _stripFootnotes_callback($matches) { - $note_id = $this->fn_id_prefix . $matches[1]; - $this->footnotes[$note_id] = $this->outdent($matches[2]); - return ''; // String that will replace the block - } - - /** - * Replace footnote references in $text [^id] with a special text-token - * which will be replaced by the actual footnote marker in appendFootnotes. - * @param string $text - * @return string - */ - protected function doFootnotes($text) { - if (!$this->in_anchor) { - $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); - } - return $text; - } - - /** - * Append footnote list to text - * @param string $text - * @return string - */ - protected function appendFootnotes($text) { - $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', - array($this, '_appendFootnotes_callback'), $text); - - if (!empty($this->footnotes_ordered)) { - $text .= "\n\n"; - $text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n"; - $text .= "<hr" . $this->empty_element_suffix . "\n"; - $text .= "<ol>\n\n"; - - $attr = ""; - if ($this->fn_backlink_class != "") { - $class = $this->fn_backlink_class; - $class = $this->encodeAttribute($class); - $attr .= " class=\"$class\""; - } - if ($this->fn_backlink_title != "") { - $title = $this->fn_backlink_title; - $title = $this->encodeAttribute($title); - $attr .= " title=\"$title\""; - $attr .= " aria-label=\"$title\""; - } - $attr .= " role=\"doc-backlink\""; - $backlink_text = $this->fn_backlink_html; - $num = 0; - - while (!empty($this->footnotes_ordered)) { - $footnote = reset($this->footnotes_ordered); - $note_id = key($this->footnotes_ordered); - unset($this->footnotes_ordered[$note_id]); - $ref_count = $this->footnotes_ref_count[$note_id]; - unset($this->footnotes_ref_count[$note_id]); - unset($this->footnotes[$note_id]); - - $footnote .= "\n"; // Need to append newline before parsing. - $footnote = $this->runBlockGamut("$footnote\n"); - $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', - array($this, '_appendFootnotes_callback'), $footnote); - - $attr = str_replace("%%", ++$num, $attr); - $note_id = $this->encodeAttribute($note_id); - - // Prepare backlink, multiple backlinks if multiple references - $backlink = "<a href=\"#fnref:$note_id\"$attr>$backlink_text</a>"; - for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) { - $backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>$backlink_text</a>"; - } - // Add backlink to last paragraph; create new paragraph if needed. - if (preg_match('{</p>$}', $footnote)) { - $footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>"; - } else { - $footnote .= "\n\n<p>$backlink</p>"; - } - - $text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n"; - $text .= $footnote . "\n"; - $text .= "</li>\n\n"; - } - - $text .= "</ol>\n"; - $text .= "</div>"; - } - return $text; - } - - /** - * Callback for appending footnotes - * @param array $matches - * @return string - */ - protected function _appendFootnotes_callback($matches) { - $node_id = $this->fn_id_prefix . $matches[1]; - - // Create footnote marker only if it has a corresponding footnote *and* - // the footnote hasn't been used by another marker. - if (isset($this->footnotes[$node_id])) { - $num =& $this->footnotes_numbers[$node_id]; - if (!isset($num)) { - // Transfer footnote content to the ordered list and give it its - // number - $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; - $this->footnotes_ref_count[$node_id] = 1; - $num = $this->footnote_counter++; - $ref_count_mark = ''; - } else { - $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1; - } - - $attr = ""; - if ($this->fn_link_class != "") { - $class = $this->fn_link_class; - $class = $this->encodeAttribute($class); - $attr .= " class=\"$class\""; - } - if ($this->fn_link_title != "") { - $title = $this->fn_link_title; - $title = $this->encodeAttribute($title); - $attr .= " title=\"$title\""; - } - $attr .= " role=\"doc-noteref\""; - - $attr = str_replace("%%", $num, $attr); - $node_id = $this->encodeAttribute($node_id); - - return - "<sup id=\"fnref$ref_count_mark:$node_id\">". - "<a href=\"#fn:$node_id\"$attr>$num</a>". - "</sup>"; - } - - return "[^" . $matches[1] . "]"; - } - - - /** - * Abbreviations - strips abbreviations from text, stores titles in hash - * references. - * @param string $text - * @return string - */ - protected function stripAbbreviations($text) { - $less_than_tab = $this->tab_width - 1; - - // Link defs are in the form: [id]*: url "optional title" - $text = preg_replace_callback('{ - ^[ ]{0,' . $less_than_tab . '}\*\[(.+?)\][ ]?: # abbr_id = $1 - (.*) # text = $2 (no blank lines allowed) - }xm', - array($this, '_stripAbbreviations_callback'), - $text); - return $text; - } - - /** - * Callback for stripping abbreviations - * @param array $matches - * @return string - */ - protected function _stripAbbreviations_callback($matches) { - $abbr_word = $matches[1]; - $abbr_desc = $matches[2]; - if ($this->abbr_word_re) { - $this->abbr_word_re .= '|'; - } - $this->abbr_word_re .= preg_quote($abbr_word); - $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); - return ''; // String that will replace the block - } - - /** - * Find defined abbreviations in text and wrap them in <abbr> elements. - * @param string $text - * @return string - */ - protected function doAbbreviations($text) { - if ($this->abbr_word_re) { - // cannot use the /x modifier because abbr_word_re may - // contain significant spaces: - $text = preg_replace_callback('{' . - '(?<![\w\x1A])' . - '(?:' . $this->abbr_word_re . ')' . - '(?![\w\x1A])' . - '}', - array($this, '_doAbbreviations_callback'), $text); - } - return $text; - } - - /** - * Callback for processing abbreviations - * @param array $matches - * @return string - */ - protected function _doAbbreviations_callback($matches) { - $abbr = $matches[0]; - if (isset($this->abbr_desciptions[$abbr])) { - $desc = $this->abbr_desciptions[$abbr]; - if (empty($desc)) { - return $this->hashPart("<abbr>$abbr</abbr>"); - } else { - $desc = $this->encodeAttribute($desc); - return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>"); - } - } else { - return $matches[0]; - } - } -} - -// Markdown extra parser extensions -// Copyright (c) 2013-2019 Datenstrom - -class YellowMarkdownExtraParser extends MarkdownExtraParser { - public $yellow; //access to API - public $page; //access to page - public $idAttributes; //id attributes - - public function __construct($yellow, $page) { - $this->yellow = $yellow; - $this->page = $page; - $this->idAttributes = array(); - $this->no_markup = $page->safeMode; - $this->url_filter_func = function($url) use ($yellow, $page) { - return $yellow->lookup->normaliseLocation($url, $page->location, - $page->safeMode && $page->statusCode==200); - }; - parent::__construct(); - } - - // Handle links - 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("/\[(\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); - $text = preg_replace_callback("/((http|https|ftp):\/\/\S+[^\'\"\,\.\;\:\s]+)/", array(&$this, "_doAutoLinks_url_callback"), $text); - $text = preg_replace_callback("/([\w\+\-\.]+@[\w\-\.]+\.[\w]{2,4})/", array(&$this, "_doAutoLinks_email_callback"), $text); - return $text; - } - - // Handle shortcuts, block style - public function _doAutoLinks_shortcutBlock_callback($matches) { - $output = $this->page->parseContentShortcut($matches[1], trim($matches[2]), "block"); - return is_null($output) ? $matches[0] : $this->hashBlock($output); - } - - // Handle shortcuts, inline style - public function _doAutoLinks_shortcutInline_callback($matches) { - $output = $this->page->parseContentShortcut($matches[1], trim($matches[2]), "inline"); - return is_null($output) ? $matches[0] : $this->hashPart($output); - } - - // Handle shortcuts, comment style - public function _doAutoLinks_shortcutComment_callback($matches) { - $output = "<!--".htmlspecialchars($matches[1], ENT_NOQUOTES)."-->"; - return $this->hashBlock($output); - } - - // Handle shortcuts, symbol style - public function _doAutoLinks_shortcutSymbol_callback($matches) { - $output = $this->page->parseContentShortcut("", $matches[1], "symbol"); - return is_null($output) ? $matches[0] : $this->hashPart($output); - } - - // Handle fenced code blocks - public function _doFencedCodeBlocks_callback($matches) { - $text = $matches[4]; - $name = empty($matches[2]) ? "" : "$matches[2] $matches[3]"; - $output = $this->page->parseContentShortcut($name, $text, "code"); - if (is_null($output)) { - $attr = $this->doExtraAttributes("pre", ".$matches[2] $matches[3]"); - $output = "<pre$attr><code>".htmlspecialchars($text, ENT_NOQUOTES)."</code></pre>"; - } - return "\n\n".$this->hashBlock($output)."\n\n"; - } - - // Handle headers, text style - public function _doHeaders_callback_setext($matches) { - if ($matches[3]=="-" && preg_match('{^- }', $matches[1])) return $matches[0]; - $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); - $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; - return "\n".$this->hashBlock($output)."\n\n"; - } - - // Handle headers, atx style - public function _doHeaders_callback_atx($matches) { - $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); - $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; - return "\n".$this->hashBlock($output)."\n\n"; - } - - // Handle inline links - public function _doAnchors_inline_callback($matches) { - $url = $matches[3]=="" ? $matches[4] : $matches[3]; - $text = $matches[2]; - $title = $matches[7]; - $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); - $output = "<a href=\"".$this->encodeURLAttribute($url)."\""; - if (!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; - $output .= $attr; - $output .= ">".$this->runSpanGamut($text)."</a>"; - return $this->hashPart($output); - } - - // Handle inline images - public function _doImages_inline_callback($matches) { - $width = $height = 0; - $src = $matches[3]=="" ? $matches[4] : $matches[3]; - if (!preg_match("/^\w+:/", $src)) { - list($width, $height) = $this->yellow->toolbox->detectImageInformation($this->yellow->config->get("imageDir").$src); - $src = $this->yellow->config->get("serverBase").$this->yellow->config->get("imageLocation").$src; - } - $alt = $matches[2]; - $title = $matches[7]=="" ? $matches[2] : $matches[7]; - $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); - $output = "<img src=\"".$this->encodeURLAttribute($src)."\""; - if ($width && $height) $output .= " width=\"$width\" height=\"$height\""; - if (!empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\""; - if (!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; - $output .= $attr; - $output .= $this->empty_element_suffix; - return $this->hashPart($output); - } - - // Return unique id attribute - public function getIdAttribute($text) { - $text = $this->yellow->lookup->normaliseName($text, true, false, true); - $text = trim(preg_replace("/-+/", "-", $text), "-"); - if (is_null($this->idAttributes[$text])) { - $this->idAttributes[$text] = $text; - $attr = " id=\"$text\""; - } - return $attr; - } -} diff --git a/system/plugins/update.php b/system/plugins/update.php @@ -1,710 +0,0 @@ -<?php -// Update plugin, https://github.com/datenstrom/yellow-plugins/tree/master/update -// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowUpdate { - const VERSION = "0.8.1"; - const PRIORITY = "2"; - public $yellow; //access to API - public $updates; //number of updates - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - $this->yellow->config->setDefault("updatePluginsUrl", "https://github.com/datenstrom/yellow-plugins"); - $this->yellow->config->setDefault("updateThemesUrl", "https://github.com/datenstrom/yellow-themes"); - $this->yellow->config->setDefault("updateInformationFile", "update.ini"); - $this->yellow->config->setDefault("updateVersionFile", "version.ini"); - $this->yellow->config->setDefault("updateResourceFile", "resource.ini"); - } - - // Handle startup - public function onStartup($update) { - if ($update) { //TODO: remove later, converts old config - $fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile"); - if ($this->yellow->config->isExisting("parserSafeMode")) { - $this->yellow->config->save($fileNameConfig, array("safeMode" => $this->yellow->config->get("parserSafeMode"))); - } - if ($this->yellow->config->get("staticDir")=="cache/") { - $this->yellow->config->save($fileNameConfig, array("staticDir" => "public/")); - } - } - if ($update) { //TODO: remove later, converts old robots file - $fileNameRobots = $this->yellow->config->get("configDir")."robots.txt"; - $fileNameError = $this->yellow->config->get("configDir")."system-error.log"; - if (is_file($fileNameRobots)) { - if (!$this->yellow->toolbox->renameFile($fileNameRobots, "./robots.txt")) { - $fileDataError .= "ERROR moving file '$fileNameRobots'!\n"; - } - if (!empty($fileDataError)) { - $this->yellow->toolbox->createFile($fileNameError, $fileDataError); - } - } - } - if ($update) { //TODO: remove later, converts old Markdown extension - $fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile"); - $fileNameError = $this->yellow->config->get("configDir")."system-error.log"; - if ($this->yellow->config->get("contentDefaultFile")=="page.txt") { - $config = array("contentDefaultFile" => "page.md", "contentExtension" => ".md", - "errorFile" => "page-error-(.*).md", "newFile" => "page-new-(.*).md"); - $this->yellow->config->save($fileNameConfig, $config); - $path = $this->yellow->config->get("contentDir"); - foreach ($this->yellow->toolbox->getDirectoryEntriesRecursive($path, "/^.*\.txt$/", true, false) as $entry) { - if (!$this->yellow->toolbox->renameFile($entry, str_replace(".txt", ".md", $entry))) { - $fileDataError .= "ERROR renaming file '$entry'!\n"; - } - } - $path = $this->yellow->config->get("configDir"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.txt$/", true, false) as $entry) { - if (!$this->yellow->toolbox->renameFile($entry, str_replace(".txt", ".md", $entry))) { - $fileDataError .= "ERROR renaming file '$entry!'\n"; - } - } - if (!empty($fileDataError)) { - $this->yellow->toolbox->createFile($fileNameError, $fileDataError); - } - $_GET["clean-url"] = "system-updated"; - } - } - if ($update) { //TODO: remove later, updates shared pages - $fileNameError = $this->yellow->config->get("configDir")."system-error.log"; - $pathConfig = $this->yellow->config->get("configDir"); - $pathShared = $this->yellow->config->get("contentDir").$this->yellow->config->get("contentSharedDir"); - if (count($this->yellow->toolbox->getDirectoryEntries($pathConfig, "/.*/", false, false))>3) { - $regex = "/^page-error-(.*)\.md$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($pathConfig, $regex, true, false) as $entry) { - if (!$this->yellow->toolbox->deleteFile($entry, $this->yellow->config->get("trashDir"))) { - $fileDataError .= "ERROR deleting file '$entry'!\n"; - } - } - $regex = "/^page-new-(.*)\.md$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($pathConfig, $regex, true, false) as $entry) { - if (!$this->yellow->toolbox->renameFile($entry, str_replace($pathConfig, $pathShared, $entry), true)) { - $fileDataError .= "ERROR moving file '$entry'!\n"; - } - } - $fileNameHeader = $pathShared."header.md"; - if (!is_file($fileNameHeader) && $this->yellow->config->isExisting("tagline")) { - $fileDataHeader = "---\nTitle: Header\nStatus: hidden\n---\n".$this->yellow->config->get("tagline"); - if (!$this->yellow->toolbox->createFile($fileNameHeader, $fileDataHeader, true)) { - $fileDataError .= "ERROR writing file '$fileNameHeader'!\n"; - } - } - $fileNameFooter = $pathShared."footer.md"; - if (!is_file($fileNameFooter)) { - $fileDataFooter = "---\nTitle: Footer\nStatus: hidden\n---\n"; - $fileDataFooter .= $this->yellow->text->getText("InstallFooterText", $this->yellow->config->get("language")); - if (!$this->yellow->toolbox->createFile($fileNameFooter, $fileDataFooter, true)) { - $fileDataError .= "ERROR writing file '$fileNameFooter'!\n"; - } - } - $this->updateSoftwareMultiLanguage("shared-pages"); - if (!empty($fileDataError)) { - $this->yellow->toolbox->createFile($fileNameError, $fileDataError); - } - } - } - if ($update) { - $fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile"); - $fileData = $this->yellow->toolbox->readFile($fileNameConfig); - $configDefaults = new YellowDataCollection(); - $configDefaults->exchangeArray($this->yellow->config->configDefaults->getArrayCopy()); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !is_null($configDefaults[$matches[1]])) unset($configDefaults[$matches[1]]); - if (!empty($matches[1]) && $matches[1][0]!="#" && is_null($this->yellow->config->configDefaults[$matches[1]])) { - $fileDataNew .= "# $line"; - } else { - $fileDataNew .= $line; - } - } - unset($configDefaults["configFile"]); - foreach ($configDefaults as $key=>$value) { - $fileDataNew .= ucfirst($key).": $value\n"; - } - if ($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileNameConfig, $fileDataNew); - } - } - - // Handle request - public function onRequest($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->yellow->lookup->isContentFile($fileName) && $this->isSoftwarePending()) { - $statusCode = $this->processRequestPending($scheme, $address, $base, $location, $fileName); - } - return $statusCode; - } - - // Handle command - public function onCommand($args) { - $statusCode = 0; - if ($this->isSoftwarePending()) $statusCode = $this->processCommandPending(); - if ($statusCode==0) { - list($command) = $args; - switch ($command) { - case "clean": $statusCode = $this->processCommandClean($args); break; - case "install": $statusCode = $this->processCommandInstall($args); break; - case "uninstall": $statusCode = $this->processCommandUninstall($args); break; - case "update": $statusCode = $this->processCommandUpdate($args); break; - default: $statusCode = 0; break; - } - } - return $statusCode; - } - - // Handle command help - public function onCommandHelp() { - $help .= "install [feature]\n"; - $help .= "uninstall [feature]\n"; - $help .= "update [feature]\n"; - return $help; - } - - // Process command to clean downloads - public function processCommandClean($args) { - $statusCode = 0; - list($command, $path) = $args; - if ($path=="all") { - $path = $this->yellow->config->get("pluginDir"); - $regex = "/^.*\\".$this->yellow->config->get("downloadExtension")."$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, false) as $entry) { - if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; - } - if ($statusCode==500) echo "ERROR cleaning downloads: Can't delete files in directory '$path'!\n"; - } - return $statusCode; - } - - // Process command to install website features - public function processCommandInstall($args) { - list($command, $features) = $this->getCommandFeatures($args); - if (!empty($features)) { - $this->updates = 0; - list($statusCode, $data) = $this->getInstallInformation($features); - if ($statusCode==200) $statusCode = $this->downloadSoftware($data); - if ($statusCode==200) $statusCode = $this->updateSoftware(); - if ($statusCode>=400) echo "ERROR installing files: ".$this->yellow->page->get("pageError")."\n"; - echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; - echo ", $this->updates feature".($this->updates!=1 ? "s" : "")." installed\n"; - } else { - $statusCode = $this->showSoftware(); - } - return $statusCode; - } - - // Process command to uninstall website features - public function processCommandUninstall($args) { - list($command, $features) = $this->getCommandFeatures($args); - if (!empty($features)) { - $this->updates = 0; - list($statusCode, $data) = $this->getUninstallInformation($features, "YellowCore, YellowUpdate"); - if ($statusCode==200) $statusCode = $this->removeSoftware($data); - if ($statusCode>=400) echo "ERROR uninstalling files: ".$this->yellow->page->get("pageError")."\n"; - echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; - echo ", $this->updates feature".($this->updates!=1 ? "s" : "")." uninstalled\n"; - } else { - $statusCode = $this->showSoftware(); - } - return $statusCode; - } - - // Process command to update website - public function processCommandUpdate($args) { - list($command, $features, $force) = $this->getCommandFeatures($args); - list($statusCode, $data) = $this->getUpdateInformation($features, $force); - if ($statusCode!=200 || !empty($data)) { - $this->updates = 0; - if ($statusCode==200) $statusCode = $this->downloadSoftware($data); - if ($statusCode==200) $statusCode = $this->updateSoftware($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"; - } else { - echo "Your website is up to date\n"; - } - return $statusCode; - } - - // Process command to update website with pending software - public function processCommandPending() { - $statusCode = $this->updateSoftware(); - 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 update website with pending software - public function processRequestPending($scheme, $address, $base, $location, $fileName) { - $statusCode = $this->updateSoftware(); - if ($statusCode==200) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } - return $statusCode; - } - - // Return install information - public function getInstallInformation($features) { - $data = array(); - list($statusCodeCurrent, $dataCurrent) = $this->getSoftwareVersion(); - list($statusCodeLatest, $dataLatest) = $this->getSoftwareVersion(true, true); - $statusCode = max($statusCodeCurrent, $statusCodeLatest); - foreach ($features as $feature) { - $found = false; - foreach ($dataLatest as $key=>$value) { - if (strtoloweru($key)==strtoloweru($feature)) { - $data[$key] = $dataLatest[$key]; - $found = true; - break; - } - } - if (!$found) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't find feature '$feature'!"); - } - } - return array($statusCode, $data); - } - - // Return uninstall information - public function getUninstallInformation($features, $featuresProtected) { - $data = array(); - list($statusCodeCurrent, $dataCurrent) = $this->getSoftwareVersion(); - list($statusCodeLatest, $dataLatest) = $this->getSoftwareVersion(true, true); - list($statusCodeFiles, $dataFiles) = $this->getSoftwareFiles(); - $statusCode = max($statusCodeCurrent, $statusCodeLatest, $statusCodeFiles); - foreach ($features as $feature) { - $found = false; - foreach ($dataCurrent as $key=>$value) { - if (strtoloweru($key)==strtoloweru($feature) && !is_null($dataLatest[$key]) && !is_null($dataFiles[$key])) { - $data[$key] = $dataFiles[$key]; - $found = true; - break; - } - } - if (!$found) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't find feature '$feature'!"); - } - } - $protected = preg_split("/\s*,\s*/", $featuresProtected); - foreach ($data as $key=>$value) { - if (in_array($key, $protected)) unset($data[$key]); - } - return array($statusCode, $data); - } - - // Return update information - public function getUpdateInformation($features, $force) { - $data = array(); - list($statusCodeCurrent, $dataCurrent) = $this->getSoftwareVersion(); - list($statusCodeLatest, $dataLatest) = $this->getSoftwareVersion(true, true); - list($statusCodeModified, $dataModified) = $this->getSoftwareModified(); - $statusCode = max($statusCodeCurrent, $statusCodeLatest, $statusCodeModified); - if (empty($features)) { - foreach ($dataCurrent as $key=>$value) { - list($version) = explode(",", $dataLatest[$key]); - if (strnatcasecmp($dataCurrent[$key], $version)<0) $data[$key] = $dataLatest[$key]; - if (!is_null($dataModified[$key]) && !empty($version) && $force) $data[$key] = $dataLatest[$key]; - } - } else { - foreach ($features as $feature) { - $found = false; - foreach ($dataCurrent as $key=>$value) { - list($version) = explode(",", $dataLatest[$key]); - if (strtoloweru($key)==strtoloweru($feature) && !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 feature '$feature'!"); - } - } - } - if ($statusCode==200) { - foreach (array_merge($dataModified, $data) as $key=>$value) { - list($version) = explode(",", $value); - if (is_null($dataModified[$key]) || $force) { - echo "$key $version\n"; - } else { - echo "$key $version has been modified - Force update\n"; - } - } - } - return array($statusCode, $data); - } - - // Show software - public function showSoftware() { - list($statusCode, $dataLatest) = $this->getSoftwareVersion(true, true); - foreach ($dataLatest as $key=>$value) { - list($version, $url, $description) = explode(",", $value, 3); - echo "$key: $description\n"; - } - if ($statusCode!=200) echo "ERROR checking features: ".$this->yellow->page->get("pageError")."\n"; - return $statusCode; - } - - // Download software - public function downloadSoftware($data) { - $statusCode = 200; - $path = $this->yellow->config->get("pluginDir"); - $fileExtension = $this->yellow->config->get("downloadExtension"); - foreach ($data as $key=>$value) { - $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; - list($version, $url) = explode(",", $value); - list($statusCode, $fileData) = $this->getSoftwareFile($url); - if (empty($fileData) || !$this->yellow->toolbox->createFile($fileName.$fileExtension, $fileData)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - break; - } - } - if ($statusCode==200) { - foreach ($data as $key=>$value) { - $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; - if (!$this->yellow->toolbox->renameFile($fileName.$fileExtension, $fileName)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - } - } - return $statusCode; - } - - // Update software - public function updateSoftware($force = false) { - $statusCode = 200; - if (function_exists("opcache_reset")) opcache_reset(); - $path = $this->yellow->config->get("pluginDir"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { - $statusCode = max($statusCode, $this->updateSoftwareArchive($entry, $force)); - if (!$this->yellow->toolbox->deleteFile($entry)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't delete file '$entry'!"); - } - } - $path = $this->yellow->config->get("themeDir"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { - $statusCode = max($statusCode, $this->updateSoftwareArchive($entry, $force)); - if (!$this->yellow->toolbox->deleteFile($entry)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't delete file '$entry'!"); - } - } - return $statusCode; - } - - // Update software from archive - public function updateSoftwareArchive($path, $force = false) { - $statusCode = 200; - $zip = new ZipArchive(); - if ($zip->open($path)===true) { - if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateSoftwareArchive file:$path<br/>\n"; - if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1]; - $fileData = $zip->getFromName($pathBase.$this->yellow->config->get("updateInformationFile")); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2])) { - list($dummy, $entry) = explode("/", $matches[1], 2); - list($fileName) = explode(",", $matches[2], 2); - if ($dummy[0]!="Y") $fileName = $matches[1]; //TODO: remove later, converts old file format - if (is_file($fileName)) { - $lastPublished = filemtime($fileName); - break; - } - } - } - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (lcfirst($matches[1])=="plugin" || lcfirst($matches[1])=="theme") $software = $matches[2]; - if (lcfirst($matches[1])=="published") $modified = strtotime($matches[2]); - if (!empty($matches[1]) && !empty($matches[2]) && strposu($matches[1], "/")) { - list($dummy, $entry) = explode("/", $matches[1], 2); - list($fileName, $flags) = explode(",", $matches[2], 2); - if ($dummy[0]!="Y") { //TODO: remove later, converts old file format - list($entry, $flags) = explode(",", $matches[2], 2); - $fileName = $matches[1]; - } - $fileData = $zip->getFromName($pathBase.$entry); - $lastModified = $this->yellow->toolbox->getFileModified($fileName); - $statusCode = $this->updateSoftwareFile($fileName, $fileData, $modified, $lastModified, $lastPublished, $flags, $force, $software); - if ($statusCode!=200) break; - } - } - $zip->close(); - if ($statusCode==200) $statusCode = $this->updateSoftwareMultiLanguage($software); - if ($statusCode==200) $statusCode = $this->updateSoftwareNotification($software); - ++$this->updates; - } else { - $statusCode = 500; - $this->yellow->page->error(500, "Can't open file '$path'!"); - } - return $statusCode; - } - - // Update software file - public function updateSoftwareFile($fileName, $fileData, $modified, $lastModified, $lastPublished, $flags, $force, $software) { - $statusCode = 200; - $fileName = $this->yellow->toolbox->normaliseTokens($fileName); - if ($this->yellow->lookup->isValidFile($fileName) && !empty($software)) { - $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("/delete/i", $flags) && is_file($fileName)) $delete = true; - if (preg_match("/careful/i", $flags) && is_file($fileName) && $lastModified!=$lastPublished && !$force) $update = false; - if (preg_match("/optional/i", $flags) && $this->isSoftwareExisting($software)) $create = $update = $delete = false; - if ($create) { - if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || - !$this->yellow->toolbox->modifyFile($fileName, $modified)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - } - if ($update) { - if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir")) || - !$this->yellow->toolbox->createFile($fileName, $fileData) || - !$this->yellow->toolbox->modifyFile($fileName, $modified)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - } - if ($delete) { - if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir"))) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); - } - } - if (defined("DEBUG") && DEBUG>=2) { - $debug = "action:".($create ? "create" : "").($update ? "update" : "").($delete ? "delete" : ""); - if (!$create && !$update && !$delete) $debug = "action:none"; - echo "YellowUpdate::updateSoftwareFile file:$fileName $debug<br/>\n"; - } - } - return $statusCode; - } - - // Update software for multiple languages - public function updateSoftwareMultiLanguage($software) { - $statusCode = 200; - if ($this->yellow->config->get("multiLanguageMode") && !$this->isSoftwareExisting($software)) { - $pathsSource = $pathsTarget = array(); - $pathBase = $this->yellow->config->get("contentDir"); - $fileExtension = $this->yellow->config->get("contentExtension"); - $fileRegex = "/^.*\\".$fileExtension."$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true) as $entry) { - if (count($this->yellow->toolbox->getDirectoryEntries($entry, $fileRegex, false, false))) { - array_push($pathsSource, $entry."/"); - } elseif (count($this->yellow->toolbox->getDirectoryEntries($entry, "/.*/", false, true))) { - array_push($pathsTarget, $entry."/"); - } - } - if (count($pathsSource) && count($pathsTarget)) { - foreach ($pathsSource as $pathSource) { - foreach ($pathsTarget as $pathTarget) { - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($pathSource, "/.*/", false, false); - foreach ($fileNames as $fileName) { - $modified = $this->yellow->toolbox->getFileModified($fileName); - $fileNameTarget = $pathTarget.substru($fileName, strlenu($pathBase)); - if (!is_file($fileNameTarget)) { - if (!$this->yellow->toolbox->copyFile($fileName, $fileNameTarget, true) || - !$this->yellow->toolbox->modifyFile($fileNameTarget, $modified)) { - $statusCode = 500; - $this->yellow->page->error(500, "Can't write file '$fileNameTarget'!"); - } - } - if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateSoftwareNew file:$fileNameTarget<br/>\n"; - } - } - if (!$this->yellow->toolbox->deleteDirectory($pathSource)) { - $statusCode = 500; - $this->yellow->page->error(500, "Can't delete path '$pathSource'!"); - } - } - } - } - return $statusCode; - } - - // Update software notification for next startup - public function updateSoftwareNotification($software) { - $statusCode = 200; - $startupUpdate = $this->yellow->config->get("startupUpdate"); - if ($startupUpdate=="none") $startupUpdate = "YellowUpdate"; - if ($software!="YellowUpdate") $startupUpdate .= ",$software"; - $fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile"); - if (!$this->yellow->config->save($fileNameConfig, array("startupUpdate" => $startupUpdate))) { - $statusCode = 500; - $this->yellow->page->error(500, "Can't write file '$fileNameConfig'!"); - } - return $statusCode; - } - - // Remove software - public function removeSoftware($data) { - $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->removeSoftwareFile($fileName, $key)); - } - ++$this->updates; - } - if ($statusCode==200) $statusCode = $this->updateSoftwareNotification("YellowUpdate"); - return $statusCode; - } - - // Remove software file - public function removeSoftwareFile($fileName, $software) { - $statusCode = 200; - $fileName = $this->yellow->toolbox->normaliseTokens($fileName); - if ($this->yellow->lookup->isValidFile($fileName) && !empty($software)) { - if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir"))) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); - } - if (defined("DEBUG") && DEBUG>=2) { - echo "YellowUpdate::removeSoftwareFile file:$fileName action:delete<br/>\n"; - } - } - return $statusCode; - } - - // Return features from commandline arguments - public function getCommandFeatures($args) { - $command = array_shift($args); - $features = array_unique(array_filter($args, "strlen")); - foreach ($features as $key=>$value) { - if ($value=="force") { - $force = true; - unset($features[$key]); - } - } - return array($command, $features, $force); - } - - // Return software version - public function getSoftwareVersion($latest = false, $rawFormat = false) { - $data = array(); - if ($latest) { - $urlPlugins = $this->yellow->config->get("updatePluginsUrl")."/raw/master/".$this->yellow->config->get("updateVersionFile"); - $urlThemes = $this->yellow->config->get("updateThemesUrl")."/raw/master/".$this->yellow->config->get("updateVersionFile"); - list($statusCodePlugins, $fileDataPlugins) = $this->getSoftwareFile($urlPlugins); - list($statusCodeThemes, $fileDataThemes) = $this->getSoftwareFile($urlThemes); - $statusCode = max($statusCodePlugins, $statusCodeThemes); - if ($statusCode==200) { - foreach ($this->yellow->toolbox->getTextLines($fileDataPlugins."\n".$fileDataThemes) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2])) { - list($version) = explode(",", $matches[2]); - $data[$matches[1]] = $rawFormat ? $matches[2] : $version; - } - } - } - } else { - $statusCode = 200; - $data = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData()); - } - return array($statusCode, $data); - } - - // Return software modification - public function getSoftwareModified() { - $data = array(); - $dataCurrent = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData()); - $urlPlugins = $this->yellow->config->get("updatePluginsUrl")."/raw/master/".$this->yellow->config->get("updateResourceFile"); - $urlThemes = $this->yellow->config->get("updateThemesUrl")."/raw/master/".$this->yellow->config->get("updateResourceFile"); - list($statusCodePlugins, $fileDataPlugins) = $this->getSoftwareFile($urlPlugins); - list($statusCodeThemes, $fileDataThemes) = $this->getSoftwareFile($urlThemes); - $statusCode = max($statusCodePlugins, $statusCodeThemes); - if ($statusCode==200) { - foreach ($this->yellow->toolbox->getTextLines($fileDataPlugins."\n".$fileDataThemes) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2])) { - list($softwareNew) = explode("/", $matches[1]); - list($fileName, $flags) = explode(",", $matches[2], 2); - if ($software!=$softwareNew) { - $software = $softwareNew; - $lastPublished = $this->yellow->toolbox->getFileModified($fileName); - } - if (!is_null($dataCurrent[$software])) { - $lastModified = $this->yellow->toolbox->getFileModified($fileName); - if (preg_match("/update/i", $flags) && preg_match("/careful/i", $flags) && $lastModified!=$lastPublished) { - $data[$software] = $dataCurrent[$software]; - if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::getSoftwareModified detected file:$fileName<br/>\n"; - } - } - } - } - } - return array($statusCode, $data); - } - - // Return software files - public function getSoftwareFiles() { - $data = array(); - $urlPlugins = $this->yellow->config->get("updatePluginsUrl")."/raw/master/".$this->yellow->config->get("updateResourceFile"); - $urlThemes = $this->yellow->config->get("updateThemesUrl")."/raw/master/".$this->yellow->config->get("updateResourceFile"); - list($statusCodePlugins, $fileDataPlugins) = $this->getSoftwareFile($urlPlugins); - list($statusCodeThemes, $fileDataThemes) = $this->getSoftwareFile($urlThemes); - $statusCode = max($statusCodePlugins, $statusCodeThemes); - if ($statusCode==200) { - foreach ($this->yellow->toolbox->getTextLines($fileDataPlugins."\n".$fileDataThemes) as $line) { - preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches); - if (!empty($matches[1]) && !empty($matches[2])) { - list($software) = explode("/", $matches[1]); - list($fileName) = explode(",", $matches[2], 2); - if (!is_null($data[$software])) $data[$software] .= ","; - $data[$software] .= $fileName; - } - } - } - return array($statusCode, $data); - } - - // Return software file - public function getSoftwareFile($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_RETURNTRANSFER, 1); - curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); - $rawData = curl_exec($curlHandle); - $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); - 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 { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't download file '$url'!"); - } - if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::getSoftwareFile status:$statusCode url:$url<br/>\n"; - return array($statusCode, $fileData); - } - - // Check if software pending - public function isSoftwarePending() { - $path = $this->yellow->config->get("pluginDir"); - $foundPlugins = count($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))>0; - $path = $this->yellow->config->get("themeDir"); - $foundThemes = count($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))>0; - return $foundPlugins || $foundThemes; - } - - // Check if software exists - public function isSoftwareExisting($software) { - $data = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData()); - return !is_null($data[$software]); - } -} diff --git a/system/resources/flatsite-icon.png b/system/resources/flatsite-icon.png Binary files differ. diff --git a/system/resources/flatsite.css b/system/resources/flatsite.css @@ -0,0 +1,541 @@ +/* Flatsite extension, https://github.com/datenstrom/yellow-extensions/tree/master/themes/flatsite */ +/* Copyright (c) 2013-2019 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(opensans-light.woff) format("woff"); +} +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + src: url(opensans-regular.woff) format("woff"); +} +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + src: url(opensans-bold.woff) format("woff"); +} +body { + margin: 1em; + background-color: #fff; + color: #717171; + 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: normal; +} +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:first-child, +.content > *:first-child { + margin-top: 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 blockquote { + margin-left: 0; + padding-left: 1em; + border-left: 0.5em solid #0a0; +} +.content blockquote blockquote { + margin-left: -1.5em; + border-left: 0.5em solid #fb0; +} +.content blockquote blockquote blockquote { + border-color: #d00; +} +.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 .flexible { + position: relative; + padding-top: 0; + padding-bottom: 56.25%; +} +.content .flexible iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.content .stretchable ul { + margin: 0 -0.5em; + padding: 0; + list-style: none; + text-align: center; +} +.content .stretchable li { + margin: 0; + padding: 1em 0; + display: inline-block; + text-align: center; + vertical-align: top; +} +.content .stretchable a { + color: #717171; + text-decoration: none; +} +.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 .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 .sitename { + display: block; + float: left; + margin-top: 0.25em; + margin-bottom: 1em; +} +.header .sitename h1 { + margin:0; + font-size: 1em; + font-weight: 300; +} +.header .sitename h1 a { + color: #111; + text-decoration: none; +} +.header .sitename h1 a:hover { + color: #07d; + text-decoration: underline; +} +.header .sitename p { + margin-top: 0; + color: #111; +} + +/* Navigation */ + +.navigation { + display: block; + float: right; + margin-bottom: 1em; + line-height: 2; +} +.navigation a { + padding: 0 0.3em; +} +.navigation ul { + margin: 0 -0.3em; + padding: 0; + list-style: none; +} +.navigation li { + display: inline; +} +.navigation-tree { + display: block; + float: right; + margin-bottom: 1em; + line-height: 2; +} +.navigation-tree a { + padding: 0 0.3em; +} +.navigation-tree ul { + margin: 0 -0.3em; + padding: 0; + list-style: none; +} +.navigation-tree li { + display: inline; +} +.navigation-tree ul li { + display: inline-block; + position: relative; + cursor: pointer; + margin: 0; +} +.navigation-tree ul li ul { + padding: 0.3em; + position: absolute; + width: 13em; + background: #fff; + z-index: 100; + display: none; +} +.navigation-tree ul li ul { + border: 1px solid #bbb; + border-radius: 4px; + box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2); +} +.navigation-tree ul li ul li { + display: block; +} +.navigation-tree > ul > li:hover > ul { + display: block; +} +.navigation-banner { + clear: both; +} +.navigation-search { + padding-bottom: 0.75em; +} +.navigation-search .search-form { + position: relative; +} +.navigation-search .search-text { + font-family: inherit; + font-size: inherit; + font-weight: inherit; +} +.navigation-search .search-text { + padding: 0.5em; + border: 1px solid #bbb; + border-radius: 4px; + width: 100%; + box-sizing: border-box; +} +.navigation-search .search-text { + background-color: #fff; + background-image: linear-gradient(to bottom, #fff, #fff); +} +.navigation-search .search-button { + position: absolute; + top: 0; + right: 0; +} +.navigation-search .search-button { + font-family: inherit; + font-size: inherit; + font-weight: inherit; +} +.navigation-search .search-button { + margin: 5px; + padding: 0.3em; + border: none; + background-color: transparent; +} + +/* Footer */ + +.footer { + margin-top: 1em; +} +.footer .siteinfo a { + color: #07d; +} +.footer .siteinfo a:hover { + color: #07d; + text-decoration: underline; +} +.footer .siteinfo a.language img { + vertical-align: middle; + margin-top: -5px; + margin-right: 0.75em; +} +.footer .siteinfo-left { + float: left; +} +.footer .siteinfo-right { + float: right; +} +.footer .siteinfo-banner { + clear: both; +} + +/* Sidebar */ + +.with-sidebar .main { + margin-right: 15em; +} +.with-sidebar .sidebar { + float: right; + width: 13em; + margin-top: 3.2em; + padding: 2px; + overflow: hidden; + text-align: right; +} +.with-sidebar .sidebar ul { + padding: 0; + list-style: none; +} +.with-sidebar .sidebar .search-form input { + width: 100%; + box-sizing: border-box; +} +.with-sidebar .content:after { + content: ""; + display: table; + clear: both; +} + +/* Forms and buttons */ + +.form-control { + margin: 0; + padding: 2px 4px; + display: inline-block; + min-width: 7em; + background-color: #fff; + color: #555; + 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); +} + +/* Misc */ + +.layout-default .content img.screenshot { + margin: 0 -0.5em; +} +.layout-language .content div.language { + font-size: 1.2em; + text-align: left; + width: 9em; + margin: 0 auto; +} +.layout-language .content div.language p { + margin: 1.5em 0em; +} +.layout-language .content div.language img { + vertical-align: middle; + margin-top: -5px; + margin-right: 1.5em; +} +.hljs-meta, +.hljs-keyword, +.hljs-literal { + color: #b0b; +} +.hljs-attr, +.hljs-attribute, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-pseudo { + color: #b0b; +} +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-params { + color: #b0b; +} +.hljs-string { + color: #717171; +} +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-number { + color: #717171; +} + +/* 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.3em; + } + .footer, + .page { + margin: 0; + padding: 0; + } + .header .sitename, + .navigation, + .navigation-tree { + float: none; + } + .header .sitename, + .navigation, + .navigation-tree { + margin-bottom: 0.5em; + } + .navigation-search { + padding-bottom: 1em; + } + .with-sidebar .main { + margin-right: 0; + } + .with-sidebar .sidebar { + display: none; + } +} +@media print { + .page { + border: none !important; + } +} diff --git a/system/themes/assets/opensans-bold.woff b/system/resources/opensans-bold.woff Binary files differ. diff --git a/system/themes/assets/opensans-light.woff b/system/resources/opensans-light.woff Binary files differ. diff --git a/system/themes/assets/opensans-regular.woff b/system/resources/opensans-regular.woff Binary files differ. diff --git a/system/settings/system.ini b/system/settings/system.ini @@ -0,0 +1,74 @@ +# Datenstrom Yellow system settings + +Sitename: Datenstrom Yellow +Author: Datenstrom +Email: webmaster +Language: en +Timezone: UTC + +StaticUrl: +StaticDefaultFile: index.html +StaticErrorFile: 404.html +StaticDir: public/ +CacheDir: cache/ +MediaLocation: /media/ +DownloadLocation: /media/downloads/ +ImageLocation: /media/images/ +ExtensionLocation: /media/extensions/ +ResourceLocation: /media/resources/ +MediaDir: media/ +DownloadDir: media/downloads/ +ImageDir: media/images/ +SystemDir: system/ +ExtensionDir: system/extensions/ +LayoutDir: system/layouts/ +ResourceDir: system/resources/ +SettingDir: system/settings/ +TrashDir: system/trash/ +ContentDir: content/ +ContentRootDir: default/ +ContentHomeDir: home/ +ContentSharedDir: shared/ +ContentPagination: page +ContentDefaultFile: page.md +ContentExtension: .md +DownloadExtension: .download +TextFile: text.ini +ServerUrl: +Layout: default +Theme: flatsite +Parser: markdown +Navigation: navigation +Header: header +Footer: footer +Sidebar: sidebar +StartupUpdate: none +MultiLanguageMode: 0 +SafeMode: 0 +BundleAndMinify: 1 +EditLocation: /edit/ +EditUploadNewLocation: /media/@group/@filename +EditUploadExtensions: .gif, .jpg, .pdf, .png, .svg, .tgz, .zip +EditKeyboardShortcuts: ctrl+b bold, ctrl+i italic, ctrl+e code, ctrl+k link, ctrl+s save, ctrl+shift+p preview +EditToolbarButtons: auto +EditEndOfLine: auto +EditUserFile: user.ini +EditUserPasswordMinLength: 8 +EditUserHashAlgorithm: bcrypt +EditUserHashCost: 10 +EditUserHome: / +EditNewFile: page-new-(.*).md +EditLoginRestrictions: 0 +EditLoginSessionTimeout: 2592000 +EditBruteForceProtection: 25 +ImageAlt: Image +ImageUploadWidthMax: 1280 +ImageUploadHeightMax: 1280 +ImageUploadJpgQuality: 80 +ImageThumbnailLocation: /media/thumbnails/ +ImageThumbnailDir: media/thumbnails/ +ImageThumbnailJpgQuality: 80 +UpdateExtensionUrl: https://github.com/datenstrom/yellow-extensions +UpdateInformationFile: update.ini +UpdateVersionFile: version.ini +UpdateWaffleFile: waffle.ini diff --git a/system/settings/text.ini b/system/settings/text.ini @@ -0,0 +1,2 @@ +# Datenstrom Yellow text settings + diff --git a/system/config/user.ini b/system/settings/user.ini diff --git a/system/themes/assets/flatsite.css b/system/themes/assets/flatsite.css @@ -1,541 +0,0 @@ -/* Flatsite theme, https://github.com/datenstrom/yellow-themes/tree/master/flatsite */ -/* Copyright (c) 2013-2019 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(opensans-light.woff) format("woff"); -} -@font-face { - font-family: "Open Sans"; - font-style: normal; - font-weight: 400; - src: url(opensans-regular.woff) format("woff"); -} -@font-face { - font-family: "Open Sans"; - font-style: normal; - font-weight: 700; - src: url(opensans-bold.woff) format("woff"); -} -body { - margin: 1em; - background-color: #fff; - color: #717171; - 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: normal; -} -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:first-child, -.content > *:first-child { - margin-top: 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 blockquote { - margin-left: 0; - padding-left: 1em; - border-left: 0.5em solid #0a0; -} -.content blockquote blockquote { - margin-left: -1.5em; - border-left: 0.5em solid #fb0; -} -.content blockquote blockquote blockquote { - border-color: #d00; -} -.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 .flexible { - position: relative; - padding-top: 0; - padding-bottom: 56.25%; -} -.content .flexible iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} -.content .stretchable ul { - margin: 0 -0.5em; - padding: 0; - list-style: none; - text-align: center; -} -.content .stretchable li { - margin: 0; - padding: 1em 0; - display: inline-block; - text-align: center; - vertical-align: top; -} -.content .stretchable a { - color: #717171; - text-decoration: none; -} -.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 .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 .sitename { - display: block; - float: left; - margin-top: 0.25em; - margin-bottom: 1em; -} -.header .sitename h1 { - margin:0; - font-size: 1em; - font-weight: 300; -} -.header .sitename h1 a { - color: #111; - text-decoration: none; -} -.header .sitename h1 a:hover { - color: #07d; - text-decoration: underline; -} -.header .sitename p { - margin-top: 0; - color: #111; -} - -/* Navigation */ - -.navigation { - display: block; - float: right; - margin-bottom: 1em; - line-height: 2; -} -.navigation a { - padding: 0 0.3em; -} -.navigation ul { - margin: 0 -0.3em; - padding: 0; - list-style: none; -} -.navigation li { - display: inline; -} -.navigation-tree { - display: block; - float: right; - margin-bottom: 1em; - line-height: 2; -} -.navigation-tree a { - padding: 0 0.3em; -} -.navigation-tree ul { - margin: 0 -0.3em; - padding: 0; - list-style: none; -} -.navigation-tree li { - display: inline; -} -.navigation-tree ul li { - display: inline-block; - position: relative; - cursor: pointer; - margin: 0; -} -.navigation-tree ul li ul { - padding: 0.3em; - position: absolute; - width: 13em; - background: #fff; - z-index: 100; - display: none; -} -.navigation-tree ul li ul { - border: 1px solid #bbb; - border-radius: 4px; - box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2); -} -.navigation-tree ul li ul li { - display: block; -} -.navigation-tree > ul > li:hover > ul { - display: block; -} -.navigation-banner { - clear: both; -} -.navigation-search { - padding-bottom: 0.75em; -} -.navigation-search .search-form { - position: relative; -} -.navigation-search .search-text { - font-family: inherit; - font-size: inherit; - font-weight: inherit; -} -.navigation-search .search-text { - padding: 0.5em; - border: 1px solid #bbb; - border-radius: 4px; - width: 100%; - box-sizing: border-box; -} -.navigation-search .search-text { - background-color: #fff; - background-image: linear-gradient(to bottom, #fff, #fff); -} -.navigation-search .search-button { - position: absolute; - top: 0; - right: 0; -} -.navigation-search .search-button { - font-family: inherit; - font-size: inherit; - font-weight: inherit; -} -.navigation-search .search-button { - margin: 5px; - padding: 0.3em; - border: none; - background-color: transparent; -} - -/* Footer */ - -.footer { - margin-top: 1em; -} -.footer .siteinfo a { - color: #07d; -} -.footer .siteinfo a:hover { - color: #07d; - text-decoration: underline; -} -.footer .siteinfo a.language img { - vertical-align: middle; - margin-top: -5px; - margin-right: 0.75em; -} -.footer .siteinfo-left { - float: left; -} -.footer .siteinfo-right { - float: right; -} -.footer .siteinfo-banner { - clear: both; -} - -/* Sidebar */ - -.with-sidebar .main { - margin-right: 15em; -} -.with-sidebar .sidebar { - float: right; - width: 13em; - margin-top: 3.2em; - padding: 2px; - overflow: hidden; - text-align: right; -} -.with-sidebar .sidebar ul { - padding: 0; - list-style: none; -} -.with-sidebar .sidebar .search-form input { - width: 100%; - box-sizing: border-box; -} -.with-sidebar .content:after { - content: ""; - display: table; - clear: both; -} - -/* Forms and buttons */ - -.form-control { - margin: 0; - padding: 2px 4px; - display: inline-block; - min-width: 7em; - background-color: #fff; - color: #555; - 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); -} - -/* Misc */ - -.template-default .content img.screenshot { - margin: 0 -0.5em; -} -.template-language .content div.language { - font-size: 1.2em; - text-align: left; - width: 9em; - margin: 0 auto; -} -.template-language .content div.language p { - margin: 1.5em 0em; -} -.template-language .content div.language img { - vertical-align: middle; - margin-top: -5px; - margin-right: 1.5em; -} -.hljs-meta, -.hljs-keyword, -.hljs-literal { - color: #b0b; -} -.hljs-attr, -.hljs-attribute, -.hljs-selector-id, -.hljs-selector-class, -.hljs-selector-pseudo { - color: #b0b; -} -.hljs-type, -.hljs-built_in, -.hljs-builtin-name, -.hljs-params { - color: #b0b; -} -.hljs-string { - color: #717171; -} -.hljs-symbol, -.hljs-bullet, -.hljs-link, -.hljs-number { - color: #717171; -} - -/* 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.3em; - } - .footer, - .page { - margin: 0; - padding: 0; - } - .header .sitename, - .navigation, - .navigation-tree { - float: none; - } - .header .sitename, - .navigation, - .navigation-tree { - margin-bottom: 0.5em; - } - .navigation-search { - padding-bottom: 1em; - } - .with-sidebar .main { - margin-right: 0; - } - .with-sidebar .sidebar { - display: none; - } -} -@media print { - .page { - border: none !important; - } -} diff --git a/system/themes/assets/flatsite.php b/system/themes/assets/flatsite.php @@ -1,8 +0,0 @@ -<?php -// Flatsite theme, https://github.com/datenstrom/yellow-themes/tree/master/flatsite -// Copyright (c) 2013-2019 Datenstrom, https://datenstrom.se -// This file may be used and distributed under the terms of the public license. - -class YellowThemeFlatsite { - const VERSION = "0.8.1"; -} diff --git a/system/themes/assets/icon.png b/system/themes/assets/icon.png Binary files differ. diff --git a/system/themes/snippets/footer.php b/system/themes/snippets/footer.php @@ -1,11 +0,0 @@ -<div class="footer" role="contentinfo"> -<div class="siteinfo"> -<?php if ($yellow->page->isPage("footer")) echo $yellow->page->getPage("footer")->getContent() ?> -</div> -<div class="siteinfo-banner"></div> -</div> -</div> -</div> -<?php echo $yellow->page->getExtra("footer") ?> -</body> -</html> diff --git a/system/themes/snippets/header.php b/system/themes/snippets/header.php @@ -1,27 +0,0 @@ -<!DOCTYPE html><html lang="<?php echo $yellow->page->getHtml("language") ?>"> -<head> -<title><?php echo $yellow->page->getHtml("titleHeader") ?></title> -<meta charset="utf-8" /> -<meta name="description" content="<?php echo $yellow->page->getHtml("description") ?>" /> -<meta name="keywords" content="<?php echo $yellow->page->getHtml("keywords") ?>" /> -<meta name="author" content="<?php echo $yellow->page->getHtml("author") ?>" /> -<meta name="generator" content="Datenstrom Yellow" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<?php echo $yellow->page->getExtra("header") ?> -</head> -<body> -<?php if ($page = $yellow->pages->shared($yellow->page->location, false, $yellow->page->get("header"))) $yellow->page->setPage("header", $page) ?> -<?php if ($page = $yellow->pages->shared($yellow->page->location, false, $yellow->page->get("footer"))) $yellow->page->setPage("footer", $page) ?> -<?php if ($page = $yellow->pages->shared($yellow->page->location, false, $yellow->page->get("sidebar"))) $yellow->page->setPage("sidebar", $page) ?> -<?php if ($yellow->page->get("navigation")=="navigation-sidebar") $yellow->page->setPage("navigation-sidebar", $yellow->page->getParentTop(true)) ?> -<?php $yellow->page->set("pageClass", "page template-".$yellow->page->get("template")) ?> -<?php if (!$yellow->page->isError() && ($yellow->page->isPage("sidebar") || $yellow->page->isPage("navigation-sidebar"))) $yellow->page->set("pageClass", $yellow->page->get("pageClass")." with-sidebar") ?> -<div class="<?php echo $yellow->page->getHtml("pageClass") ?>"> -<div class="header" role="banner"> -<div class="sitename"> -<h1><a href="<?php echo $yellow->page->getBase(true)."/" ?>"><i class="sitename-logo"></i><?php echo $yellow->page->getHtml("sitename") ?></a></h1> -<?php if ($yellow->page->isPage("header")) echo $yellow->page->getPage("header")->getContent() ?> -</div> -<div class="sitename-banner"></div> -<?php $yellow->snippet($yellow->page->get("navigation")) ?> -</div> diff --git a/system/themes/snippets/navigation-sidebar.php b/system/themes/snippets/navigation-sidebar.php @@ -1,10 +0,0 @@ -<?php $pages = $yellow->pages->top() ?> -<?php $yellow->page->setLastModified($pages->getModified()) ?> -<div class="navigation" role="navigation"> -<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> -<?php endforeach ?> -</ul> -</div> -<div class="navigation-banner"></div> diff --git a/system/themes/snippets/navigation-tree.php b/system/themes/snippets/navigation-tree.php @@ -1,16 +0,0 @@ -<?php list($name, $pages, $level) = $yellow->getSnippetArgs() ?> -<?php if (!$pages) $pages = $yellow->pages->top() ?> -<?php $yellow->page->setLastModified($pages->getModified()) ?> -<?php if (!$level): ?> -<div class="navigation-tree" role="navigation"> -<?php endif ?> -<ul> -<?php foreach ($pages as $page): ?> -<?php $children = $page->getChildren() ?> -<li><a<?php echo $page->isActive() ? " class=\"active\" aria-current=\"page\"" : "" ?> href="<?php echo $page->getLocation(true) ?>"><?php echo $page->getHtml("titleNavigation") ?></a><?php if ($children->count()) { echo "\n"; $yellow->snippet($name, $children, $level+1); } ?></li> -<?php endforeach ?> -</ul> -<?php if (!$level): ?> -</div> -<div class="navigation-banner"></div> -<?php endif ?> diff --git a/system/themes/snippets/navigation.php b/system/themes/snippets/navigation.php @@ -1,10 +0,0 @@ -<?php $pages = $yellow->pages->top() ?> -<?php $yellow->page->setLastModified($pages->getModified()) ?> -<div class="navigation" role="navigation"> -<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> -<?php endforeach ?> -</ul> -</div> -<div class="navigation-banner"></div> diff --git a/system/themes/snippets/pagination.php b/system/themes/snippets/pagination.php @@ -1,11 +0,0 @@ -<?php list($name, $pages) = $yellow->getSnippetArgs() ?> -<?php if ($pages->isPagination()): ?> -<div class="pagination" role="navigation"> -<?php if ($pages->getPaginationPrevious()): ?> -<a class="previous" href="<?php echo $pages->getPaginationPrevious() ?>"><?php echo $yellow->text->getHtml("paginationPrevious") ?></a> -<?php endif ?> -<?php if ($pages->getPaginationNext()): ?> -<a class="next" href="<?php echo $pages->getPaginationNext() ?>"><?php echo $yellow->text->getHtml("paginationNext") ?></a> -<?php endif ?> -</div> -<?php endif ?> diff --git a/system/themes/snippets/sidebar.php b/system/themes/snippets/sidebar.php @@ -1,21 +0,0 @@ -<?php if ($yellow->page->isPage("sidebar")): ?> -<div class="sidebar" role="complementary"> -<?php $page = $yellow->page->getPage("sidebar") ?> -<?php $page->setPage("main", $yellow->page) ?> -<?php echo $page->getContent() ?> -</div> -<?php elseif ($yellow->page->isPage("navigation-sidebar")): ?> -<div class="sidebar" role="complementary"> -<div class="navigation-sidebar"> -<?php $page = $yellow->page->getPage("navigation-sidebar") ?> -<?php $pages = $page->getChildren(!$page->isVisible()) ?> -<?php $yellow->page->setLastModified($pages->getModified()) ?> -<p><?php echo $page->getHtml("titleNavigation") ?></p> -<ul> -<?php foreach ($pages as $page): ?> -<li><a<?php echo $page->isActive() ? " class=\"active\"" : "" ?> href="<?php echo $page->getLocation(true) ?>"><?php echo $page->getHtml("titleNavigation") ?></a></li> -<?php endforeach ?> -</ul> -</div> -</div> -<?php endif ?> diff --git a/system/themes/templates/default.html b/system/themes/templates/default.html @@ -1,9 +0,0 @@ -<?php $yellow->snippet("header") ?> -<div class="content"> -<?php $yellow->snippet("sidebar") ?> -<div class="main" role="main"> -<h1><?php echo $yellow->page->getHtml("titleContent") ?></h1> -<?php echo $yellow->page->getContent() ?> -</div> -</div> -<?php $yellow->snippet("footer") ?> diff --git a/system/themes/templates/error.html b/system/themes/templates/error.html @@ -1,8 +0,0 @@ -<?php $yellow->snippet("header") ?> -<div class="content"> -<div class="main" role="main"> -<h1><?php echo $yellow->page->getHtml("titleContent") ?></h1> -<?php echo $yellow->page->getContent() ?> -</div> -</div> -<?php $yellow->snippet("footer") ?>