commit c17da68c1a5911c79390f6f002fcd3a278ad067f
parent a19b77b40cdda6b533a78ef543b7d2e435c68fe7
Author: markseu <mark2011@mayberg.se>
Date: Sat, 23 Feb 2019 15:04:34 +0100
Updated system, new API
Diffstat:
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, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """);
+ },
+
+ // 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: 
+ // 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('"', '"', $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('&', '&', $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+);)/',
+ '&', $text);
+ }
+ // Encode remaining <'s
+ $text = str_replace('<', '<', $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.:
+ *
+ * mailto:foo
+ * @example.co
+ * m
+ *
+ * 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 = '↩︎';
+
+ /**
+ * 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: 
+ // 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) . " $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, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/"/g, """);
- },
-
- // 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: 
- // 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('"', '"', $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('&', '&', $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+);)/',
- '&', $text);
- }
- // Encode remaining <'s
- $text = str_replace('<', '<', $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.:
- *
- * mailto:foo
- * @example.co
- * m
- *
- * 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 = '↩︎';
-
- /**
- * 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: 
- // 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) . " $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") ?>