mikuli.cz

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

commit 3f6cc2ea6d26c03277f03ad7d21dc5fe886c2659
parent dd0fc7d664c5ab35b2c64f7f775800c00c62e5c0
Author: markseu <mark2011@mayberg.se>
Date:   Thu,  4 Apr 2024 23:54:22 +0200

Datenstrom Yellow 0.9 has been released

Co-Authored-By: Anna <14218799+annaesvensson@users.noreply.github.com>
Co-Authored-By: Steffen Schultz <steffenschultz@mailbox.org>

Diffstat:
MREADME-de.md | 2+-
MREADME-sv.md | 2+-
MREADME.md | 2+-
Mcontent/1-home/page.md | 2+-
Dmedia/downloads/yellow.pdf | 0
Dsystem/extensions/core.php | 3964-------------------------------------------------------------------------------
Dsystem/extensions/edit.php | 2018-------------------------------------------------------------------------------
Dsystem/extensions/generate.php | 438-------------------------------------------------------------------------------
Dsystem/extensions/image.php | 199-------------------------------------------------------------------------------
Dsystem/extensions/install-blog.bin | 0
Dsystem/extensions/install-language.bin | 0
Dsystem/extensions/install-wiki.bin | 0
Dsystem/extensions/install.php | 575-------------------------------------------------------------------------------
Dsystem/extensions/markdown.php | 4080-------------------------------------------------------------------------------
Dsystem/extensions/serve.php | 61-------------------------------------------------------------
Dsystem/extensions/stockholm.php | 22----------------------
Msystem/extensions/update-available.ini | 227++++++++++++++++++++++++++++++++++++++++---------------------------------------
Dsystem/extensions/update-current.ini | 139-------------------------------------------------------------------------------
Dsystem/extensions/update.php | 941-------------------------------------------------------------------------------
Asystem/extensions/yellow-extension.ini | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/workers/core.php | 3961+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsystem/extensions/edit-stack.svg -> system/workers/edit-stack.svg | 0
Rsystem/extensions/edit.css -> system/workers/edit.css | 0
Rsystem/extensions/edit.js -> system/workers/edit.js | 0
Asystem/workers/edit.php | 2018+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/workers/generate.php | 438+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/workers/image.php | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/workers/install-blog.bin | 0
Asystem/workers/install-language.bin | 0
Asystem/workers/install-wiki.bin | 0
Asystem/workers/install.php | 575+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/workers/markdown.php | 4080+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/workers/serve.php | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystem/workers/stockholm.php | 22++++++++++++++++++++++
Asystem/workers/update.php | 940+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Myellow.php | 2+-
36 files changed, 12555 insertions(+), 12555 deletions(-)

diff --git a/README-de.md b/README-de.md @@ -1,6 +1,6 @@ <p align="right"><a href="README-de.md">Deutsch</a> &nbsp; <a href="README.md">English</a> &nbsp; <a href="README-sv.md">Svenska</a></p> -# Datenstrom Yellow 0.8.23 +# Datenstrom Yellow 0.9 Datenstrom Yellow ist für Menschen die kleine Webseiten machen. [Demo ausprobieren](https://datenstrom.se/de/yellow/demo/). diff --git a/README-sv.md b/README-sv.md @@ -1,6 +1,6 @@ <p align="right"><a href="README-de.md">Deutsch</a> &nbsp; <a href="README.md">English</a> &nbsp; <a href="README-sv.md">Svenska</a></p> -# Datenstrom Yellow 0.8.23 +# Datenstrom Yellow 0.9 Datenstrom Yellow är för människor som skapar små webbsidor. [Prova demon](https://datenstrom.se/sv/yellow/demo/). diff --git a/README.md b/README.md @@ -1,6 +1,6 @@ <p align="right"><a href="README-de.md">Deutsch</a> &nbsp; <a href="README.md">English</a> &nbsp; <a href="README-sv.md">Svenska</a></p> -# Datenstrom Yellow 0.8.23 +# Datenstrom Yellow 0.9 Datenstrom Yellow is for people who make small websites. [Try the demo](https://datenstrom.se/yellow/demo/). diff --git a/content/1-home/page.md b/content/1-home/page.md @@ -3,4 +3,4 @@ Title: Home --- [image photo.jpg Example rounded] -[edit - You can edit this page in a web browser] or use a text editor. [Get help](https://datenstrom.se/yellow/help/). +[edit - You can edit this page] or use a text editor. [Get help](https://datenstrom.se/yellow/help/). diff --git a/media/downloads/yellow.pdf b/media/downloads/yellow.pdf Binary files differ. diff --git a/system/extensions/core.php b/system/extensions/core.php @@ -1,3964 +0,0 @@ -<?php -// Core extension, https://github.com/annaesvensson/yellow-core - -class YellowCore { - const VERSION = "0.8.133"; - const RELEASE = "0.8.23"; - public $content; // content files - public $media; // media files - public $system; // system settings - public $language; // language settings - public $user; // user settings - public $extension; // extensions - public $lookup; // lookup and normalisation methods - public $toolbox; // toolbox with helper methods - public $page; // current page - - public function __construct() { - $this->content = new YellowContent($this); - $this->media = new YellowMedia($this); - $this->system = new YellowSystem($this); - $this->language = new YellowLanguage($this); - $this->user = new YellowUser($this); - $this->extension = new YellowExtension($this); - $this->lookup = new YellowLookup($this); - $this->toolbox = new YellowToolbox($this); - $this->page = new YellowPage($this); - $this->checkRequirements(); - $this->system->setDefault("sitename", "Localhost"); - $this->system->setDefault("author", "Datenstrom"); - $this->system->setDefault("email", "webmaster"); - $this->system->setDefault("language", "en"); - $this->system->setDefault("layout", "default"); - $this->system->setDefault("theme", "default"); - $this->system->setDefault("parser", "markdown"); - $this->system->setDefault("status", "public"); - $this->system->setDefault("coreServerUrl", "auto"); - $this->system->setDefault("coreTimezone", "UTC"); - $this->system->setDefault("coreContentExtension", ".md"); - $this->system->setDefault("coreContentDefaultFile", "page.md"); - $this->system->setDefault("coreContentErrorFile", "page-error-(.*).md"); - $this->system->setDefault("coreLanguageFile", "yellow-language.ini"); - $this->system->setDefault("coreUserFile", "yellow-user.ini"); - $this->system->setDefault("coreWebsiteFile", "yellow-website.log"); - $this->system->setDefault("coreMediaLocation", "/media/"); - $this->system->setDefault("coreDownloadLocation", "/media/downloads/"); - $this->system->setDefault("coreImageLocation", "/media/images/"); - $this->system->setDefault("coreThumbnailLocation", "/media/thumbnails/"); - $this->system->setDefault("coreExtensionLocation", "/media/extensions/"); - $this->system->setDefault("coreThemeLocation", "/media/themes/"); - $this->system->setDefault("coreMultiLanguageMode", "0"); - $this->system->setDefault("coreDebugMode", "0"); - } - - public function __destruct() { - $this->shutdown(); - } - - // Check requirements - public function checkRequirements() { - if (!version_compare(PHP_VERSION, "7.0", ">=")) $this->exitFatalError("Datenstrom Yellow requires PHP 7.0 or higher!"); - if (!extension_loaded("curl")) $this->exitFatalError("Datenstrom Yellow requires PHP curl extension!"); - if (!extension_loaded("gd")) $this->exitFatalError("Datenstrom Yellow requires PHP gd extension!"); - if (!extension_loaded("mbstring")) $this->exitFatalError("Datenstrom Yellow requires PHP mbstring extension!"); - if (!extension_loaded("zip")) $this->exitFatalError("Datenstrom Yellow requires PHP zip extension!"); - mb_internal_encoding("UTF-8"); - } - - // Handle initialisation - public function load() { - $this->system->load("system/extensions/yellow-system.ini"); - $this->system->set("coreSystemFile", "yellow-system.ini"); - $this->system->set("coreContentDirectory", "content/"); - $this->system->set("coreMediaDirectory", $this->lookup->findMediaDirectory("coreMediaLocation")); - $this->system->set("coreSystemDirectory", "system/"); - $this->system->set("coreCacheDirectory", "system/cache/"); - $this->system->set("coreExtensionDirectory", "system/extensions/"); - $this->system->set("coreLayoutDirectory", "system/layouts/"); - $this->system->set("coreThemeDirectory", "system/themes/"); - $this->system->set("coreTrashDirectory", "system/trash/"); - list($pathInstall, $pathRoot, $pathHome) = $this->lookup->findFileSystemInformation(); - $this->system->set("coreServerInstallDirectory", $pathInstall); - $this->system->set("coreServerRootDirectory", $pathRoot); - $this->system->set("coreServerHomeDirectory", $pathHome); - register_shutdown_function(array($this, "processFatalError")); - if ($this->system->get("coreDebugMode")>=1) { - ini_set("display_errors", 1); - error_reporting(E_ALL); - } - date_default_timezone_set($this->system->get("coreTimezone")); - $this->extension->load($this->system->get("coreExtensionDirectory")); - $this->language->load($this->system->get("coreExtensionDirectory").$this->system->get("coreLanguageFile")); - $this->user->load($this->system->get("coreExtensionDirectory").$this->system->get("coreUserFile")); - $this->startup(); - } - - // Handle request from web browser - public function request() { - $statusCode = 0; - $this->toolbox->timerStart($time); - ob_start(); - list($scheme, $address, $base, $location, $fileName) = $this->lookup->getRequestInformation(); - $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, true); - foreach ($this->extension->data as $key=>$value) { - if (method_exists($value["object"], "onRequest")) { - $this->lookup->requestHandler = $key; - $statusCode = $value["object"]->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->isError()) $statusCode = $this->processRequestError(); - ob_end_flush(); - $this->toolbox->timerStop($time); - if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) { - echo "YellowCore::request status:$statusCode time:$time ms<br/>\n"; - } - return $statusCode; - } - - // Process request - public function processRequest($scheme, $address, $base, $location, $fileName, $cacheable) { - $statusCode = 0; - if (is_readable($fileName)) { - if ($this->lookup->isRequestCleanUrl($location)) { - $location = $location.$this->toolbox->getLocationArgumentsCleanUrl(); - $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->sendStatus(303, $location); - } - } else { - if ($this->lookup->isRedirectLocation($location)) { - $location = $this->lookup->getRedirectLocation($location); - $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->sendStatus(301, $location); - } - } - if ($statusCode==0) { - if ($this->lookup->isContentFile($fileName)) { - $statusCode = $this->sendPage($scheme, $address, $base, $location, $fileName, $cacheable, true); - } elseif (!is_string_empty($fileName)) { - $statusCode = $this->sendFile(200, $fileName, $cacheable); - } - if (!is_readable($fileName)) $this->page->error(404); - } - if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) { - echo "YellowCore::processRequest file:$fileName<br/>\n"; - } - return $statusCode; - } - - // Process request with error - public function processRequestError() { - ob_clean(); - $statusCode = $this->sendPage($this->page->scheme, $this->page->address, $this->page->base, - $this->page->location, $this->page->fileName, false, false); - if ($this->system->get("coreDebugMode")>=1) echo "YellowCore::processRequestError file:".$this->page->fileName."<br/>\n"; - return $statusCode; - } - - // Process fatal runtime error - public function processFatalError() { - $error = error_get_last(); - if (!is_null($error) && isset($error["type"]) && ($error["type"]==E_ERROR || $error["type"]==E_PARSE)) { - $fileNameAbsolute = isset($error["file"]) ? $error["file"] : ""; - $fileName = substru($fileNameAbsolute, strlenu($this->system->get("coreServerInstallDirectory"))); - $this->toolbox->log("error", "Can't parse file '$fileName'!"); - $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500)); - $troubleshooting = PHP_SAPI!="cli" ? - "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl(); - echo "<br/>\nDatenstrom Yellow stopped with fatal error. Activate the debug mode for more information. $troubleshooting\n"; - } - } - - // Show error message and terminate immediately - public function exitFatalError($errorMessage = "") { - $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500)); - $troubleshooting = PHP_SAPI!="cli" ? - "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl(); - echo "$errorMessage $troubleshooting\n"; - exit(1); - } - - // Send page response - public function sendPage($scheme, $address, $base, $location, $fileName, $cacheable, $showSource) { - $rawData = $showSource ? $this->toolbox->readFile($fileName) : $this->page->getRawDataError(); - $statusCode = max($this->page->statusCode, 200); - $errorMessage = $this->page->errorMessage; - $this->page = new YellowPage($this); - $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable); - $this->page->parseMeta($rawData, $statusCode, $errorMessage); - $this->language->set($this->page->get("language")); - $this->page->parseContent(); - $this->page->parsePage(); - $statusCode = $this->sendData($this->page->statusCode, $this->page->headerData, $this->page->outputData); - if ($this->system->get("coreDebugMode")>=1) { - foreach ($this->page->headerData as $key=>$value) { - echo "YellowCore::sendPage $key: $value<br/>\n"; - } - $language = $this->page->get("language"); - $layout = $this->page->get("layout"); - $theme = $this->page->get("theme"); - $parser = $this->page->get("parser"); - echo "YellowCore::sendPage language:$language layout:$layout theme:$theme parser:$parser<br/>\n"; - } - return $statusCode; - } - - // Send data response - public function sendData($statusCode, $headerData, $outputData) { - $lastModifiedFormatted = isset($headerData["Last-Modified"]) ? $headerData["Last-Modified"] : ""; - if ($statusCode==200 && !isset($headerData["Cache-Control"]) && $this->toolbox->isNotModified($lastModifiedFormatted)) { - $statusCode = 304; - $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); - } else { - $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); - foreach ($headerData as $key=>$value) { - $this->toolbox->sendHttpHeader("$key: $value"); - } - if (!is_null($outputData)) echo $outputData; - } - return $statusCode; - } - - // Send file response - public function sendFile($statusCode, $fileName, $cacheable) { - $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted($this->toolbox->getFileModified($fileName)); - if ($statusCode==200 && $cacheable && $this->toolbox->isNotModified($lastModifiedFormatted)) { - $statusCode = 304; - $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); - } else { - $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); - if (!$cacheable) $this->toolbox->sendHttpHeader("Cache-Control: no-cache, no-store"); - $this->toolbox->sendHttpHeader("Content-Type: ".$this->toolbox->getMimeContentType($fileName)); - $this->toolbox->sendHttpHeader("Last-Modified: ".$lastModifiedFormatted); - echo $this->toolbox->readFile($fileName); - } - return $statusCode; - } - - // Send status response - public function sendStatus($statusCode, $location = "") { - if (!is_string_empty($location)) $this->page->status($statusCode, $location); - $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); - foreach ($this->page->headerData as $key=>$value) { - $this->toolbox->sendHttpHeader("$key: $value"); - } - return $statusCode; - } - - // Handle command from command line - public function command($line = "") { - $statusCode = 0; - $this->toolbox->timerStart($time); - list($command, $text) = $this->lookup->getCommandInformation($line); - foreach ($this->extension->data as $key=>$value) { - if (method_exists($value["object"], "onCommand")) { - $this->lookup->commandHandler = $key; - $statusCode = $value["object"]->onCommand($command, $text); - if ($statusCode!=0) break; - } - } - if ($statusCode==0 && is_string_empty($command)) { - $lines = array(); - foreach ($this->extension->data as $key=>$value) { - if (method_exists($value["object"], "onCommandHelp")) { - $this->lookup->commandHandler = $key; - $output = $value["object"]->onCommandHelp(); - $lines = array_merge($lines, is_array($output) ? $output : array($output)); - } - } - usort($lines, "strnatcasecmp"); - $this->showCommandHelp($lines); - $statusCode = 200; - } - if ($statusCode==0) { - $this->lookup->commandHandler = "core"; - $statusCode = 400; - echo "Yellow $command: Command not found\n"; - } - $this->toolbox->timerStop($time); - if ($this->system->get("coreDebugMode")>=1) { - echo "YellowCore::command status:$statusCode time:$time ms<br/>\n"; - } - return $statusCode<400 ? 0 : 1; - } - - // Show command help - public function showCommandHelp($lines) { - echo "Datenstrom Yellow is for people who make small websites. https://datenstrom.se/yellow/\n"; - $lineCounter = 0; - foreach ($lines as $line) { - echo(++$lineCounter>1 ? " " : "Syntax: ")."php yellow.php $line\n"; - } - } - - // Handle startup - public function startup() { - if (isset($this->extension->data)) { - foreach ($this->extension->data as $key=>$value) { - if (method_exists($value["object"], "onStartup")) $value["object"]->onStartup(); - } - } - } - - // Handle shutdown - public function shutdown() { - if (isset($this->extension->data)) { - foreach ($this->extension->data as $key=>$value) { - if (method_exists($value["object"], "onShutdown")) $value["object"]->onShutdown(); - } - } - } - - // Include layout - public function layout($name, $arguments = null) { - $this->lookup->layoutArguments = func_get_args(); - $this->page->includeLayout($name); - } - - // Return layout arguments - public function getLayoutArguments($sizeMin = 9) { - return array_pad($this->lookup->layoutArguments, $sizeMin, null); - } -} - -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 (!isset($this->pages[$location])) { - $this->pages[$location] = array(); - $scheme = $this->yellow->page->scheme; - $address = $this->yellow->page->address; - $base = $this->yellow->page->base; - if (is_string_empty($location)) { - $rootLocations = $this->yellow->lookup->findContentRootLocations(); - foreach ($rootLocations as $rootLocation=>$rootFileName) { - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $rootLocation, $rootFileName, false); - $page->parseMeta(""); - array_push($this->pages[$location], $page); - } - } else { - if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowContent::scanLocation location:$location<br/>\n"; - $fileNames = $this->yellow->lookup->findChildrenFromContentLocation($location); - foreach ($fileNames as $fileName) { - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, - $this->yellow->lookup->findContentLocationFromFile($fileName), $fileName, false); - $page->parseMeta($this->yellow->toolbox->readFile($fileName, 4096)); - if (strlenb($page->rawData)<4096) $page->statusCode = 200; - array_push($this->pages[$location], $page); - } - } - } - return $this->pages[$location]; - } - - // Return page from, null if not found - public function find($location, $absoluteLocation = false) { - $found = false; - if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); - foreach ($this->scanLocation($this->getParentLocation($location)) as $page) { - if ($page->location==$location) { - $found = true; - break; - } - } - return $found ? $page : null; - } - - // Return page collection with all pages - public function index($showInvisible = false, $multiLanguage = false) { - $rootLocation = $multiLanguage ? "" : $this->getRootLocation($this->yellow->page->location); - return $this->getChildrenRecursive($rootLocation, $showInvisible); - } - - // 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 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)) { - $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(); - if ($this->yellow->system->get("coreMultiLanguageMode")) { - 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)) { - $pages->append($page); - } - } - return $pages; - } - - // Return child pages recursively - public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { - --$levelMax; - $pages = new YellowPageCollection($this->yellow); - foreach ($this->scanLocation($location) as $page) { - if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { - $pages->append($page); - } - if (!$this->yellow->lookup->isFileLocation($page->location) && $levelMax!=0) { - $pages->merge($this->getChildrenRecursive($page->location, $showInvisible, $levelMax)); - } - } - return $pages; - } - - // Return shared pages - public function getShared($location) { - $pages = new YellowPageCollection($this->yellow); - $sharedLocation = $this->getHomeLocation($location)."shared/"; - return $pages->merge($this->scanLocation($sharedLocation)); - } - - // Return root location - public function getRootLocation($location) { - $rootLocation = "root/"; - if ($this->yellow->system->get("coreMultiLanguageMode")) { - foreach ($this->scanLocation("") as $page) { - $token = substru($page->location, 4); - if ($token!="/" && substru($location, 0, strlenu($token))==$token) { - $rootLocation = "root$token"; - break; - } - } - } - return $rootLocation; - } - - // Return home location - public function getHomeLocation($location) { - return substru($this->getRootLocation($location), 4); - } - - // Return parent location - public function getParentLocation($location) { - $parentLocation = ""; - $token = rtrim(substru($this->getRootLocation($location), 4), "/"); - if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { - if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; - } - if (is_string_empty($parentLocation)) $parentLocation = "root$token/"; - return $parentLocation; - } - - // Return top-level location - public function getParentTopLocation($location) { - $parentTopLocation = ""; - $token = rtrim(substru($this->getRootLocation($location), 4), "/"); - if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; - if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/"; - return $parentTopLocation; - } -} - -class YellowMedia { - public $yellow; // access to API - public $files; // scanned files - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->files = array(); - } - - // Scan file system on demand - public function scanLocation($location) { - if (!isset($this->files[$location])) { - $this->files[$location] = array(); - $scheme = $this->yellow->page->scheme; - $address = $this->yellow->page->address; - $base = $this->yellow->system->get("coreServerBase"); - if (is_string_empty($location)) { - $fileNames = array($this->yellow->system->get("coreMediaDirectory")); - } else { - if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowMedia::scanLocation location:$location<br/>\n"; - $fileNames = $this->yellow->lookup->findChildrenFromMediaLocation($location); - } - foreach ($fileNames as $fileName) { - $file = new YellowPage($this->yellow); - $file->setRequestInformation($scheme, $address, $base, - $this->yellow->lookup->findMediaLocationFromFile($fileName), $fileName, false); - $file->parseMeta(null); - array_push($this->files[$location], $file); - } - } - return $this->files[$location]; - } - - // Return page with media file information, null if not found - public function find($location, $absoluteLocation = false) { - $found = false; - if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->system->get("coreServerBase"))); - foreach ($this->scanLocation($this->getParentLocation($location)) as $file) { - if ($file->location==$location) { - $found = true; - break; - } - } - return $found ? $file : null; - } - - // Return page collection with all media files - public function index($showInvisible = false, $multiPass = false) { - return $this->getChildrenRecursive("", $showInvisible); - } - - // 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)) { - $files->append($file); - } - } - return $files; - } - - // Return child files recursively - public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { - --$levelMax; - $files = new YellowPageCollection($this->yellow); - foreach ($this->scanLocation($location) as $file) { - if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) { - $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("coreMediaLocation"); - } - - // Return parent location - public function getParentLocation($location) { - $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); - if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { - if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; - } - if (is_string_empty($parentLocation)) $parentLocation = ""; - return $parentLocation; - } - - // Return top-level location - public function getParentTopLocation($location) { - $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); - if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; - if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/"; - return $parentTopLocation; - } -} - -class YellowSystem { - public $yellow; // access to API - public $modified; // system modification date - public $settings; // system settings - public $settingsDefaults; // system settings defaults - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->modified = 0; - $this->settings = new YellowArray(); - $this->settingsDefaults = new YellowArray(); - } - - // Load system settings from file - public function load($fileName) { - $this->modified = $this->yellow->toolbox->getFileModified($fileName); - $fileData = $this->yellow->toolbox->readFile($fileName); - $this->settings = $this->yellow->toolbox->getTextSettings($fileData, ""); - if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowSystem::load file:$fileName<br/>\n"; - if ($this->yellow->system->get("coreDebugMode")>=3) { - foreach ($this->settings as $key=>$value) { - echo "YellowSystem::load ".ucfirst($key).":$value<br/>\n"; - } - } - } - - // Save system settings to file - public function save($fileName, $settings) { - $this->modified = time(); - $settingsNew = new YellowArray(); - foreach ($settings as $key=>$value) { - if (!is_string_empty($key) && !is_string_empty($value)) { - $this->set($key, $value); - $settingsNew[$key] = $value; - } - } - $fileData = $this->yellow->toolbox->readFile($fileName); - $fileData = $this->yellow->toolbox->setTextSettings($fileData, "", "", $settingsNew); - return $this->yellow->toolbox->createFile($fileName, $fileData); - } - - // Set default system setting - public function setDefault($key, $value) { - $this->settingsDefaults[$key] = $value; - } - - // Set default system settings - public function setDefaults($lines) { - foreach ($lines as $line) { - if (preg_match("/^\#/", $line)) continue; - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { - $this->settingsDefaults[$matches[1]] = $matches[2]; - } - } - } - } - - // Set system setting - public function set($key, $value) { - $this->settings[$key] = $value; - } - - // Return system setting - public function get($key) { - if (isset($this->settings[$key])) { - $value = $this->settings[$key]; - } else { - $value = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : ""; - } - return $value; - } - - // Return system setting, HTML encoded - public function getHtml($key) { - return htmlspecialchars($this->get($key)); - } - - // Return different value for system setting - public function getDifferent($key) { - $array = array_diff($this->getAvailable($key), array($this->get($key))); - return reset($array); - } - - // Return available values for system setting - public function getAvailable($key) { - $values = array(); - $valueDefault = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : ""; - if ($key=="email") { - foreach ($this->yellow->user->settings as $userKey=>$userValue) { - array_push($values, $userKey); - } - } elseif ($key=="language") { - foreach ($this->yellow->language->settings as $languageKey=>$languageValue) { - array_push($values, $languageKey); - } - } elseif ($key=="layout") { - $path = $this->yellow->system->get("coreLayoutDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.html$/", true, false, false) as $entry) { - array_push($values, lcfirst(substru($entry, 0, -5))); - } - } elseif ($key=="theme") { - $path = $this->yellow->system->get("coreThemeDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false, false) as $entry) { - array_push($values, lcfirst(substru($entry, 0, -4))); - } - } - return !is_array_empty($values) ? $values : array($valueDefault); - } - public function getValues($key) { return $this->getAvailable($key); } //TODO: remove later, for backwards compatibility - - // Return system settings - public function getSettings($filterStart = "", $filterEnd = "") { - $settings = array(); - if (is_string_empty($filterStart) && is_string_empty($filterEnd)) { - $settings = array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()); - } else { - foreach (array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()) as $key=>$value) { - if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; - if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; - } - } - return $settings; - } - - // Return 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 isset($this->settings[$key]); - } -} - -class YellowLanguage { - public $yellow; // access to API - public $modified; // language modification date - public $settings; // language settings - public $settingsDefaults; // language settings defaults - public $language; // current language - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->modified = 0; - $this->settings = new YellowArray(); - $this->settingsDefaults = new YellowArray(); - $this->language = ""; - } - - // Load language settings from file - public function load($fileName) { - $this->modified = $this->yellow->toolbox->getFileModified($fileName); - $fileData = $this->yellow->toolbox->readFile($fileName); - $settings = $this->yellow->toolbox->getTextSettings($fileData, "language"); - foreach ($settings as $language=>$block) { - if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); - foreach ($block as $key=>$value) { - $this->settings[$language][$key] = $value; - } - } - if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowLanguage::load file:$fileName<br/>\n"; - foreach ($this->settings->getArrayCopy() as $key=>$value) { - if (!isset($this->settings[$key]["languageDescription"])) { - unset($this->settings[$key]); - } - } - $callback = function ($a, $b) { - return strnatcmp($a["languageDescription"], $b["languageDescription"]); - }; - $this->settings->uasort($callback); - } - - // Set current language - public function set($language) { - $this->language = $language; - } - - // Set default language setting - public function setDefault($key, $value, $language) { - if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); - $this->settings[$language][$key] = $value; - $this->settingsDefaults[$key] = true; - } - - // Set default language settings - public function setDefaults($lines) { - $language = ""; - foreach ($lines as $line) { - if (preg_match("/^\#/", $line)) continue; - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])=="language" && !is_string_empty($matches[2])) { - $language = $matches[2]; - if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); - } - if (!is_string_empty($language) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) { - $this->settings[$language][$matches[1]] = $matches[2]; - $this->settingsDefaults[$matches[1]] = true; - } - } - } - } - - // Set language setting - public function setText($key, $value, $language) { - if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); - $this->settings[$language][$key] = $value; - } - - // Return language setting - public function getText($key, $language = "") { - if (is_string_empty($language)) $language = $this->language; - return $this->isText($key, $language) ? $this->settings[$language][$key] : "[$key]"; - } - - // Return language setting, HTML encoded - public function getTextHtml($key, $language = "") { - return htmlspecialchars($this->getText($key, $language)); - } - - // Return text as language specific date, convert to one of the standard formats - public function getDateStandard($text, $language = "") { - if (preg_match("/^\d+$/", $text)) { - $output = $text; - } elseif (preg_match("/^\d+\-\d+$/", $text)) { - $format = $this->getText("coreDateFormatShort", $language); - $output = $this->getDateFormatted(strtotime($text), $format, $language); - } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) { - $format = $this->getText("coreDateFormatMedium", $language); - $output = $this->getDateFormatted(strtotime($text), $format, $language); - } else { - $format = $this->getText("coreDateFormatLong", $language); - $output = $this->getDateFormatted(strtotime($text), $format, $language); - } - return $output; - } - - // Return Unix time as date, relative to today - public function getDateRelative($timestamp, $format, $daysLimit, $language = "") { - $timeDifference = mktime(0, 0, 0) - strtotime(date("Y-m-d", $timestamp)); - $days = abs(intval($timeDifference/86400)); - $key = $timeDifference>=0 ? "coreDatePast" : "coreDateFuture"; - $tokens = preg_split("/\s*,\s*/", $this->getText($key, $language)); - if (count($tokens)>=8) { - if ($days<=$daysLimit || $daysLimit==0) { - if ($days==0) { - $output = $tokens[0]; - } elseif ($days==1) { - $output = $tokens[1]; - } elseif ($days>=2 && $days<=29) { - $output = preg_replace("/@x/i", $days, $tokens[2]); - } elseif ($days>=30 && $days<=59) { - $output = $tokens[3]; - } elseif ($days>=60 && $days<=364) { - $output = preg_replace("/@x/i", intval($days/30), $tokens[4]); - } elseif ($days>=365 && $days<=729) { - $output = $tokens[5]; - } else { - $output = preg_replace("/@x/i", intval($days/365.25), $tokens[6]); - } - } else { - $output = preg_replace("/@x/i", $this->getDateFormatted($timestamp, $format, $language), $tokens[7]); - } - } else { - $output = "[$key]"; - } - return $output; - } - - // Return Unix time as date - public function getDateFormatted($timestamp, $format, $language = "") { - $dateMonthsNominative = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsNominative", $language)); - $dateMonthsGenitive = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsGenitive", $language)); - $dateWeekdays = preg_split("/\s*,\s*/", $this->getText("coreDateWeekdays", $language)); - $monthNominative = $dateMonthsNominative[date("n", $timestamp) - 1]; - $monthGenitive = $dateMonthsGenitive[date("n", $timestamp) - 1]; - $weekday = $dateWeekdays[date("N", $timestamp) - 1]; - $timeZone = $this->yellow->system->get("coreTimezone"); - $timeZoneHelper = new DateTime("now", new DateTimeZone($timeZone)); - $timeZoneOffset = $timeZoneHelper->getOffset(); - $timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600)); - $format = preg_replace("/(?<!\\\)F/", addcslashes($monthNominative, "A..Za..z"), $format); - $format = preg_replace("/(?<!\\\)V/", addcslashes($monthGenitive, "A..Za..z"), $format); - $format = preg_replace("/(?<!\\\)M/", addcslashes(substru($monthNominative, 0, 3), "A..Za..z"), $format); - $format = preg_replace("/(?<!\\\)D/", addcslashes(substru($weekday, 0, 3), "A..Za..z"), $format); - $format = preg_replace("/(?<!\\\)l/", addcslashes($weekday, "A..Za..z"), $format); - $format = preg_replace("/(?<!\\\)T/", addcslashes($timeZoneAbbreviation, "A..Za..z"), $format); - return date($format, $timestamp); - } - - // Return language settings - public function getSettings($filterStart = "", $filterEnd = "", $language = "") { - $settings = array(); - if (is_string_empty($language)) $language = $this->language; - if (isset($this->settings[$language])) { - if (is_string_empty($filterStart) && is_string_empty($filterEnd)) { - $settings = $this->settings[$language]->getArrayCopy(); - } else { - foreach ($this->settings[$language] as $key=>$value) { - if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; - if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; - } - } - } - return $settings; - } - - // Return language settings modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; - } - - // Check if language setting exists - public function isText($key, $language = "") { - if (is_string_empty($language)) $language = $this->language; - return isset($this->settings[$language]) && isset($this->settings[$language][$key]); - } - - // Check if language exists - public function isExisting($language) { - return isset($this->settings[$language]); - } -} - -class YellowUser { - public $yellow; // access to API - public $modified; // user modification date - public $settings; // user settings - public $email; // current email - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->modified = 0; - $this->settings = new YellowArray(); - $this->email = ""; - } - - // Load user settings from file - public function load($fileName) { - $this->modified = $this->yellow->toolbox->getFileModified($fileName); - $fileData = $this->yellow->toolbox->readFile($fileName); - $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "email"); - if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUser::load file:$fileName<br/>\n"; - } - - // Save user settings to file - public function save($fileName, $email, $settings) { - $this->modified = time(); - $settingsNew = new YellowArray(); - $settingsNew["email"] = $email; - foreach ($settings as $key=>$value) { - if (!is_string_empty($key) && !is_string_empty($value)) { - $this->setUser($key, $value, $email); - $settingsNew[$key] = $value; - } - } - $fileData = $this->yellow->toolbox->readFile($fileName); - $fileData = $this->yellow->toolbox->setTextSettings($fileData, "email", $email, $settingsNew); - return $this->yellow->toolbox->createFile($fileName, $fileData); - } - - // Remove user settings from file - public function remove($fileName, $email) { - $this->modified = time(); - if (isset($this->settings[$email])) unset($this->settings[$email]); - $fileData = $this->yellow->toolbox->readFile($fileName); - $fileData = $this->yellow->toolbox->unsetTextSettings($fileData, "email", $email); - return $this->yellow->toolbox->createFile($fileName, $fileData); - } - - // Set current email - public function set($email) { - $this->email = $email; - } - - // Set user setting - public function setUser($key, $value, $email) { - if (!isset($this->settings[$email])) $this->settings[$email] = new YellowArray(); - $this->settings[$email][$key] = $value; - } - - // Return user setting - public function getUser($key, $email = "") { - if (is_string_empty($email)) $email = $this->email; - return isset($this->settings[$email]) && isset($this->settings[$email][$key]) ? $this->settings[$email][$key] : ""; - } - - // Return user setting, HTML encoded - public function getUserHtml($key, $email = "") { - return htmlspecialchars($this->getUser($key, $email)); - } - - // Return user settings - public function getSettings($email = "") { - $settings = array(); - if (is_string_empty($email)) $email = $this->email; - if (isset($this->settings[$email])) $settings = $this->settings[$email]->getArrayCopy(); - return $settings; - } - - // Return user settings modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; - } - - // Check if user setting exists - public function isUser($key, $email = "") { - if (is_string_empty($email)) $email = $this->email; - return isset($this->settings[$email]) && isset($this->settings[$email][$key]); - } - - // Check if user exists - public function isExisting($email) { - return isset($this->settings[$email]); - } -} - -class YellowExtension { - public $yellow; // access to API - public $modified; // extension modification date - public $data; // extension data - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->modified = 0; - $this->data = array(); - } - - // Load extensions - public function load($path) { - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) { - $this->modified = max($this->modified, $this->yellow->toolbox->getFileModified($entry)); - require_once($entry); - $name = $this->yellow->lookup->normaliseName(basename($entry), true, true); - $this->register(lcfirst($name), "Yellow".ucfirst($name)); - if ($this->yellow->system->get("coreDebugMode")>=3) echo "YellowExtension::load file:$entry<br/>\n"; - } - $callback = function ($a, $b) { - return $a["priority"] - $b["priority"]; - }; - uasort($this->data, $callback); - foreach ($this->data as $key=>$value) { - if (method_exists($this->data[$key]["object"], "onLoad")) $this->data[$key]["object"]->onLoad($this->yellow); - } - } - - // Register extension - public function register($key, $class) { - if (!$this->isExisting($key) && class_exists($class)) { - $this->data[$key] = array(); - $this->data[$key]["object"] = $class=="YellowCore" ? new stdClass : new $class; - $this->data[$key]["class"] = $class; - $this->data[$key]["version"] = defined("$class::VERSION") ? $class::VERSION : 0; - $this->data[$key]["priority"] = defined("$class::PRIORITY") ? $class::PRIORITY : count($this->data) + 10; - } - } - - // Return extension - public function get($key) { - return $this->data[$key]["object"]; - } - - // 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($key) { - return isset($this->data[$key]); - } -} - -class YellowLookup { - public $yellow; // access to API - public $requestHandler; // request handler name - public $commandHandler; // command handler name - public $layoutArguments; // layout arguments - - public function __construct($yellow) { - $this->yellow = $yellow; - } - - // Return file system information - public function findFileSystemInformation() { - $pathInstall = substru(__DIR__, 0, 1-strlenu($this->yellow->system->get("coreExtensionDirectory"))); - $pathBase = $this->yellow->system->get("coreContentDirectory"); - $pathRoot = $this->yellow->system->get("coreMultiLanguageMode") ? "default/" : ""; - $pathHome = "home/"; - if (!is_string_empty($pathRoot)) { - $firstRoot = ""; - $token = $root = rtrim($pathRoot, "/"); - foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { - if (is_string_empty($firstRoot)) $firstRoot = $token = $entry; - if ($this->normaliseToken($entry)==$root) { - $token = $entry; - break; - } - } - $pathRoot = $this->normaliseToken($token)."/"; - $pathBase .= "$firstRoot/"; - } - if (!is_string_empty($pathHome)) { - $firstHome = ""; - $token = $home = rtrim($pathHome, "/"); - foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { - if (is_string_empty($firstHome)) $firstHome = $token = $entry; - if ($this->normaliseToken($entry)==$home) { - $token = $entry; - break; - } - } - $pathHome = $this->normaliseToken($token)."/"; - } - return array($pathInstall, $pathRoot, $pathHome); - } - - // Return content language - public function findContentLanguage($fileName, $languageDefault) { - $language = $languageDefault; - $pathBase = $this->yellow->system->get("coreContentDirectory"); - $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); - if (!is_string_empty($pathRoot)) { - $fileName = substru($fileName, strlenu($pathBase)); - if (preg_match("/^(.+?)\//", $fileName, $matches)) { - $name = $this->normaliseToken($matches[1]); - if (strlenu($name)==2) $language = $name; - } - } - return $language; - } - - // Return content root locations - public function findContentRootLocations() { - $rootLocations = array(); - $pathBase = $this->yellow->system->get("coreContentDirectory"); - $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); - if (!is_string_empty($pathRoot)) { - foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { - $token = $this->normaliseToken($entry)."/"; - if ($token==$pathRoot) $token = ""; - $rootLocations["root/$token"] = "$pathBase$entry/"; - } - } else { - $rootLocations["root/"] = "$pathBase"; - } - if ($this->yellow->system->get("coreDebugMode")>=3) { - foreach ($rootLocations as $key=>$key) { - echo "YellowLookup::findContentRootLocations $key -> $value<br/>\n"; - } - } - return $rootLocations; - } - - // Return content location from file path - public function findContentLocationFromFile($fileName) { - $invalid = false; - $location = "/"; - $pathBase = $this->yellow->system->get("coreContentDirectory"); - $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); - $pathHome = $this->yellow->system->get("coreServerHomeDirectory"); - $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); - $fileExtension = $this->yellow->system->get("coreContentExtension"); - if (substru($fileName, 0, strlenu($pathBase))==$pathBase && mb_check_encoding($fileName, "UTF-8")) { - $fileName = substru($fileName, strlenu($pathBase)); - $tokens = explode("/", $fileName); - if (!is_string_empty($pathRoot)) { - $token = $this->normaliseToken($tokens[0])."/"; - if ($token!=$pathRoot) $location .= $token; - array_shift($tokens); - } - for ($i=0; $i<count($tokens)-1; ++$i) { - $token = $this->normaliseToken($tokens[$i])."/"; - if ($i || $token!=$pathHome) $location .= $token; - } - $token = $this->normaliseToken($tokens[$i], $fileExtension); - if ($token!=$fileDefault) { - $location .= $this->normaliseToken($tokens[$i], $fileExtension, true); - } - $extension = ($pos = strrposu($fileName, ".")) ? substru($fileName, $pos) : ""; - if ($extension!=$fileExtension) $invalid = true; - } else { - $invalid = true; - } - if ($this->yellow->system->get("coreDebugMode")>=2) { - $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName"; - echo "YellowLookup::findContentLocationFromFile $debug<br/>\n"; - } - return $invalid ? "" : $location; - } - - // Return file path from content location - public function findFileFromContentLocation($location, $directory = false) { - $found = $invalid = false; - $path = $this->yellow->system->get("coreContentDirectory"); - $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); - $pathHome = $this->yellow->system->get("coreServerHomeDirectory"); - $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); - $fileExtension = $this->yellow->system->get("coreContentExtension"); - $tokens = explode("/", $location); - if ($this->isRootLocation($location)) { - if (!is_string_empty($pathRoot)) { - $token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, "/"); - $path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid); - } - } else { - if (!is_string_empty($pathRoot)) { - if (count($tokens)>2) { - if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathRoot, "/"))) $invalid = true; - $path .= $this->findFileDirectory($path, $tokens[1], "", true, false, $found, $invalid); - if ($found) array_shift($tokens); - } - if (!$found) { - $path .= $this->findFileDirectory($path, rtrim($pathRoot, "/"), "", true, true, $found, $invalid); - } - } - if (count($tokens)>2) { - if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathHome, "/"))) $invalid = true; - for ($i=1; $i<count($tokens)-1; ++$i) { - $path .= $this->findFileDirectory($path, $tokens[$i], "", true, true, $found, $invalid); - } - } else { - $i = 1; - $tokens[0] = rtrim($pathHome, "/"); - $path .= $this->findFileDirectory($path, $tokens[0], "", true, true, $found, $invalid); - } - if (!$directory) { - if (!is_string_empty($tokens[$i])) { - $token = $tokens[$i].$fileExtension; - if ($token==$fileDefault) $invalid = true; - $path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid); - } else { - $path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false); - } - if ($this->yellow->system->get("coreDebugMode")>=2) { - $debug = "$location -> ".($invalid ? "INVALID" : $path); - echo "YellowLookup::findFileFromContentLocation $debug<br/>\n"; - } - } - } - return $invalid ? "" : $path; - } - - // Return children from content location - public function findChildrenFromContentLocation($location) { - $fileNames = array(); - if (!$this->isFileLocation($location)) { - $path = $this->findFileFromContentLocation($location, true); - $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); - $fileExtension = $this->yellow->system->get("coreContentExtension"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { - $token = $this->findFileDefault($path.$entry, $fileDefault, $fileExtension, false); - array_push($fileNames, $path.$entry."/".$token); - } - if (!$this->isRootLocation($location)) { - $regex = "/^.*\\".$fileExtension."$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { - if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue; - array_push($fileNames, $path.$entry); - } - } - } - return $fileNames; - } - - // Return media location from file path - public function findMediaLocationFromFile($fileName) { - $location = ""; - $extensionDirectoryLength = strlenu($this->yellow->system->get("coreExtensionDirectory")); - $themeDirectoryLength = strlenu($this->yellow->system->get("coreThemeDirectory")); - $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); - if (substru($fileName, 0, $extensionDirectoryLength)==$this->yellow->system->get("coreExtensionDirectory")) { - if ($this->isSafeFile($fileName)) { - $location = $this->yellow->system->get("coreExtensionLocation").substru($fileName, $extensionDirectoryLength); - } - } elseif (substru($fileName, 0, $themeDirectoryLength)==$this->yellow->system->get("coreThemeDirectory")) { - if ($this->isSafeFile($fileName)) { - $location = $this->yellow->system->get("coreThemeLocation").substru($fileName, $themeDirectoryLength); - } - } elseif (substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory")) { - $location = "/".$fileName; - } - return $location; - } - - // Return file path from media location - public function findFileFromMediaLocation($location) { - $fileName = ""; - $extensionLocationLength = strlenu($this->yellow->system->get("coreExtensionLocation")); - $themeLocationLength = strlenu($this->yellow->system->get("coreThemeLocation")); - $mediaLocationLength = strlenu($this->yellow->system->get("coreMediaLocation")); - if (substru($location, 0, $extensionLocationLength)==$this->yellow->system->get("coreExtensionLocation")) { - if ($this->isSafeFile($location)) { - $fileName = $this->yellow->system->get("coreExtensionDirectory").substru($location, $extensionLocationLength); - } - } elseif (substru($location, 0, $themeLocationLength)==$this->yellow->system->get("coreThemeLocation")) { - if ($this->isSafeFile($location)) { - $fileName = $this->yellow->system->get("coreThemeDirectory").substru($location, $themeLocationLength); - } - } elseif (substru($location, 0, $mediaLocationLength)==$this->yellow->system->get("coreMediaLocation")) { - $fileName = substru($location, 1); - } - return $fileName; - } - - // Return children from media location - public function findChildrenFromMediaLocation($location) { - $fileNames = array(); - if (!$this->isFileLocation($location)) { - $path = $this->findFileFromMediaLocation($location); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, true) as $entry) { - array_push($fileNames, $entry."/"); - } - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, false, true) as $entry) { - array_push($fileNames, $entry); - } - } - return $fileNames; - } - - // Return media directory from a system setting - public function findMediaDirectory($key) { - return substru($key, -8, 8)=="Location" ? $this->findFileFromMediaLocation($this->yellow->system->get($key)) : ""; - } - - // Return file or directory that matches token - public function findFileDirectory($path, $token, $fileExtension, $directory, $default, &$found, &$invalid) { - if ($this->normaliseToken($token, $fileExtension)!=$token) $invalid = true; - if (!$invalid) { - $regex = "/^[\d\-\_\.]*".str_replace("-", ".", $token)."$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, $directory, false) as $entry) { - if ($this->normaliseToken($entry, $fileExtension)==$token) { - $token = $entry; - $found = true; - break; - } - } - } - if ($directory) $token .= "/"; - return ($default || $found) ? $token : ""; - } - - // Return default file in directory - public function findFileDefault($path, $fileDefault, $fileExtension, $includePath = true) { - $token = $fileDefault; - if (!is_file($path."/".$fileDefault)) { - $regex = "/^[\d\-\_\.]*($fileDefault)$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { - if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) { - $token = $entry; - break; - } - } - } - return $includePath ? "$path/$token" : $token; - } - - // Normalise file/directory token - public function normaliseToken($text, $fileExtension = "", $removeExtension = false) { - if (!is_string_empty($fileExtension)) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; - if (preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_empty($matches[1])) $text = $matches[1]; - return preg_replace("/[^\pL\d\-\_]/u", "-", $text).($removeExtension ? "" : $fileExtension); - } - - // Normalise name - public function normaliseName($text, $removePrefix = false, $removeExtension = false, $filterStrict = false) { - if ($removeExtension) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; - if ($removePrefix && preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_empty($matches[1])) $text = $matches[1]; - if ($filterStrict) $text = strtoloweru($text); - return preg_replace("/[^\pL\d\-\_]/u", "-", $text); - } - - // Normalise prefix - public function normalisePrefix($text) { - $prefix = ""; - if (preg_match("/^([\d\-\_\.]*)(.*)$/", $text, $matches)) $prefix = $matches[1]; - if (!is_string_empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-"; - return $prefix; - } - - // Normalise elements and attributes in HTML/SVG data - public function normaliseData($text, $type = "html", $filterStrict = true) { - $output = ""; - $elementsHtml = array( - "a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"); - $elementsSvg = array( - "svg", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern"); - $attributesHtml = array( - "accept", "action", "align", "allow", "allowfullscreen", "alt", "autocomplete", "autoplay", "background", "bgcolor", "border", "cellpadding", "cellspacing", "charset", "checked", "cite", "class", "clear", "color", "cols", "colspan", "content", "contenteditable", "controls", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "frameborder", "headers", "height", "hidden", "high", "href", "hreflang", "id", "integrity", "ismap", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "muted", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "poster", "prefix", "preload", "property", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "sandbox", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "width", "xmlns"); - $attributesSvg = array( - "accent-height", "accumulate", "additivive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "datenstrom", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "tabindex", "targetx", "targety", "transform", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xlink:href", "xml:id", "xml:space", "xmlns", "y", "y1", "y2", "z", "zoomandpan"); - $attributesAllowEmptyString = array("alt", "download", "sandbox", "value"); - $elementsSafe = $elementsHtml; - $attributesSafe = $attributesHtml; - if ($type=="svg") { - $elementsSafe = array_merge($elementsHtml, $elementsSvg); - $attributesSafe = array_merge($attributesHtml, $attributesSvg); - } - $offsetBytes = 0; - while (true) { - $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); - $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); - $elementStart = $elementFound ? $matches[1][0] : ""; - $elementName = $elementFound ? $matches[2][0]: ""; - $elementMiddle = $elementFound ? $matches[3][0]: ""; - $elementEnd = $elementFound ? $matches[4][0]: ""; - $output .= $elementBefore; - if (substrb($elementName, 0, 1)=="!") { - $output .= "<$elementName$elementMiddle>"; - } elseif (in_array(strtolower($elementName), $elementsSafe)) { - $elementAttributes = $this->getTextAttributes($elementMiddle, $attributesAllowEmptyString); - foreach ($elementAttributes as $key=>$value) { - if (!in_array(strtolower($key), $attributesSafe) && !preg_match("/^(aria|data)-/i", $key)) { - unset($elementAttributes[$key]); - } - } - if ($filterStrict) { - $href = isset($elementAttributes["href"]) ? $elementAttributes["href"] : ""; - if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) { - $elementAttributes["href"] = "error-xss-filter"; - } - $href = isset($elementAttributes["xlink:href"]) ? $elementAttributes["xlink:href"] : ""; - if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) { - $elementAttributes["xlink:href"] = "error-xss-filter"; - } - } - $output .= "<$elementStart$elementName"; - foreach ($elementAttributes as $key=>$value) $output .= " $key=\"$value\""; - if (!is_string_empty($elementEnd)) $output .= " "; - $output .= "$elementEnd>"; - } - if (!$elementFound) break; - $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); - } - return $output; - } - - // Normalise name and email for a single address - public function normaliseAddress($input, $type = "mail", $filterStrict = true) { - $output = ""; - if ($type=="mail") { - if (preg_match("/^(.*?)(\s*)<(.*?)>$/", $input, $matches)) { - $name = $matches[1]; - $email = $matches[3]; - } else { - $name = ""; - $email = $input; - } - $name = preg_replace("/[^\pL\d\-\. ]/u", "", $name); - $name = preg_replace("/\s+/s", " ", $name); - if ($filterStrict && !preg_match("/^[\w\+\-\.\@]+$/", $email)) { - $email = "error-mail-address"; - } - $output = is_string_empty($name) ? "<$email>" : "$name <$email>"; - } - return $output; - } - - // Normalise fields in MIME headers - public function normaliseHeaders($input, $type = "mime", $filterStrict = true) { - $output = ""; - if ($type=="mime") { - $keysMixedEncoding = array("To", "From", "Reply-To", "Cc", "Bcc"); - foreach ($input as $key=>$value) { - $key = ucwords(preg_replace("/[^a-zA-Z\-]/u", "-", $key), "-"); - if (in_array($key, $keysMixedEncoding)) { - $text = "$key: "; - foreach (preg_split("/\s*,\s*/", $value) as $email) { - if (!preg_match("/^(.*?)(\s*)<(.*?)>$/", $email, $matches)) { - $matches[1] = $matches[2] = ""; - $matches[3] = $email; - } - if (!is_string_empty($matches[1]) && !preg_match("/^[\pL\d\-\. ]+$/u", $matches[1])) { - $matches[1] = $matches[2] = ""; - $matches[3] = "error-mail-address"; - } - if ($filterStrict && !preg_match("/^[\w\+\-\.\@]+$/", $matches[3])) { - $matches[3] = "error-mail-address"; - } - if (substru($text, -2, 2)!=": ") $text .= ",\r\n "; - $text = $this->getMimeHeader($text, $matches[1]); - $text = $this->getMimeHeader($text, "$matches[2]<$matches[3]>", false); - } - $text .= "\r\n"; - } else { - $text = $this->getMimeHeader("$key: ", $value)."\r\n"; - } - $output .= $text; - } - } - return $output; - } - - // Normalise relative path tokens - public function normalisePath($text) { - $textFiltered = ""; - $textLength = strlenb($text); - for ($pos=0; $pos<$textLength; ++$pos) { - if ($text[$pos]=="." && ($pos==0 || $text[$pos-1]=="/")) { - while ($text[$pos]==".") ++$pos; - if ($text[$pos]=="/") ++$pos; - --$pos; - continue; - } - $textFiltered .= $text[$pos]; - } - return $textFiltered; - } - - // 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; - } - - // 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; - } - - // Normalise location, make absolute location - public function normaliseLocation($location, $pageLocation, $filterStrict = true) { - if (!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) { - $pageBase = $this->yellow->page->base; - $mediaBase = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreMediaLocation"); - if (!preg_match("/^\#/", $location)) { - if (!preg_match("/^\//", $location)) { - $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; - } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) { - $location = $pageBase.$location; - } - } else { - $location = $pageBase.$pageLocation.$location; - } - $location = str_replace("/./", "/", $location); - $location = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $location); - } else { - if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter"; - } - return $location; - } - - // Normalise location arguments - public function normaliseArguments($text, $appendSlash = true, $filterStrict = true) { - if ($appendSlash) $text .= "/"; - if ($filterStrict) $text = str_replace(" ", "-", strtoloweru($text)); - $text = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $text); - return str_replace(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text)); - } - - // Normalise URL, make absolute URL - public function normaliseUrl($scheme, $address, $base, $location, $filterStrict = true) { - if (!preg_match("/^\w+:/", $location)) { - $url = "$scheme://$address$base$location"; - } else { - if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter"; - $url = $location; - } - return $url; - } - - // Return URL information - public function getUrlInformation($url) { - $scheme = $address = $base = ""; - if (preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, "/"), $matches)) { - $scheme = $matches[1]; - $address = $matches[2]; - $base = $matches[3]; - } - return array($scheme, $address, $base); - } - - // Return request information - public function getRequestInformation($scheme = "", $address = "", $base = "") { - if (is_string_empty($scheme) && is_string_empty($address) && is_string_empty($base)) { - $url = $this->yellow->system->get("coreServerUrl"); - if ($url=="auto" || $this->isCommandLine()) $url = $this->yellow->toolbox->detectServerUrl(); - list($scheme, $address, $base) = $this->getUrlInformation($url); - $this->yellow->system->set("coreServerScheme", $scheme); - $this->yellow->system->set("coreServerAddress", $address); - $this->yellow->system->set("coreServerBase", $base); - if ($this->yellow->system->get("coreDebugMode")>=3) { - echo "YellowLookup::getRequestInformation $scheme://$address$base<br/>\n"; - } - } - $location = substru($this->yellow->toolbox->detectServerLocation(), strlenu($base)); - $fileName = ""; - if (is_string_empty($fileName)) $fileName = $this->findFileFromMediaLocation($location); - if (is_string_empty($fileName)) $fileName = $this->findFileFromContentLocation($location); - return array($scheme, $address, $base, $location, $fileName); - } - - // Return command information - public function getCommandInformation($line = "") { - if (is_string_empty($line)) { - $line = $this->yellow->toolbox->getTextString(array_slice($this->yellow->toolbox->getServer("argv"), 1)); - if ($this->yellow->system->get("coreDebugMode")>=3) { - echo "YellowLookup::getCommandInformation $line<br/>\n"; - } - } - return $this->yellow->toolbox->getTextList($line, " ", 2); - } - - // Return request handler - public function getRequestHandler() { - return $this->requestHandler; - } - - // Return command handler - public function getCommandHandler() { - return $this->commandHandler; - } - - // Return attributes from text - public function getTextAttributes($text, $attributesAllowEmptyString) { - $tokens = array(); - $posStart = $posQuote = 0; - $textLength = strlenb($text); - for ($pos=0; $pos<$textLength; ++$pos) { - if ($text[$pos]==" " && !$posQuote) { - if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); - $posStart = $pos+1; - } - if ($text[$pos]=="=" && !$posQuote) { - if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); - array_push($tokens, "="); - $posStart = $pos+1; - } - if ($text[$pos]=="\"") { - if ($posQuote) { - if ($pos>$posQuote) array_push($tokens, substrb($text, $posQuote+1, $pos-$posQuote-1)); - $posQuote = 0; - $posStart = $pos+1; - } else { - if ($pos==$posStart) $posQuote = $pos; - } - } - } - if ($pos>$posStart && !$posQuote) { - array_push($tokens, substrb($text, $posStart, $pos-$posStart)); - } - $attributes = array(); - for ($i=0; $i<count($tokens); ++$i) { - if ($i+2<count($tokens) && $tokens[$i+1]=="=") { - $key = $tokens[$i]; - $value = $tokens[$i+2]; - $i += 2; - } else { - $key = $value = $tokens[$i]; - } - if (!is_string_empty($key) && (!is_string_empty($value) || in_array(strtolower($key), $attributesAllowEmptyString))) { - $attributes[$key] = $value; - } - } - return $attributes; - } - - // Return HTML attributes from generic Markdown attributes - public function getHtmlAttributes($text) { - $htmlAttributes = ""; - $htmlAttributesData = array(); - foreach (explode(" ", $text) as $token) { - if (substru($token, 0, 1)==".") { - if (!isset($htmlAttributesData["class"])) { - $htmlAttributesData["class"] = substru($token, 1); - } else { - $htmlAttributesData["class"] .= " ".substru($token, 1); - } - } - if (substru($token, 0, 1)=="#") $htmlAttributesData["id"] = substru($token, 1); - if (preg_match("/^([\w]+)=(.+)/", $token, $matches)) $htmlAttributesData[$matches[1]] = $matches[2]; - } - foreach ($htmlAttributesData as $key=>$value) { - $htmlAttributes .= " $key=\"".htmlspecialchars($value)."\""; - } - return $htmlAttributes; - } - - // Return MIME header field, encode and fold if necessary - public function getMimeHeader($text, $field, $allowEncode = true) { - if ($allowEncode) { - $encode = preg_match("/[\x7F-\xFF]/", $field); - $fieldPos = 0; - while (true) { - $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0); - $bytesAvailable = max(0, 78-$textPos); - $fragment = substrb($field, $fieldPos); - if ($encode && !is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?="; - if ($bytesAvailable<strlenb($fragment)) { - $bytesHandled = $bytesAvailable; - if (!$encode) { - for ($pos=$bytesHandled;$pos>0;--$pos) { - if ($field[$fieldPos+$pos]==" ") { - $fragment = substrb($field, $fieldPos, $pos); - $bytesHandled = $pos+1; - break; - } - } - if ($pos==0) $encode = true; - } - if ($encode) { - while (true) { - $fragment = substrb($field, $fieldPos, $bytesHandled); - if (!is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?="; - if ($bytesAvailable>=strlenb($fragment) || $bytesHandled==0) break; - --$bytesHandled; - } - } - $text .= $fragment."\r\n "; - $fieldPos += $bytesHandled; - } else { - $text .= $fragment; - break; - } - } - } else { - $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0); - $bytesAvailable = max(0, 78-$textPos); - if ($bytesAvailable<strlenb($field)) { - $text .= "\r\n ".ltrim($field); - } else { - $text .= $field; - } - } - return $text; - } - - // Return directory location - public function getDirectoryLocation($location) { - return ($pos = strrposu($location, "/")) ? substru($location, 0, $pos+1) : "/"; - } - - // Return redirect location - public function getRedirectLocation($location) { - if ($this->isFileLocation($location)) { - $location = "$location/"; - } else { - $languageDefault = $this->yellow->system->get("language"); - $language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->content->getLanguages(), $languageDefault); - $location = "/$language/"; - } - return $location; - } - - // Check if clean URL is requested - public function isRequestCleanUrl($location) { - return isset($_REQUEST["clean-url"]) && substru($location, -1, 1)=="/"; - } - - // Check if location is specifying root - public function isRootLocation($location) { - return substru($location, 0, 1)!="/"; - } - - // 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->findFileFromContentLocation("$location/", true)); - } elseif ($location=="/") { - $redirect = $this->yellow->system->get("coreMultiLanguageMode"); - } - 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 (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true; - } - return $nested; - } - - // Check if location is within shared directory - public function isSharedLocation($location) { - $sharedLocation = $this->yellow->content->getHomeLocation($location)."shared/"; - return substru($location, 0, strlenu($sharedLocation))==$sharedLocation; - } - - // Check if location is within current HTTP request - public function isActiveLocation($location, $currentLocation) { - if ($this->isFileLocation($location)) { - $active = $currentLocation==$location; - } else { - if ($location==$this->yellow->content->getHomeLocation($location)) { - $active = $this->getDirectoryLocation($currentLocation)==$location; - } else { - $active = substru($currentLocation, 0, strlenu($location))==$location; - } - } - return $active; - } - - // Check if URL is a well-known URL scheme - public function isSafeUrl($url) { - return preg_match("/^(http|https|ftp|mailto|tel):/", $url); - } - - // Check if file is a well-known file type - public function isSafeFile($fileName) { - return preg_match("/\.(css|gif|ico|js|jpg|map|png|scss|svg|woff|woff2)$/", $fileName); - } - - // Check if file is valid - public function isValidFile($fileName) { - $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); - $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); - $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); - return strposu($fileName, "/")===false || - substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory") || - substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory") || - substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); - } - - // Check if content file - public function isContentFile($fileName) { - $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); - return substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory"); - } - - // Check if media file - public function isMediaFile($fileName) { - $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); - return substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory"); - } - - // Check if system file - public function isSystemFile($fileName) { - $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); - return substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); - } - - // Check if running at command line - public function isCommandLine() { - return isset($this->commandHandler); - } -} - -class YellowToolbox { - public $yellow; // access to API - - public function __construct($yellow) { - $this->yellow = $yellow; - } - - // Return browser cookie from from current HTTP request - public function getCookie($key) { - return isset($_COOKIE[$key]) ? $_COOKIE[$key] : ""; - } - - // Return server argument from current HTTP request - public function getServer($key) { - return isset($_SERVER[$key]) ? $_SERVER[$key] : ""; - } - - // Return location arguments from current HTTP request - public function getLocationArguments() { - return $this->getServer("LOCATION_ARGUMENTS"); - } - - // Return location arguments from current HTTP request, modify existing arguments - public function getLocationArgumentsNew($key, $value) { - $locationArguments = ""; - $found = false; - $separator = $this->getLocationArgumentsSeparator(); - foreach (explode("/", $this->getServer("LOCATION_ARGUMENTS")) as $token) { - if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { - if ($matches[1]==$key) { - $matches[2] = $value; - $found = true; - } - if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { - if (!is_string_empty($locationArguments)) $locationArguments .= "/"; - $locationArguments .= "$matches[1]:$matches[2]"; - } - } - } - if (!$found && !is_string_empty($key) && !is_string_empty($value)) { - if (!is_string_empty($locationArguments)) $locationArguments .= "/"; - $locationArguments .= "$key:$value"; - } - if (!is_string_empty($locationArguments)) { - $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false, false); - if (!$this->isLocationArgumentsPagination($locationArguments)) $locationArguments .= "/"; - } - return $locationArguments; - } - - // Return location arguments from current HTTP request, convert form parameters - public function getLocationArgumentsCleanUrl() { - $locationArguments = ""; - foreach (array_merge($_GET, $_POST) as $key=>$value) { - if (!is_string_empty($key) && !is_string_empty($value)) { - if (!is_string_empty($locationArguments)) $locationArguments .= "/"; - $key = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $key); - $value = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $value); - $locationArguments .= "$key:$value"; - } - } - if (!is_string_empty($locationArguments)) { - $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false, false); - if (!$this->isLocationArgumentsPagination($locationArguments)) $locationArguments .= "/"; - } - return $locationArguments; - } - - // Return location arguments separator - public function getLocationArgumentsSeparator() { - return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ":" : "="; - } - - // Return 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 420: $text = "Not public"; break; - case 430: $text = "Login failed"; break; - case 434: $text = "Can create"; break; - case 435: $text = "Can restore"; break; - case 450: $text = "Update error"; break; - case 500: $text = "Server error"; break; - case 503: $text = "Service unavailable"; break; - default: $text = "Error $statusCode"; - } - $serverProtocol = $this->getServer("SERVER_PROTOCOL"); - if (!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1"; - return $shortFormat ? $text : "$serverProtocol $statusCode $text"; - } - - // Return MIME content type - public function getMimeContentType($fileName) { - $contentType = ""; - $contentTypes = array( - "css" => "text/css", - "gif" => "image/gif", - "html" => "text/html; charset=utf-8", - "ico" => "image/x-icon", - "js" => "application/javascript", - "json" => "application/json", - "jpg" => "image/jpeg", - "md" => "text/markdown", - "png" => "image/png", - "scss" => "text/x-scss", - "svg" => "image/svg+xml", - "txt" => "text/plain", - "woff" => "application/font-woff", - "woff2" => "application/font-woff2", - "xml" => "text/xml; charset=utf-8"); - $fileType = $this->getFileType($fileName); - if (is_string_empty($fileType)) { - $contentType = $contentTypes["html"]; - } elseif (array_key_exists($fileType, $contentTypes)) { - $contentType = $contentTypes[$fileType]; - } - return $contentType; - } - - // Send HTTP header - public function sendHttpHeader($text) { - if (!headers_sent()) header($text); - } - - // Return files and directories - public function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true) { - return $this->getDirectoryEntriesRecursive($path, $regex, $sort, $directories, $includePath, 1); - } - - // Return files and directories recursively - public function getDirectoryEntriesRecursive($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true, $levelMax = 0) { - --$levelMax; - $entries = array(); - $directoryHandle = @opendir($path); - if ($directoryHandle) { - $path = rtrim($path, "/"); - $directoryEntries = array(); - while (($entry = readdir($directoryHandle))!==false) { - if (substru($entry, 0, 1)==".") continue; - $entry = $this->yellow->lookup->normaliseUnicode($entry); - if (preg_match($regex, $entry)) { - if ($directories) { - if (is_dir("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); - } else { - if (is_file("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); - } - } - if (is_dir("$path/$entry") && $levelMax!=0) array_push($directoryEntries, "$path/$entry"); - } - if ($sort) { - natcasesort($entries); - natcasesort($directoryEntries); - } - closedir($directoryHandle); - foreach ($directoryEntries as $directoryEntry) { - $entries = array_merge($entries, $this->getDirectoryEntriesRecursive($directoryEntry, $regex, $sort, $directories, $includePath, $levelMax)); - } - } - return $entries; - } - - // Return directory information, modification date and file count - public function getDirectoryInformation($path) { - return $this->getDirectoryInformationRecursive($path, 1); - } - - // Return directory information recursively, modification date and file count - public function getDirectoryInformationRecursive($path, $levelMax = 0) { - --$levelMax; - $modified = $fileCount = 0; - $directoryHandle = @opendir($path); - if ($directoryHandle) { - $path = rtrim($path, "/"); - $directoryEntries = array(); - while (($entry = readdir($directoryHandle))!==false) { - if (substru($entry, 0, 1)==".") continue; - $modified = max($modified, $this->getFileModified("$path/$entry")); - if (is_file("$path/$entry")) ++$fileCount; - if (is_dir("$path/$entry") && $levelMax!=0) array_push($directoryEntries, "$path/$entry"); - } - closedir($directoryHandle); - foreach ($directoryEntries as $directoryEntry) { - list($modifiedBelow, $fileCountBelow) = $this->getDirectoryInformationRecursive($directoryEntry, $levelMax); - $modified = max($modified, $modifiedBelow); - $fileCount += $fileCountBelow; - } - } - return array($modified, $fileCount); - } - - // Read file, empty string if not found - public function readFile($fileName, $sizeMax = 0) { - $fileData = ""; - $fileHandle = @fopen($fileName, "rb"); - if ($fileHandle) { - clearstatcache(true, $fileName); - if (flock($fileHandle, LOCK_SH)) { - $fileSize = $sizeMax ? $sizeMax : filesize($fileName); - if ($fileSize) $fileData = fread($fileHandle, $fileSize); - flock($fileHandle, LOCK_UN); - } - fclose($fileHandle); - } - return $fileData; - } - - // Create file - public function createFile($fileName, $fileData, $mkdir = false) { - $ok = false; - if ($mkdir) { - $path = dirname($fileName); - if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); - } - $fileHandle = @fopen($fileName, "cb"); - if ($fileHandle) { - clearstatcache(true, $fileName); - if (flock($fileHandle, LOCK_EX)) { - ftruncate($fileHandle, 0); - fwrite($fileHandle, $fileData); - flock($fileHandle, LOCK_UN); - } - fclose($fileHandle); - $ok = true; - } - return $ok; - } - - // Append file - public function appendFile($fileName, $fileData, $mkdir = false) { - $ok = false; - if ($mkdir) { - $path = dirname($fileName); - if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); - } - $fileHandle = @fopen($fileName, "ab"); - if ($fileHandle) { - clearstatcache(true, $fileName); - if (flock($fileHandle, LOCK_EX)) { - fwrite($fileHandle, $fileData); - flock($fileHandle, LOCK_UN); - } - fclose($fileHandle); - $ok = true; - } - return $ok; - } - - // Copy file - public function copyFile($fileNameSource, $fileNameDestination, $mkdir = false) { - clearstatcache(); - if ($mkdir) { - $path = dirname($fileNameDestination); - if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); - } - return @copy($fileNameSource, $fileNameDestination); - } - - // Rename file - public function renameFile($fileNameSource, $fileNameDestination, $mkdir = false) { - clearstatcache(); - if ($mkdir) { - $path = dirname($fileNameDestination); - if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); - } - return @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 (is_string_empty($pathTrash)) { - $ok = @unlink($fileName); - } else { - if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); - $fileNameDestination = $pathTrash; - $fileNameDestination .= pathinfo($fileName, PATHINFO_FILENAME); - $fileNameDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s")); - $fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION); - $ok = @rename($fileName, $fileNameDestination); - } - return $ok; - } - - // Delete directory - public function deleteDirectory($path, $pathTrash = "") { - clearstatcache(); - if (is_string_empty($pathTrash)) { - $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); - $files = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST); - foreach ($files as $file) { - if ($file->getType()=="dir") { - @rmdir($file->getPathname()); - } else { - @unlink($file->getPathname()); - } - } - $ok = @rmdir($path); - } else { - if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); - $pathDestination = $pathTrash; - $pathDestination .= basename($path); - $pathDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s")); - $ok = @rename($path, $pathDestination); - } - return $ok; - } - - // Set file/directory modification date, Unix time - public function modifyFile($fileName, $modified) { - clearstatcache(true, $fileName); - return @touch($fileName, $modified); - } - - // Return file/directory modification date, Unix time - public function getFileModified($fileName) { - return (is_file($fileName) || is_dir($fileName)) ? filemtime($fileName) : 0; - } - - // Return file/directory deletion date, Unix time - public function getFileDeleted($fileName) { - $deleted = 0; - $text = basename($fileName); - $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; - if (preg_match("#^(.+)-(\d\d\d\d-\d\d-\d\d)-(\d\d)-(\d\d)-(\d\d)$#", $text, $matches)) { - $deleted = strtotime("$matches[2] $matches[3]:$matches[4]:$matches[5]"); - } - return $deleted; - } - - // Return file type - public function getFileType($fileName) { - return strtoloweru(($pos = strrposu($fileName, ".")) ? substru($fileName, $pos+1) : ""); - } - - // Return file group - public function getFileGroup($fileName, $path) { - $group = "none"; - if (preg_match("#^$path(.+?)\/#", $fileName, $matches)) $group = strtoloweru($matches[1]); - return $group; - } - - // Return number of bytes - public function getNumberBytes($text) { - $bytes = intval($text); - switch (strtoupperu(substru($text, -1))) { - case "G": $bytes *= 1024*1024*1024; break; - case "M": $bytes *= 1024*1024; break; - case "K": $bytes *= 1024; break; - } - return $bytes; - } - - // Return lines from text, including newline - public function getTextLines($text) { - $lines = preg_split("/\n/", $text); - foreach ($lines as &$line) { - $line = $line."\n"; - } - if (is_string_empty($text) || substru($text, -1, 1)=="\n") array_pop($lines); - return $lines; - } - - // Return settings from text - function getTextSettings($text, $blockStart) { - $settings = new YellowArray(); - if (is_string_empty($blockStart)) { - foreach ($this->getTextLines($text) as $line) { - if (preg_match("/^\#/", $line)) continue; - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { - $settings[$matches[1]] = $matches[2]; - } - } - } - } else { - $blockKey = ""; - foreach ($this->getTextLines($text) as $line) { - if (preg_match("/^\#/", $line)) continue; - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { - $blockKey = $matches[2]; - $settings[$blockKey] = new YellowArray(); - } - if (!is_string_empty($blockKey) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) { - $settings[$blockKey][$matches[1]] = $matches[2]; - } - } - } - } - return $settings; - } - - // Set settings in text - function setTextSettings($text, $blockStart, $blockKey, $settings) { - $textNew = ""; - if (is_string_empty($blockStart)) { - foreach ($this->getTextLines($text) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!is_string_empty($matches[1]) && isset($settings[$matches[1]])) { - $textNew .= "$matches[1]: ".$settings[$matches[1]]."\n"; - unset($settings[$matches[1]]); - continue; - } - } - $textNew .= $line; - } - foreach ($settings as $key=>$value) { - $textNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; - } - } else { - $scan = false; - $textStart = $textMiddle = $textEnd = ""; - foreach ($this->getTextLines($text) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { - $scan = lcfirst($matches[2])==lcfirst($blockKey); - } - } - if (!$scan && is_string_empty($textMiddle)) { - $textStart .= $line; - } elseif ($scan) { - $textMiddle .= $line; - } else { - $textEnd .= $line; - } - } - $textSettings = ""; - foreach ($this->getTextLines($textMiddle) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!is_string_empty($matches[1]) && isset($settings[$matches[1]])) { - $textSettings .= "$matches[1]: ".$settings[$matches[1]]."\n"; - unset($settings[$matches[1]]); - continue; - } - $textSettings .= $line; - } - } - foreach ($settings as $key=>$value) { - $textSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; - } - if (!is_string_empty($textMiddle)) { - $textMiddle = $textSettings; - if (!is_string_empty($textEnd)) $textMiddle .= "\n"; - } else { - if (!is_string_empty($textStart)) $textEnd .= "\n"; - $textEnd .= $textSettings; - } - $textNew = $textStart.$textMiddle.$textEnd; - } - return $textNew; - } - - // Remove settings from text - function unsetTextSettings($text, $blockStart, $blockKey) { - $textNew = ""; - if (!is_string_empty($blockStart)) { - $scan = false; - $textStart = $textMiddle = $textEnd = ""; - foreach ($this->getTextLines($text) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { - $scan = lcfirst($matches[2])==lcfirst($blockKey); - } - } - if (!$scan && is_string_empty($textMiddle)) { - $textStart .= $line; - } elseif ($scan) { - $textMiddle .= $line; - } else { - $textEnd .= $line; - } - } - $textNew = rtrim($textStart.$textEnd)."\n"; - } - return $textNew; - } - - // Return array of specific size from text - public function getTextList($text, $separator, $size) { - $tokens = explode($separator, $text, $size); - return array_pad($tokens, $size, ""); - } - - // Return array of variable size from text, space separated - public function getTextArguments($text, $optional = "-", $sizeMin = 9) { - $text = preg_replace("/\s+/s", " ", trim($text)); - $tokens = str_getcsv($text, " ", "\""); - foreach ($tokens as $key=>$value) { - if (is_null($value) || $value==$optional) $tokens[$key] = ""; - } - return array_pad($tokens, $sizeMin, ""); - } - - // Return text from array, space separated - public function getTextString($tokens, $optional = "-") { - $text = ""; - foreach ($tokens as $token) { - if (preg_match("/\s/", $token)) $token = "\"$token\""; - if (is_string_empty($token)) $token = $optional; - if (!is_string_empty($text)) $text .= " "; - $text .= $token; - } - return $text; - } - - // Return number of words in text - public function getTextWords($text) { - $text = preg_replace("/([\p{Han}\p{Hiragana}\p{Katakana}]{3})/u", "$1 ", $text); - $text = preg_replace("/(\pL|\p{N})/u", "x", $text); - return str_word_count($text); - } - - // Return text truncated at word boundary - public function getTextTruncated($text, $lengthMax) { - if (strlenu($text)>$lengthMax-1) { - $text = substru($text, 0, $lengthMax); - $pos = strrposu($text, " "); - $text = substru($text, 0, $pos ? $pos : $lengthMax-1)."…"; - } - return $text; - } - - // Create text description, with or without HTML - public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") { - $output = ""; - $elementsBlock = array("blockquote", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "li", "ol", "p", "pre", "ul"); - $elementsVoid = array("area", "br", "col", "embed", "hr", "img", "input", "param", "source", "wbr"); - if ($lengthMax==0) $lengthMax = strlenu($text); - if ($removeHtml) { - $hiddenLevel = 0; - $offsetBytes = 0; - while (true) { - $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); - $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); - $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; - $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; - $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; - $elementAttributes = isset($matches[3][0]) ? $matches[3][0] : ""; - $elementEnd = isset($matches[4][0]) ? $matches[4][0] : ""; - if (!is_string_empty($elementBefore) && !$hiddenLevel) { - $rawText = preg_replace("/\s+/s", " ", html_entity_decode($elementBefore, ENT_QUOTES, "UTF-8")); - if (is_string_empty($elementStart) && in_array(strtolower($elementName), $elementsBlock)) $rawText = rtrim($rawText)." "; - if (substru($rawText, 0, 1)==" " && (is_string_empty($output) || substru($output, -1)==" ")) $rawText = ltrim($rawText); - $output .= $this->getTextTruncated($rawText, $lengthMax); - $lengthMax -= strlenu($rawText); - } - if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) { - $output .= $endMarkerText; - $lengthMax = 0; - } - if ($lengthMax<=0 || !$elementFound) break; - if ($hiddenLevel>0 || - preg_match("/aria-hidden=\"true\"/i", $elementAttributes) || - preg_match("/role=\"doc-noteref\"/i", $elementAttributes)) { - if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { - if (is_string_empty($elementStart)) { - ++$hiddenLevel; - } else { - --$hiddenLevel; - } - } - } - $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); - } - $output = preg_replace("/\s+\…$/s", "…", $output); - } else { - $elementsOpen = array(); - $offsetBytes = 0; - while (true) { - $elementFound = preg_match("/&.*?\;|<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); - $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); - $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; - $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; - $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; - $elementEnd = isset($matches[4][0]) ? $matches[4][0] : ""; - if (!is_string_empty($elementBefore)) { - $output .= $this->getTextTruncated($elementBefore, $lengthMax); - $lengthMax -= strlenu($elementBefore); - } - if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) { - $output .= $endMarkerText; - $lengthMax = 0; - } - if ($lengthMax<=0 || !$elementFound) break; - if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { - if (is_string_empty($elementStart)) { - array_push($elementsOpen, $elementName); - } else { - array_pop($elementsOpen); - } - } - $output .= $elementRawData; - if ($elementRawData[0]=="&") --$lengthMax; - $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); - } - $output = preg_replace("/\s+\…$/s", "…", $output); - for ($i=count($elementsOpen)-1; $i>=0; --$i) { - $output .= "</".$elementsOpen[$i].">"; - } - } - return trim($output); - } - - // Create title from text - public function createTextTitle($text) { - if (preg_match("/^.*\/([\pL\d\-\_]+)/u", $text, $matches)) $text = str_replace("-", " ", 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 (is_string_empty($dataBuffer) && function_exists("random_bytes")) { - $dataBuffer = @random_bytes($dataBufferSize); - } - if (is_string_empty($dataBuffer) && function_exists("openssl_random_pseudo_bytes")) { - $dataBuffer = @openssl_random_pseudo_bytes($dataBufferSize); - } - if (strlenb($dataBuffer)==$dataBufferSize) { - if ($bcryptFormat) { - $salt = substrb(base64_encode($dataBuffer), 0, $length); - $base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - $bcrypt64Chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - $salt = strtr($salt, $base64Chars, $bcrypt64Chars); - } else { - $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 (is_string_empty($salt) || strlenb($hash)!=60) $hash = ""; - break; - case "sha256": $prefix = "$5y$"; - $salt = $this->createSalt(32); - $hash = "$prefix$salt".hash("sha256", $salt.$text); - if (is_string_empty($salt) || strlenb($hash)!=100) $hash = ""; - break; - } - return $hash; - } - - // 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 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) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])==$key && !is_string_empty($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)) { - $found = false; - $key = lcfirst($key); - $rawDataMiddle = ""; - foreach ($this->getTextLines($parts[2]) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])==$key) { - $rawDataMiddle .= "$matches[1]: $value\n"; - $found = true; - continue; - } - } - $rawDataMiddle .= $line; - } - if (!$found) $rawDataMiddle .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; - $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3]; - } else { - $rawDataNew = $rawData; - } - return $rawDataNew; - } - - // Remove meta data in raw data - public function unsetMetaData($rawData, $key) { - if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { - $key = lcfirst($key); - $rawDataMiddle = ""; - foreach ($this->getTextLines($parts[2]) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])==$key) continue; - } - $rawDataMiddle .= $line; - } - $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3]; - } else { - $rawDataNew = $rawData; - } - return $rawDataNew; - } - - // Return troubleshooting URL - public function getTroubleshootingUrl() { - return "https://datenstrom.se/yellow/help/troubleshooting"; - } - - // Detect server URL - public function detectServerUrl() { - $scheme = "http"; - if ($this->getServer("REQUEST_SCHEME")=="https" || $this->getServer("HTTPS")=="on") $scheme = "https"; - if ($this->getServer("HTTP_X_FORWARDED_PROTO")=="https") $scheme = "https"; - $address = $this->getServer("SERVER_NAME"); - $port = $this->getServer("SERVER_PORT"); - if ($port!=80 && $port!=443) $address .= ":$port"; - $base = ""; - if (preg_match("/^(.*)\/.*\.php$/", $this->getServer("SCRIPT_NAME"), $matches)) $base = $matches[1]; - return "$scheme://$address$base/"; - } - - // Detect server location - public function detectServerLocation() { - if (isset($_SERVER["REQUEST_URI"])) { - $location = $_SERVER["REQUEST_URI"]; - $location = rawurldecode(($pos = strposu($location, "?")) ? substru($location, 0, $pos) : $location); - $location = $this->yellow->lookup->normalisePath($location); - if (substru($location, 0, 1)!="/") $location = "/".$location; - $separator = $this->getLocationArgumentsSeparator(); - if (preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches)) { - $_SERVER["LOCATION"] = $location = $matches[1]; - $_SERVER["LOCATION_ARGUMENTS"] = $matches[2]; - foreach (explode("/", $matches[2]) as $token) { - if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { - if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { - $matches[1] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[1]); - $matches[2] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[2]); - $_REQUEST[$matches[1]] = $matches[2]; - } - } - } - } else { - $_SERVER["LOCATION"] = $location; - $_SERVER["LOCATION_ARGUMENTS"] = ""; - } - } - return $this->getServer("LOCATION"); - } - - // Detect server sitename - public function detectServerSitename() { - $sitename = "Localhost"; - if (preg_match("#^(www\.)?([\w\-]+)#", $this->getServer("SERVER_NAME"), $matches)) { - $sitename = ucfirst($matches[2]); - } - return $sitename; - } - - // Detect server timezone - public function detectServerTimezone() { - $timezone = ini_get("date.timezone"); - if (is_string_empty($timezone)) { - if (PHP_OS=="Darwin") { - if (preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1]; - } else { - if (preg_match("/^(\S+)\/(\S+)/", $this->readFile("/etc/timezone"), $matches)) $timezone = $matches[1]; - } - } - if (!in_array($timezone, timezone_identifiers_list())) $timezone = "UTC"; - return $timezone; - } - - // Detect server name, version and operating system - public function detectServerInformation() { - $name = "Unknown"; - $version = "x.x.x"; - $os = PHP_OS; - if (preg_match("/^(\S+)\/(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) { - $name = $matches[1]; - $version = $matches[2]; - } elseif (preg_match("/^(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) { - $name = $matches[1]; - } - if (PHP_SAPI=="cli" || PHP_SAPI=="cli-server") { - $name = "Built-in"; - $version = PHP_VERSION; - } - if (PHP_OS=="Darwin") { - $os = "Mac"; - } elseif (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") { - $os = "Windows"; - } - return array($name, $version, $os); - } - - // Detect browser language - public function detectBrowserLanguage($languages, $languageDefault) { - $languageFound = $languageDefault; - foreach (preg_split("/\s*,\s*/", $this->getServer("HTTP_ACCEPT_LANGUAGE")) as $text) { - list($language, $dummy) = $this->getTextList($text, ";", 2); - if (!is_string_empty($language) && in_array($language, $languages)) { - $languageFound = $language; - break; - } - } - return $languageFound; - } - - // Detect terminal width and height - public function detectTerminalInformation() { - $width = $height = 0; - if (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") { - exec("powershell \$Host.UI.RawUI.WindowSize.Width", $outputLines, $returnStatus); - if ($returnStatus==0 && !is_array_empty($outputLines)) { - $width = intval(end($outputLines)); - } - exec("powershell \$Host.UI.RawUI.WindowSize.Height", $outputLines, $returnStatus); - if ($returnStatus==0 && !is_array_empty($outputLines)) { - $height = intval(end($outputLines)); - } - } else { - exec("stty size", $outputLines, $returnStatus); - if ($returnStatus==0 && preg_match("/^(\d+)\s+(\d+)/", implode("\n", $outputLines), $matches)) { - $width = intval($matches[2]); - $height = intval($matches[1]); - } - } - return array($width, $height); - } - - // Detect image width, height, orientation and type for GIF/JPG/PNG/SVG - public function detectImageInformation($fileName, $fileType = "") { - $width = $height = $orientation = 0; - $type = ""; - $fileHandle = @fopen($fileName, "rb"); - if ($fileHandle) { - if (is_string_empty($fileType)) $fileType = $this->getFileType($fileName); - if ($fileType=="gif") { - $dataSignature = fread($fileHandle, 6); - $dataHeader = fread($fileHandle, 7); - if (!feof($fileHandle) && ($dataSignature=="GIF87a" || $dataSignature=="GIF89a")) { - $width = (ord($dataHeader[1])<<8) + ord($dataHeader[0]); - $height = (ord($dataHeader[3])<<8) + ord($dataHeader[2]); - $type = $fileType; - } - } elseif ($fileType=="jpg") { - $dataBufferSizeMax = filesize($fileName); - $dataBufferSize = min($dataBufferSizeMax, 4096); - if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); - $dataSignature = substrb($dataBuffer, 0, 2); - if (!feof($fileHandle) && $dataSignature=="\xff\xd8") { - for ($pos=2; $pos+8<$dataBufferSize; $pos+=$length) { - if ($dataBuffer[$pos]!="\xff") break; - $dataMarker = $dataBuffer[$pos+1]; - if ($dataMarker=="\xe1") { - $orientation = $this->getImageOrientationFromBuffer($dataBuffer, $pos+4, $dataBufferSize); - } - if (($dataMarker>="\xc0" && $dataMarker<="\xc3") || - ($dataMarker>="\xc5" && $dataMarker<="\xc7") || - ($dataMarker>="\xc9" && $dataMarker<="\xcb") || - ($dataMarker>="\xcd" && $dataMarker<="\xcf")) { - $width = (ord($dataBuffer[$pos+7])<<8) + ord($dataBuffer[$pos+8]); - $height = (ord($dataBuffer[$pos+5])<<8) + ord($dataBuffer[$pos+6]); - $type = $fileType; - break; - } - $length = (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]) + 2; - while ($pos+$length+8>=$dataBufferSize) { - if ($dataBufferSize==$dataBufferSizeMax) break; - $dataBufferDiff = min($dataBufferSizeMax, $dataBufferSize*2) - $dataBufferSize; - $dataBufferSize += $dataBufferDiff; - $dataBufferChunk = fread($fileHandle, $dataBufferDiff); - if (feof($fileHandle) || $dataBufferChunk===false) { - $dataBufferSize = 0; - break; - } - $dataBuffer .= $dataBufferChunk; - } - } - } - } elseif ($fileType=="png") { - $dataSignature = fread($fileHandle, 8); - $dataHeader = fread($fileHandle, 16); - if (!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n") { - $width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]); - $height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]); - $type = $fileType; - } - } elseif ($fileType=="svg") { - $dataBufferSizeMax = filesize($fileName); - $dataBufferSize = min($dataBufferSizeMax, 4096); - if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); - if (!feof($fileHandle) && preg_match("/<svg(\s.*?)>/s", $dataBuffer, $matches)) { - if (preg_match("/\swidth=\"(\d+)\"/s", $matches[1], $tokens)) $width = $tokens[1]; - if (preg_match("/\sheight=\"(\d+)\"/s", $matches[1], $tokens)) $height = $tokens[1]; - $type = $fileType; - } - } - fclose($fileHandle); - } - return array($width, $height, $orientation, $type); - } - - // Return image orientation from Exif - public function getImageOrientationFromBuffer($dataBuffer, $pos, $size) { - $orientation = 0; - $dataSignature = substrb($dataBuffer, $pos, 6); - if ($dataSignature=="\x45\x78\x69\x66\x00\x00" && $pos+14<=$size) { - $startPos = $pos+6; - $bigEndian = $dataBuffer[$startPos]=="M"; - $ifdOffset = $this->getLongFromBuffer($dataBuffer, $startPos+4, $bigEndian); - $ifdStartPos = $startPos+$ifdOffset; - $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0; - $pos = $ifdStartPos+2; - while ($ifdCount && $pos+12<=$size) { - $ifdTag = $this->getShortFromBuffer($dataBuffer, $pos, $bigEndian); - $ifdFormat = $this->getShortFromBuffer($dataBuffer, $pos+2, $bigEndian); - if ($ifdTag==0x8769 && $ifdFormat==4) { - $ifdOffset = $this->getLongFromBuffer($dataBuffer, $pos+8, $bigEndian); - $ifdStartPos = $startPos+$ifdOffset; - $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0; - $pos = $ifdStartPos+2; - continue; - } - if ($ifdTag==0x0112 && $ifdFormat==3) { - $orientation = $this->getShortFromBuffer($dataBuffer, $pos+8, $bigEndian); - break; - } - --$ifdCount; - $pos += 12; - } - } - return $orientation; - } - - // Return unsigned short value from buffer - public function getShortFromBuffer($dataBuffer, $pos, $bigEndian) { - if ($bigEndian) { - $value = (ord($dataBuffer[$pos])<<8) + ord($dataBuffer[$pos+1]); - } else { - $value = (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]); - } - return $value; - } - - // Return unsigned long value from buffer - public function getLongFromBuffer($dataBuffer, $pos, $bigEndian) { - if ($bigEndian) { - $value = (ord($dataBuffer[$pos])<<24) + (ord($dataBuffer[$pos+1])<<16) + - (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]); - } else { - $value = (ord($dataBuffer[$pos+3])<<24) + (ord($dataBuffer[$pos+2])<<16) + - (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]); - } - return $value; - } - - // Send email message - public function mail($action, $headers, $message) { - $statusCode = 0; - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onMail")) { - $statusCode = $value["object"]->onMail($action, $headers, $message); - if ($statusCode!=0) break; - } - } - if ($statusCode==0) { - $text = $this->yellow->lookup->normaliseHeaders($headers, "mime"); - $to = $subject = $remaining = $key = ""; - foreach (preg_split("/\r\n/", $text) as $line) { - if (preg_match("/^(.*?):\s*(.*?)$/", $line, $matches) && !is_string_empty($matches[1])) { - $key = $matches[1]; - $fragment = $matches[2]; - } else { - $fragment = $line; - } - if ($key=="To") { $to .= $fragment; continue; } - if ($key=="Subject") { $subject .= $fragment; continue; } - $remaining .= $line."\r\n"; - } - $statusCode = mail($to, $subject, $message, $remaining) ? 200 : 500; - } - return $statusCode==200; - } - - // Write information to log file - public function log($action, $message) { - $statusCode = 0; - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onLog")) { - $statusCode = $value["object"]->onLog($action, $message); - if ($statusCode!=0) break; - } - } - if ($statusCode==0) { - $line = date("Y-m-d H:i:s")." ".trim($action)." ".trim($message)."\n"; - $this->appendFile($this->yellow->system->get("coreServerInstallDirectory"). - $this->yellow->system->get("coreExtensionDirectory"). - $this->yellow->system->get("coreWebsiteFile"), $line); - } - } - - // Start timer - public function timerStart(&$time) { - $time = microtime(true); - } - - // Stop timer and calculate elapsed time in milliseconds - public function timerStop(&$time) { - $time = intval((microtime(true)-$time) * 1000); - } - - // Check if there are location arguments in current HTTP request - public function isLocationArguments($location = "") { - if (is_string_empty($location)) $location = $this->getServer("LOCATION").$this->getServer("LOCATION_ARGUMENTS"); - $separator = $this->getLocationArgumentsSeparator(); - return preg_match("/[^\/]+$separator.*$/", $location); - } - - // Check if there are pagination arguments in current HTTP request - public function isLocationArgumentsPagination($location) { - $separator = $this->getLocationArgumentsSeparator(); - return preg_match("/^(.*\/)?page$separator.*$/", $location); - } - - // Check if unmodified since last HTTP request - public function isNotModified($lastModifiedFormatted) { - return $this->getServer("HTTP_IF_MODIFIED_SINCE")==$lastModifiedFormatted; - } - - // TODO: remove later, for backwards compatibility - public function normaliseArguments($text, $appendSlash = true, $filterStrict = true) { return $this->yellow->lookup->normaliseArguments($text, $appendSlash, $filterStrict); } - public function normalisePath($text) { return $this->yellow->lookup->normalisePath($text); } -} - -class YellowPage { - public $yellow; // access to API - public $scheme; // server scheme - public $address; // server address - public $base; // base location - public $location; // page location - public $fileName; // content file name - public $rawData; // raw data of page - public $metaDataOffsetBytes; // meta data offset - public $metaData; // meta data - public $pageCollections; // additional pages - public $sharedPages; // shared pages - public $headerData; // response header - public $outputData; // response output - public $parser; // content parser - public $parserData; // content data of page - public $statusCode; // status code - public $errorMessage; // error message - public $lastModified; // last modification date - public $available; // page is available? (boolean) - public $visible; // page is visible location? (boolean) - public $active; // page is active location? (boolean) - public $cacheable; // page is cacheable? (boolean) - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->scheme = ""; - $this->address = ""; - $this->base = ""; - $this->location = ""; - $this->fileName = ""; - $this->metaData = new YellowArray(); - $this->pageCollections = array(); - $this->sharedPages = array(); - $this->headerData = array(); - } - - // Set request information - public function setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable) { - $this->scheme = $scheme; - $this->address = $address; - $this->base = $base; - $this->location = $location; - $this->fileName = $fileName; - $this->cacheable = $cacheable; - } - - // Parse page meta - public function parseMeta($rawData, $statusCode = 0, $errorMessage = "") { - $this->rawData = $rawData; - $this->parser = null; - $this->parserData = ""; - $this->statusCode = $statusCode; - $this->errorMessage = $errorMessage; - $this->lastModified = 0; - $this->available = true; - $this->visible = true; - $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location); - $this->parseMetaData(); - } - - // Parse page meta update - public function parseMetaUpdate() { - if ($this->statusCode==0) { - $this->rawData = $this->yellow->toolbox->readFile($this->fileName); - $this->statusCode = 200; - $this->parseMetaData(); - } - } - - // Parse page meta data - public function parseMetaData() { - $this->metaData = new YellowArray(); - $this->metaDataOffsetBytes = 0; - if (!is_null($this->rawData)) { - $this->set("title", $this->yellow->toolbox->createTextTitle($this->location)); - $this->set("language", $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language"))); - $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); - $this->parseMetaDataRaw(array("sitename", "author", "layout", "theme", "parser", "status")); - $this->parseMetaDataShared(); - $titleHeader = ($this->location==$this->yellow->content->getHomeLocation($this->location)) ? - $this->get("sitename") : $this->get("title")." - ".$this->get("sitename"); - if (!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title")); - if (!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title")); - if (!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader); - if ($this->yellow->lookup->isRootLocation($this->location) || !is_readable($this->fileName)) $this->available = false; - if ($this->get("status")=="shared") $this->available = false; - if ($this->get("status")=="unlisted") $this->visible = false; - } else { - $this->set("size", filesize($this->fileName)); - $this->set("type", $this->yellow->toolbox->getFileType($this->fileName)); - $this->set("group", $this->yellow->toolbox->getFileGroup($this->fileName, $this->yellow->system->get("coreMediaDirectory"))); - $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); - if (!$this->yellow->lookup->isFileLocation($this->location)) $this->available = false; - } - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onParseMetaData")) $value["object"]->onParseMetaData($this); - } - } - - // Parse page meta data from raw data - public function parseMetaDataRaw($defaultKeys) { - foreach ($defaultKeys as $key) { - $value = $this->yellow->system->get($key); - if (!is_string_empty($key) && !is_string_empty($value)) $this->set($key, $value); - } - if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+/s", $this->rawData, $parts)) { - $this->metaDataOffsetBytes = strlenb($parts[0]); - foreach (preg_split("/[\r\n]+/", $parts[2]) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) $this->set($matches[1], $matches[2]); - } - } - } elseif (preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parts)) { - $this->metaDataOffsetBytes = strlenb($parts[0]); - $this->set("title", $parts[2]); - } - } - - // Parse page meta data for shared pages - public function parseMetaDataShared() { - $this->sharedPages["main"] = $this; - if (!$this->yellow->lookup->isSharedLocation($this->location) && $this->statusCode!=0) { - foreach ($this->yellow->content->getShared($this->location) as $page) { - $this->sharedPages[basename($page->location)] = $page; - $page->sharedPages["main"] = $this; - } - } - if ($this->yellow->lookup->isSharedLocation($this->location)) { - $this->set("status", "shared"); - } - } - - // Parse page content on demand - public function parseContent() { - if (!is_null($this->rawData) && !is_object($this->parser)) { - if ($this->yellow->extension->isExisting($this->get("parser"))) { - $value = $this->yellow->extension->data[$this->get("parser")]; - if (method_exists($value["object"], "onParseContentRaw")) { - $this->parser = $value["object"]; - $this->parserData = $this->getContentRaw(); - $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData); - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onParseContentHtml")) { - $output = $value["object"]->onParseContentHtml($this, $this->parserData); - if (!is_null($output)) $this->parserData = $output; - } - } - } - } else { - $this->parserData = $this->getContentRaw(); - $this->parserData = preg_replace("/\[yellow error\]/i", $this->errorMessage, $this->parserData); - } - if (!$this->isExisting("description")) { - $description = $this->yellow->toolbox->createTextDescription($this->parserData, 150); - $this->set("description", !is_string_empty($description) ? $description : $this->get("title")); - } - if ($this->yellow->system->get("coreDebugMode")>=3) { - echo "YellowPage::parseContent location:".$this->location."<br/>\n"; - } - } - } - - // Parse page content element, experimental - public function parseContentElement($name, $text, $attrributes, $type) { - $output = null; - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onParseContentElement")) { - $output = $value["object"]->onParseContentElement($this, $name, $text, $attrributes, $type); - if (!is_null($output)) break; - } - if (method_exists($value["object"], "onParseContentShortcut")) { //TODO: remove later, for backwards compatibility - $output = $value["object"]->onParseContentShortcut($this, $name, $text, $type); - if (!is_null($output)) break; - } - } - if (is_null($output)) { - if ($name=="yellow" && $type=="inline" && $text=="error") { - $output = $this->errorMessage; - } - } - if ($this->yellow->system->get("coreDebugMode")>=3 && !is_string_empty($name)) { - echo "YellowPage::parseContentElement name:$name type:$type<br/>\n"; - } - return $output; - } - - // TODO: remove later, for backwards compatibility - public function parseContentShortcut($name, $text, $type) { return $this->parseContentElement($name, $text, "", $type); } - - // Parse page - public function parsePage() { - $this->parsePageLayout($this->get("layout")); - if (!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, no-store"); - if (!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8"); - if (!$this->isHeader("Content-Modified")) $this->setHeader("Content-Modified", $this->getModified(true)); - if (!$this->isHeader("Last-Modified")) $this->setHeader("Last-Modified", $this->getLastModified(true)); - $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; - if (!is_file($fileNameTheme)) { - $this->error(500, "Theme '".$this->get("theme")."' does not exist!"); - } - if (!$this->yellow->language->isExisting($this->get("language"))) { - $this->error(500, "Language '".$this->get("language")."' 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->lookup->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200) { - $location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->location); - $location = $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, "", $location); - $this->status(301, $location); - } - if ($this->yellow->lookup->getRequestHandler()=="core" && !$this->isAvailable() && $this->statusCode==200) { - $this->error(404); - } - if ($this->isExisting("pageClean")) $this->outputData = null; - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onParsePageOutput")) { - $output = $value["object"]->onParsePageOutput($this, $this->outputData); - if (!is_null($output)) $this->outputData = $output; - } - } - } - - // Parse page layout - public function parsePageLayout($name) { - $this->outputData = null; - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onParsePageLayout")) { - $value["object"]->onParsePageLayout($this, $name); - } - } - if (is_null($this->outputData)) { - ob_start(); - $this->includeLayout($name); - $this->outputData = ob_get_contents(); - ob_end_clean(); - } - } - - // Include page layout - public function includeLayout($name) { - $fileNameLayoutNormal = $this->yellow->system->get("coreLayoutDirectory").$this->yellow->lookup->normaliseName($name).".html"; - $fileNameLayoutTheme = $this->yellow->system->get("coreLayoutDirectory"). - $this->yellow->lookup->normaliseName($this->get("theme"))."-".$this->yellow->lookup->normaliseName($name).".html"; - if (is_file($fileNameLayoutTheme)) { - if ($this->yellow->system->get("coreDebugMode")>=2) { - echo "YellowPage::includeLayout file:$fileNameLayoutTheme<br/>\n"; - } - $this->setLastModified(filemtime($fileNameLayoutTheme)); - require($fileNameLayoutTheme); - } elseif (is_file($fileNameLayoutNormal)) { - if ($this->yellow->system->get("coreDebugMode")>=2) { - echo "YellowPage::includeLayout file:$fileNameLayoutNormal<br/>\n"; - } - $this->setLastModified(filemtime($fileNameLayoutNormal)); - require($fileNameLayoutNormal); - } else { - $this->error(500, "Layout '$name' does not exist!"); - echo "Layout error<br/>\n"; - } - } - - // Set page setting - public function set($key, $value) { - $this->metaData[$key] = $value; - } - - // Return page setting - public function get($key) { - return $this->isExisting($key) ? $this->metaData[$key] : ""; - } - - // Return page setting, HTML encoded - public function getHtml($key) { - return htmlspecialchars($this->get($key)); - } - - // Return page setting as language specific date - public function getDate($key, $format = "") { - if (!is_string_empty($format)) { - $format = $this->yellow->language->getText($format); - } else { - $format = $this->yellow->language->getText("coreDateFormatMedium"); - } - return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format); - } - - // Return page setting as language specific date, HTML encoded - public function getDateHtml($key, $format = "") { - return htmlspecialchars($this->getDate($key, $format)); - } - - // Return page setting as language specific date, relative to today - public function getDateRelative($key, $format = "", $daysLimit = 30) { - if (!is_string_empty($format)) { - $format = $this->yellow->language->getText($format); - } else { - $format = $this->yellow->language->getText("coreDateFormatMedium"); - } - return $this->yellow->language->getDateRelative(strtotime($this->get($key)), $format, $daysLimit); - } - - // Return page setting as language specific date, relative to today, HTML encoded - public function getDateRelativeHtml($key, $format = "", $daysLimit = 30) { - return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit)); - } - - // Return page setting as date - public function getDateFormatted($key, $format) { - return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format); - } - - // Return page setting as date, HTML encoded - public function getDateFormattedHtml($key, $format) { - return htmlspecialchars($this->getDateFormatted($key, $format)); - } - - // Return page content data, raw format - public function getContentRaw() { - $this->parseMetaUpdate(); - return substrb($this->rawData, $this->metaDataOffsetBytes); - } - - // Return page content data, HTML encoded or raw format - public function getContentHtml() { - $this->parseContent(); - return $this->parserData; - } - - // Return page extra data, HTML encoded - public function getExtraHtml($name) { - $output = ""; - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onParsePageExtra")) { - $outputExtension = $value["object"]->onParsePageExtra($this, $name); - if (!is_null($outputExtension)) $output .= $outputExtension; - } - } - if ($name=="header") { - $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; - if (is_file($fileNameTheme)) { - $locationTheme = $this->yellow->system->get("coreServerBase"). - $this->yellow->system->get("coreThemeLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; - $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"$locationTheme\" />\n"; - } - $fileNameScript = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; - if (is_file($fileNameScript)) { - $locationScript = $this->yellow->system->get("coreServerBase"). - $this->yellow->system->get("coreThemeLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; - $output .= "<script type=\"text/javascript\" src=\"$locationScript\"></script>\n"; - } - $fileNameFavicon = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".png"; - if (is_file($fileNameFavicon)) { - $locationFavicon = $this->yellow->system->get("coreServerBase"). - $this->yellow->system->get("coreThemeLocation").$this->yellow->lookup->normaliseName($this->get("theme")).".png"; - $output .= "<link rel=\"icon\" type=\"image/png\" href=\"$locationFavicon\" />\n"; - } - } - return $output; - } - - // Return parent page, null if none - public function getParent() { - $parentLocation = $this->yellow->content->getParentLocation($this->location); - return $this->yellow->content->find($parentLocation); - } - - // Return top-level parent page, null if none - public function getParentTop($homeFallback = false) { - $parentTopLocation = $this->yellow->content->getParentTopLocation($this->location); - if (!$this->yellow->content->find($parentTopLocation) && $homeFallback) { - $parentTopLocation = $this->yellow->content->getHomeLocation($this->location); - } - return $this->yellow->content->find($parentTopLocation); - } - - // Return page collection with pages on the same level - public function getSiblings($showInvisible = false) { - $parentLocation = $this->yellow->content->getParentLocation($this->location); - return $this->yellow->content->getChildren($parentLocation, $showInvisible); - } - - // Return page collection with child pages - public function getChildren($showInvisible = false) { - return $this->yellow->content->getChildren($this->location, $showInvisible); - } - - // Return page collection with child pages recursively - public function getChildrenRecursive($showInvisible = false, $levelMax = 0) { - return $this->yellow->content->getChildrenRecursive($this->location, $showInvisible, $levelMax); - } - - // Set page collection with additional pages - public function setPages($key, $pages) { - $this->pageCollections[$key] = $pages; - } - - // Return page collection with additional pages - public function getPages($key) { - return isset($this->pageCollections[$key]) ? $this->pageCollections[$key] : new YellowPageCollection($this->yellow); - } - - // Set shared page - public function setPage($key, $page) { - $this->sharedPages[$key] = $page; - } - - // Return shared page - public function getPage($key) { - return isset($this->sharedPages[$key]) ? $this->sharedPages[$key] : new YellowPage($this->yellow); - } - - // Return page URL - public function getUrl() { - return $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, $this->base, $this->location); - } - - // Return page base - public function getBase($multiLanguage = false) { - return $multiLanguage ? rtrim($this->base.$this->yellow->content->getHomeLocation($this->location), "/") : $this->base; - } - - // Return page location - public function getLocation($absoluteLocation = false) { - return $absoluteLocation ? $this->base.$this->location : $this->location; - } - - // Set page request argument - public function setRequest($key, $value) { - $_REQUEST[$key] = $value; - } - - // Return page request argument - public function getRequest($key) { - return isset($_REQUEST[$key]) ? $_REQUEST[$key] : ""; - } - - // Return page request argument, HTML encoded - public function getRequestHtml($key) { - return htmlspecialchars($this->getRequest($key)); - } - - // Set page response header - public function setHeader($key, $value) { - $this->headerData[$key] = $value; - } - - // Return page response header - public function getHeader($key) { - return $this->isHeader($key) ? $this->headerData[$key] : ""; - } - - // Set page response output - public function setOutput($output) { - $this->outputData = $output; - } - - // Return page modification date, Unix time or HTTP format - public function getModified($httpFormat = false) { - $modified = strtotime($this->get("modified")); - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; - } - - // Set last modification date, Unix time - public function setLastModified($modified) { - $this->lastModified = max($this->lastModified, $modified); - } - - // Return last modification date, Unix time or HTTP format - public function getLastModified($httpFormat = false) { - $lastModified = max($this->lastModified, $this->getModified(), $this->yellow->system->getModified(), - $this->yellow->language->getModified(), $this->yellow->extension->getModified()); - foreach ($this->pageCollections as $pages) $lastModified = max($lastModified, $pages->getModified()); - foreach ($this->sharedPages as $page) $lastModified = max($lastModified, $page->getModified()); - return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($lastModified) : $lastModified; - } - - // Return raw data for error page - public function getRawDataError() { - $statusCode = $this->statusCode; - $sharedLocation = $this->yellow->content->getHomeLocation($this->location)."shared/"; - $fileNameError = $this->yellow->lookup->findFileFromContentLocation($sharedLocation, true).$this->yellow->system->get("coreContentErrorFile"); - $fileNameError = str_replace("(.*)", $statusCode, $fileNameError); - $languageError = $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language")); - if (is_file($fileNameError)) { - $rawData = $this->yellow->toolbox->readFile($fileNameError); - } elseif ($this->yellow->language->isText("coreError{$statusCode}Title", $languageError)) { - $rawData = "---\nTitle: ".$this->yellow->language->getText("coreError{$statusCode}Title", $languageError)."\n"; - $rawData .= "Layout: error\n---\n".$this->yellow->language->getText("coreError{$statusCode}Text", $languageError); - } else { - $rawData = "---\nTitle:".$this->yellow->toolbox->getHttpStatusFormatted($statusCode, true)."\n"; - $rawData .= "Layout:error\n---\n".$this->errorMessage; - } - return $rawData; - } - - // Return page status code, number or HTTP format - public function getStatusCode($httpFormat = false) { - $statusCode = $this->statusCode; - if ($httpFormat) { - $statusCode = $this->yellow->toolbox->getHttpStatusFormatted($statusCode); - if (!is_string_empty($this->errorMessage)) $statusCode .= ": ".$this->errorMessage; - } - return $statusCode; - } - - // Respond with status code, no page content - public function status($statusCode, $location = "") { - if ($statusCode>0 && !$this->isExisting("pageClean")) { - $this->statusCode = $statusCode; - $this->lastModified = 0; - $this->headerData = array(); - if (!is_string_empty($location)) { - $this->setHeader("Location", $location); - $this->setHeader("Cache-Control", "no-cache, no-store"); - } - $this->set("pageClean", (string)$statusCode); - } - } - - // Respond with error page - public function error($statusCode, $errorMessage = "") { - if ($statusCode>=400 && is_string_empty($this->errorMessage)) { - $this->statusCode = $statusCode; - $this->errorMessage = is_string_empty($errorMessage) ? "Page error!" : $errorMessage; - } - } - - // Check if page is available - public function isAvailable() { - return $this->available; - } - - // Check if page is visible - public function isVisible() { - return $this->visible; - } - - // Check if page is within current HTTP request - public function isActive() { - return $this->active; - } - - // Check if page is cacheable - public function isCacheable() { - return $this->cacheable; - } - - // Check if page with error - public function isError() { - return $this->statusCode>=400; - } - - // Check if page setting exists - public function isExisting($key) { - return isset($this->metaData[$key]); - } - - // Check if request argument exists - public function isRequest($key) { - return isset($_REQUEST[$key]); - } - - // Check if response header exists - public function isHeader($key) { - return isset($this->headerData[$key]); - } - - // Check if shared page exists - public function isPage($key) { - return isset($this->sharedPages[$key]); - } - - // TODO: remove later, for backwards compatibility - public function getContent($rawFormat = false) { return $rawFormat ? $this->getContentRaw() : $this->getContentHtml(); } - public function getExtra($name) { return $this->getExtraHtml($name); } -} - -class YellowPageCollection extends ArrayObject { - public $yellow; // access to API - public $filterValue; // current page filter value - public $paginationNumber; // current page number in pagination - public $paginationCount; // highest page number in pagination - - public function __construct($yellow) { - parent::__construct(array()); - $this->yellow = $yellow; - } - - // Append page to end of page collection - #[\ReturnTypeWillChange] - public function append($page) { - parent::append($page); - } - - // Prepend page to start of page collection - #[\ReturnTypeWillChange] - public function prepend($page) { - $array = $this->getArrayCopy(); - array_unshift($array, $page); - $this->exchangeArray($array); - } - - // Remove page from page collection - public function remove($page): YellowPageCollection { - $array = array(); - $location = $page->location; - foreach ($this->getArrayCopy() as $page) { - if ($page->location!=$location) array_push($array, $page); - } - $this->exchangeArray($array); - return $this; - } - - // Filter page collection by page setting - public function filter($key, $value, $exactMatch = true): YellowPageCollection { - $array = array(); - $value = str_replace(" ", "-", strtoloweru($value)); - $valueLength = strlenu($value); - $this->filterValue = ""; - foreach ($this->getArrayCopy() as $page) { - if ($page->isExisting($key)) { - foreach (preg_split("/\s*,\s*/", $page->get($key)) as $pageValue) { - $pageValueLength = $exactMatch ? strlenu($pageValue) : $valueLength; - if ($value==substru(str_replace(" ", "-", strtoloweru($pageValue)), 0, $pageValueLength)) { - if (is_string_empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength); - array_push($array, $page); - break; - } - } - } - } - $this->exchangeArray($array); - return $this; - } - - // Filter page collection by location or file - public function match($regex = "/.*/", $filterByLocation = true): YellowPageCollection { - $array = array(); - $this->filterValue = $regex; - foreach ($this->getArrayCopy() as $page) { - if (preg_match($regex, $filterByLocation ? $page->location : $page->fileName)) array_push($array, $page); - } - $this->exchangeArray($array); - return $this; - } - - // Sort page collection by settings similarity - public function similar($page): YellowPageCollection { - $location = $page->location; - $keywords = strtoloweru($page->get("title").",".$page->get("tag").",".$page->get("author")); - $tokens = array_unique(array_filter(preg_split("/[,\s\(\)\+\-]/", $keywords), "strlen")); - if (!is_array_empty($tokens)) { - $array = array(); - foreach ($this->getArrayCopy() as $page) { - $sortScore = 0; - foreach ($tokens as $token) { - if (stristr($page->get("title"), $token)) $sortScore += 50; - if (stristr($page->get("tag"), $token)) $sortScore += 5; - if (stristr($page->get("author"), $token)) $sortScore += 2; - } - if ($page->location!=$location) { - $page->set("sortScore", $sortScore); - array_push($array, $page); - } - } - $this->exchangeArray($array); - $this->sort("modified", false)->sort("sortScore", false); - } - return $this; - } - - // Sort page collection by page setting - public function sort($key, $ascendingOrder = true): YellowPageCollection { - $array = $this->getArrayCopy(); - $sortIndex = 0; - foreach ($array as $page) { - $page->set("sortIndex", ++$sortIndex); - } - $callback = function ($a, $b) use ($key, $ascendingOrder) { - $result = $ascendingOrder ? - strnatcasecmp($a->get($key), $b->get($key)) : - strnatcasecmp($b->get($key), $a->get($key)); - return $result==0 ? $a->get("sortIndex") - $b->get("sortIndex") : $result; - }; - usort($array, $callback); - $this->exchangeArray($array); - return $this; - } - - // Group page collection by page setting, return array with multiple collections - public function group($key, $ascendingOrder = true, $format = ""): array { - $array = array(); - $groupByInitial = $format=="initial"; - $groupByDate = !is_string_empty($format) && $format!="count" && $format!="initial"; - foreach ($this->getIterator() as $page) { - if ($page->isExisting($key)) { - foreach (preg_split("/\s*,\s*/", $page->get($key)) as $group) { - if ($groupByInitial) { - $group = strtoupperu(substru($group, 0, 1)); - } elseif ($groupByDate) { - $group = $this->yellow->language->getDateFormatted(strtotime($group), $format); - } - if (!is_string_empty($group)) { - if (!isset($array[$group])) { - $groupSearch = strtoloweru($group); - foreach (array_keys($array) as $groupFound) { - if (strtoloweru($groupFound)==$groupSearch) { - $group = $groupFound; - break; - } - } - if (!isset($array[$group])) $array[$group] = new YellowPageCollection($this->yellow); - } - $array[$group]->append($page); - } - } - } - } - $callbackString = function ($a, $b) use ($ascendingOrder) { - return $ascendingOrder ? strnatcasecmp($a, $b) : strnatcasecmp($b, $a); - }; - $callbackCollection = function ($a, $b) use ($ascendingOrder) { - return $ascendingOrder ? count($a)-count($b) : count($b)-count($a); - }; - if ($format!="count") { - uksort($array, $callbackString); - } else { - uasort($array, $callbackCollection); - } - return $array; - } - - // Calculate union, merge page collection - public function merge($input): YellowPageCollection { - $this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input)); - return $this; - } - - // Calculate intersection, remove pages that are not present in another page collection - public function intersect($input): YellowPageCollection { - $callback = function ($a, $b) { - return strcmp($a->location, $b->location); - }; - $this->exchangeArray(array_uintersect($this->getArrayCopy(), (array)$input, $callback)); - return $this; - } - - // Calculate difference, remove pages that are present in another page collection - public function diff($input): YellowPageCollection { - $callback = function ($a, $b) { - return strcmp($a->location, $b->location); - }; - $this->exchangeArray(array_udiff($this->getArrayCopy(), (array)$input, $callback)); - return $this; - } - - // Limit the number of pages in page collection - public function limit($pagesMax): YellowPageCollection { - $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax)); - return $this; - } - - // Reverse page collection - public function reverse(): YellowPageCollection { - $this->exchangeArray(array_reverse($this->getArrayCopy())); - return $this; - } - - // Randomize page collection - public function shuffle(): YellowPageCollection { - $array = $this->getArrayCopy(); - shuffle($array); - $this->exchangeArray($array); - return $this; - } - - // Paginate page collection - public function paginate($limit): YellowPageCollection { - if (!$this->isPagination() && $limit!=0) { - $this->paginationNumber = 1; - $this->paginationCount = ceil($this->count() / $limit); - if ($this->yellow->page->isRequest("page")) { - $this->paginationNumber = intval($this->yellow->page->getRequest("page")); - } - if ($this->paginationNumber<0 || $this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0; - if ($this->paginationNumber) { - $this->exchangeArray(array_slice($this->getArrayCopy(), ($this->paginationNumber - 1) * $limit, $limit)); - } else { - $this->yellow->page->error(404); - } - } - return $this; - } - - // 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) { - $location = $locationArguments = ""; - if ($pageNumber>=1 && $pageNumber<=$this->paginationCount) { - $location = $this->yellow->page->getLocation($absoluteLocation); - $locationArguments = $this->yellow->toolbox->getLocationArgumentsNew("page", $pageNumber>1 ? "$pageNumber" : ""); - } - return $location.$locationArguments; - } - - // 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; - } - - // Check if page collection is empty - public function isEmpty() { - return empty($this->getArrayCopy()); - } -} - -class YellowArray extends ArrayObject { - public function __construct($array = []) { - parent::__construct($array); - } - - // Set array element - public function set($key, $value) { - $this->offsetSet($key, $value); - } - - // Return array element - public function get($key) { - return $this->offsetExists($key) ? $this->offsetGet($key) : ""; - } - - // Check if array element exists - public function isExisting($key) { - return $this->offsetExists($key); - } - - // Return array element - #[\ReturnTypeWillChange] - public function offsetGet($key) { - if (is_string($key)) $key = lcfirst($key); - return parent::offsetGet($key); - } - - // Set array element - #[\ReturnTypeWillChange] - public function offsetSet($key, $value) { - if (is_string($key)) $key = lcfirst($key); - parent::offsetSet($key, $value); - } - - // Remove array element - #[\ReturnTypeWillChange] - public function offsetUnset($key) { - if (is_string($key)) $key = lcfirst($key); - parent::offsetUnset($key); - } - - // Check if array element exists - #[\ReturnTypeWillChange] - public function offsetExists($key) { - if (is_string($key)) $key = lcfirst($key); - return parent::offsetExists($key); - } - - // Check if array is empty - public function isEmpty() { - return empty($this->getArrayCopy()); - } -} - -// Make string lowercase, UTF-8 compatible -function strtoloweru() { - return call_user_func_array("mb_strtolower", func_get_args()); -} - -// Make string uppercase, UTF-8 compatible -function strtoupperu() { - return call_user_func_array("mb_strtoupper", func_get_args()); -} - -// Return string length, UTF-8 characters -function strlenu() { - return call_user_func_array("mb_strlen", func_get_args()); -} - -// Return string length, bytes -function strlenb() { - return call_user_func_array("strlen", func_get_args()); -} - -// Return string position of first match, UTF-8 characters -function strposu() { - return call_user_func_array("mb_strpos", func_get_args()); -} - -// Return string position of first match, bytes -function strposb() { - return call_user_func_array("strpos", func_get_args()); -} - -// Return string position of last match, UTF-8 characters -function strrposu() { - return call_user_func_array("mb_strrpos", func_get_args()); -} - -// Return string position of last match, bytes -function strrposb() { - return call_user_func_array("strrpos", func_get_args()); -} - -// Return part of a string, UTF-8 characters -function substru() { - return call_user_func_array("mb_substr", func_get_args()); -} - -// Return part of a string, bytes -function substrb() { - return call_user_func_array("substr", func_get_args()); -} - -// Check if string is empty -function is_string_empty($string) { - return is_null($string) || $string===""; -} -function strempty($string) { return is_null($string) || $string===""; } //TODO: remove later, for backwards compatibility - -// Check if array is empty -function is_array_empty($array) { - return is_null($array) || (is_array($array) ? empty($array) : empty($array->getArrayCopy())); -} diff --git a/system/extensions/edit.php b/system/extensions/edit.php @@ -1,2018 +0,0 @@ -<?php -// Edit extension, https://github.com/annaesvensson/yellow-edit - -class YellowEdit { - const VERSION = "0.8.79"; - public $yellow; // access to API - public $response; // web response - public $merge; // text merge - public $editable; // page can be edited? (boolean) - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - $this->response = new YellowEditResponse($yellow); - $this->merge = new YellowEditMerge($yellow); - $this->yellow->system->setDefault("editSiteEmail", "noreply"); - $this->yellow->system->setDefault("editLocation", "/edit/"); - $this->yellow->system->setDefault("editUploadNewLocation", "/media/@group/@filename"); - $this->yellow->system->setDefault("editUploadExtensions", ".gif, .jpg, .mp3, .ogg, .pdf, .png, .svg, .zip"); - $this->yellow->system->setDefault("editKeyboardShortcuts", "ctrl+b bold, ctrl+i italic, ctrl+k strikethrough, ctrl+e code, ctrl+s save, ctrl+alt+p preview"); - $this->yellow->system->setDefault("editToolbarButtons", "auto"); - $this->yellow->system->setDefault("editEndOfLine", "auto"); - $this->yellow->system->setDefault("editNewFile", "page-new-(.*).md"); - $this->yellow->system->setDefault("editUserPasswordMinLength", "8"); - $this->yellow->system->setDefault("editUserHashAlgorithm", "bcrypt"); - $this->yellow->system->setDefault("editUserHashCost", "10"); - $this->yellow->system->setDefault("editUserAccess", "create, edit, delete, restore, upload"); - $this->yellow->system->setDefault("editUserHome", "/"); - $this->yellow->system->setDefault("editLoginRestriction", "0"); - $this->yellow->system->setDefault("editLoginSessionTimeout", "2592000"); - $this->yellow->system->setDefault("editBruteForceProtection", "25"); - } - - // Handle update - public function onUpdate($action) { - if ($action=="clean" || $action=="daily") { - $cleanup = false; - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $fileData = $this->yellow->toolbox->readFile($fileNameUser); - $fileDataNew = ""; - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (lcfirst($matches[1])=="email" && !is_string_empty($matches[2])) { - $status = $this->yellow->user->getUser("status", $matches[2]); - $reserved = strtotime($this->yellow->user->getUser("modified", $matches[2])) + 60*60*24; - $cleanup = $status!="active" && $status!="inactive" && $reserved<=time(); - } - } - if (!$cleanup) $fileDataNew .= $line; - } - $fileDataNew = rtrim($fileDataNew)."\n"; - if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileNameUser, $fileDataNew)) { - $this->yellow->toolbox->log("error", "Can't write file '$fileNameUser'!"); - } - } - } - - // Handle request - public function onRequest($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->isEditLocation($location)) { - $this->editable = true; - $scheme = $this->yellow->system->get("coreServerScheme"); - $address = $this->yellow->system->get("coreServerAddress"); - $base = rtrim($this->yellow->system->get("coreServerBase").$this->yellow->system->get("editLocation"), "/"); - list($scheme, $address, $base, $location, $fileName) = $this->yellow->lookup->getRequestInformation($scheme, $address, $base); - $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); - $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName); - } - return $statusCode; - } - - // Handle command - public function onCommand($command, $text) { - switch ($command) { - case "user": $statusCode = $this->processCommandUser($command, $text); break; - default: $statusCode = 0; - } - return $statusCode; - } - - // Handle command help - public function onCommandHelp() { - return "user [option email password]"; - } - - // Handle page meta data - public function onParseMetaData($page) { - $page->set("editPageUrl", $this->yellow->lookup->normaliseUrl( - $this->yellow->system->get("coreServerScheme"), - $this->yellow->system->get("coreServerAddress"), - $this->yellow->system->get("coreServerBase"), - rtrim($this->yellow->system->get("editLocation"), "/").$page->location)); - } - - // Handle page content element - public function onParseContentElement($page, $name, $text, $attributes, $type) { - $output = null; - if ($name=="edit" && $type=="inline") { - list($target, $description) = $this->yellow->toolbox->getTextList($text, " ", 2); - if (is_string_empty($target) || $target=="-") $target = "main"; - if (is_string_empty($description)) $description = ucfirst($name); - $pageTarget = $target=="main" ? $page->getPage("main") : $page->getPage("main")->getPage($target); - $output = "<a href=\"".$pageTarget->get("editPageUrl")."\">".htmlspecialchars($description)."</a>"; - } - return $output; - } - - // Handle page layout - public function onParsePageLayout($page, $name) { - if ($this->editable) { - $this->response->processPageData($page); - } - } - - // Handle page extra data - public function onParsePageExtra($page, $name) { - $output = null; - if ($this->editable && $name=="header") { - $extensionLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreExtensionLocation"); - $output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"{$extensionLocation}edit.css\" />\n"; - $output .= "<script type=\"text/javascript\" src=\"{$extensionLocation}edit.js\"></script>\n"; - $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.user = ".json_encode($this->response->getUserData()).";\n"; - $output .= "yellow.language = ".json_encode($this->response->getLanguageData()).";\n"; - $output .= "// ]]>\n"; - $output .= "</script>\n"; - } - return $output; - } - - // Process command to update user account - public function processCommandUser($command, $text) { - list($option) = $this->yellow->toolbox->getTextArguments($text); - switch ($option) { - case "": $statusCode = $this->userShow($command, $text); break; - case "add": $statusCode = $this->userAdd($command, $text); break; - case "change": $statusCode = $this->userChange($command, $text); break; - case "remove": $statusCode = $this->userRemove($command, $text); break; - default: $statusCode = 400; echo "Yellow $command: Invalid arguments\n"; - } - return $statusCode; - } - - // Show user accounts - public function userShow($command, $text) { - $data = array(); - foreach ($this->yellow->user->settings as $key=>$value) { - $data[$key] = "$value[email] - User account by $value[name]."; - } - uksort($data, "strnatcasecmp"); - foreach ($data as $line) echo "$line\n"; - if (is_array_empty($data)) echo "Yellow $command: No user accounts\n"; - return 200; - } - - // Add user account - public function userAdd($command, $text) { - $status = "ok"; - list($option, $email, $password) = $this->yellow->toolbox->getTextArguments($text); - if (is_string_empty($email) || is_string_empty($password)) $status = $this->response->status = "incomplete"; - if ($status=="ok") $status = $this->getUserAccount("add", $email, $password); - if ($status=="ok" && $this->isUserAccountTaken($email)) $status = "taken"; - switch ($status) { - case "incomplete": echo "ERROR updating settings: Please enter email and password!\n"; break; - case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; - 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; - case "short": echo "ERROR updating settings: Please enter a longer password!\n"; break; - } - if ($status=="ok") { - $name = $this->yellow->system->get("sitename"); - $userLanguage = $this->yellow->system->get("language"); - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array( - "name" => $name, - "description" => $this->yellow->language->getText("editUserDescription", $userLanguage), - "language" => $userLanguage, - "access" => $this->yellow->system->get("editUserAccess"), - "home" => $this->yellow->system->get("editUserHome"), - "hash" => $this->response->createHash($password), - "stamp" => $this->response->createStamp(), - "pending" => "none", - "failed" => "0", - "modified" => date("Y-m-d H:i:s", time()), - "status" => "active"); - $status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; - if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; - $this->yellow->toolbox->log($status=="ok" ? "info" : "error", "Add user '".strtok($name, " ")."'"); - } - if ($status=="ok") { - $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); - $status = substru($this->yellow->user->getUser("hash", $email), 0, 10)!="error-hash" ? "ok" : "error"; - if ($status=="error") echo "ERROR updating settings: Hash algorithm '$algorithm' not supported!\n"; - } - $statusCode = $status=="ok" ? 200 : 500; - echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."added\n"; - return $statusCode; - } - - // Change user account - public function userChange($command, $text) { - $status = "ok"; - list($option, $email, $password) = $this->yellow->toolbox->getTextArguments($text); - if (is_string_empty($email)) $status = $this->response->status = "invalid"; - if ($status=="ok") $status = $this->getUserAccount("change", $email, $password); - if ($status=="ok" && !$this->yellow->user->isExisting($email)) $status = "unknown"; - switch ($status) { - case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; - case "unknown": echo "ERROR updating settings: Can't find email '$email'!\n"; break; - case "weak": echo "ERROR updating settings: Please enter a different password!\n"; break; - case "short": echo "ERROR updating settings: Please enter a longer password!\n"; break; - } - if ($status=="ok") { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array( - "hash" => is_string_empty($password) ? $this->yellow->user->getUser("hash", $email) : $this->response->createHash($password), - "failed" => "0", - "modified" => date("Y-m-d H:i:s", time())); - $status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; - if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; - } - $statusCode = $status=="ok" ? 200 : 500; - echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."changed\n"; - return $statusCode; - } - - // Remove user account - public function userRemove($command, $text) { - $status = "ok"; - list($option, $email) = $this->yellow->toolbox->getTextArguments($text); - if (is_string_empty($email)) $status = $this->response->status = "invalid"; - if ($status=="ok") $status = $this->getUserAccount("remove", $email, ""); - if ($status=="ok" && !$this->yellow->user->isExisting($email)) $status = "unknown"; - switch ($status) { - case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; - case "unknown": echo "ERROR updating settings: Can't find email '$email'!\n"; break; - } - if ($status=="ok") { - $name = $this->yellow->user->getUser("name", $email); - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $status = $this->yellow->user->remove($fileNameUser, $email) ? "ok" : "error"; - if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; - $this->yellow->toolbox->log($status=="ok" ? "info" : "error", "Remove user '".strtok($name, " ")."'"); - } - $statusCode = $status=="ok" ? 200 : 500; - echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."removed\n"; - return $statusCode; - } - - // Process request - public function processRequest($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->checkUserAuth($scheme, $address, $base, $location, $fileName)) { - switch ($this->yellow->page->getRequest("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 "quit": $statusCode = $this->processRequestQuit($scheme, $address, $base, $location, $fileName); break; - case "account": $statusCode = $this->processRequestAccount($scheme, $address, $base, $location, $fileName); break; - case "configure": $statusCode = $this->processRequestConfigure($scheme, $address, $base, $location, $fileName); break; - case "update": $statusCode = $this->processRequestUpdate($scheme, $address, $base, $location, $fileName); break; - case "create": $statusCode = $this->processRequestCreate($scheme, $address, $base, $location, $fileName); break; - case "edit": $statusCode = $this->processRequestEdit($scheme, $address, $base, $location, $fileName); break; - case "delete": $statusCode = $this->processRequestDelete($scheme, $address, $base, $location, $fileName); break; - case "restore": $statusCode = $this->processRequestRestore($scheme, $address, $base, $location, $fileName); break; - case "preview": $statusCode = $this->processRequestPreview($scheme, $address, $base, $location, $fileName); break; - case "upload": $statusCode = $this->processRequestUpload($scheme, $address, $base, $location, $fileName); break; - } - } elseif ($this->checkUserUnauth($scheme, $address, $base, $location, $fileName)) { - $this->yellow->lookup->requestHandler = "core"; - switch ($this->yellow->page->getRequest("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->getRedirectLocation($location); - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(301, $location); - } else { - $statusCode = 404; - if ($this->response->isUserAccess("create", $location)) $statusCode = 434; - if ($this->response->isUserAccess("restore", $location) && $this->response->isDeletedLocation($location)) { - $statusCode = 435; - } - $this->yellow->page->error($statusCode); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - } - return $statusCode; - } - - // Process request for user login - public function processRequestLogin($scheme, $address, $base, $location, $fileName) { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time())); - if ($this->yellow->user->save($fileNameUser, $this->response->userEmail, $settings)) { - $home = $this->yellow->user->getUser("home", $this->response->userEmail); - if (substru($location, 0, strlenu($home))==$home) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } 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("coreServerScheme"), - $this->yellow->system->get("coreServerAddress"), - $this->yellow->system->get("coreServerBase"), - $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", "-", $this->yellow->page->getRequest("name"))); - $email = trim($this->yellow->page->getRequest("email")); - $password = trim($this->yellow->page->getRequest("password")); - $consent = trim($this->yellow->page->getRequest("consent")); - if (is_string_empty($name) || is_string_empty($email) || is_string_empty($password) || is_string_empty($consent)) $this->response->status = "incomplete"; - if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); - if ($this->response->status=="ok" && $this->response->isLoginRestriction()) $this->response->status = "next"; - if ($this->response->status=="ok" && $this->isUserAccountTaken($email)) $this->response->status = "next"; - if ($this->response->status=="ok") { - $userLanguage = $this->yellow->lookup->findContentLanguage($fileName, $this->yellow->system->get("language")); - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array( - "name" => $name, - "description" => $this->yellow->language->getText("editUserDescription", $userLanguage), - "language" => $userLanguage, - "access" => $this->yellow->system->get("editUserAccess"), - "home" => $this->yellow->system->get("editUserHome"), - "hash" => $this->response->createHash($password), - "stamp" => $this->response->createStamp(), - "pending" => "none", - "failed" => "0", - "modified" => date("Y-m-d H:i:s", time()), - "status" => "unconfirmed"); - $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); - $this->response->status = substru($this->yellow->user->getUser("hash", $email), 0, 10)!="error-hash" ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Hash algorithm '$algorithm' not supported!"); - } - if ($this->response->status=="ok") { - $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 = $this->yellow->page->getRequest("email"); - $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "unapproved"); - $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $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 = $this->yellow->page->getRequest("email"); - $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); - if ($this->response->status=="ok") { - $name = $this->yellow->user->getUser("name", $email); - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "active"); - $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - $this->yellow->toolbox->log($this->response->status=="ok" ? "info" : "error", "Add user '".strtok($name, " ")."'"); - } - if ($this->response->status=="ok") { - $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "welcome") ? "done" : "error"; - 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($this->yellow->page->getRequest("email")); - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid"; - if ($this->response->status=="ok" && !$this->yellow->user->isExisting($email)) $this->response->status = "next"; - if ($this->response->status=="ok") { - $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "recover") ? "next" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); - } - $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($this->yellow->page->getRequest("email")); - $password = trim($this->yellow->page->getRequest("password")); - $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); - if ($this->response->status=="ok") { - if (is_string_empty($password)) $this->response->status = "password"; - if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array("hash" => $this->response->createHash($password), "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); - $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $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 = $this->yellow->page->getRequest("email"); - $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "active"); - $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "done" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - 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 = $this->yellow->page->getRequest("email"); - $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); - if ($this->response->status=="ok") { - $emailSource = $this->yellow->user->getUser("pending", $email); - if ($this->yellow->user->getUser("status", $emailSource)!="active") $this->response->status = "done"; - } - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "unchanged"); - $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $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($this->yellow->page->getRequest("email")); - $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); - if ($this->response->status=="ok") { - list($email, $hash) = $this->yellow->toolbox->getTextList($this->yellow->user->getUser("pending", $email), ":", 2); - if (!$this->yellow->user->isExisting($email) || is_string_empty($hash)) $this->response->status = "done"; - } - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array( - "hash" => $hash, - "pending" => "none", - "failed" => "0", - "modified" => date("Y-m-d H:i:s", time()), - "status" => "active"); - $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok" && $email!=$emailSource) { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $this->response->status = $this->yellow->user->remove($fileNameUser, $emailSource) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $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 quit account - public function processRequestQuit($scheme, $address, $base, $location, $fileName) { - $this->response->action = "quit"; - $this->response->status = "ok"; - $name = trim($this->yellow->page->getRequest("name")); - $email = $this->response->userEmail; - if (is_string_empty($name)) $this->response->status = "none"; - if ($this->response->status=="ok" && $name!=$this->yellow->user->getUser("name", $email)) $this->response->status = "mismatch"; - if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, ""); - if ($this->response->status=="ok") { - $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "remove") ? "next" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); - } - $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 = $this->yellow->page->getRequest("email"); - $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); - if ($this->response->status=="ok") { - $name = $this->yellow->user->getUser("name", $email); - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "removed"); - $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - $this->yellow->toolbox->log($this->response->status=="ok" ? "info" : "error", "Remove user '".strtok($name, " ")."'"); - } - if ($this->response->status=="ok") { - $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "goodbye") ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); - } - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $this->response->status = $this->yellow->user->remove($fileNameUser, $email) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $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 change account settings - public function processRequestAccount($scheme, $address, $base, $location, $fileName) { - $this->response->action = "account"; - $this->response->status = "ok"; - $email = trim($this->yellow->page->getRequest("email")); - $emailSource = $this->response->userEmail; - $password = trim($this->yellow->page->getRequest("password")); - $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $this->yellow->page->getRequest("name"))); - $language = trim($this->yellow->page->getRequest("language")); - if ($email!=$emailSource || !is_string_empty($password)) { - if (is_string_empty($email)) $this->response->status = "invalid"; - if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); - if ($this->response->status=="ok" && $email!=$emailSource && $this->isUserAccountTaken($email)) $this->response->status = "taken"; - if ($this->response->status=="ok" && $email!=$emailSource) { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array( - "name" => $name, - "description" => $this->yellow->user->getUser("description", $emailSource), - "language" => $language, - "access" => $this->yellow->user->getUser("access", $emailSource), - "home" => $this->yellow->user->getUser("home", $emailSource), - "hash" => $this->response->createHash("none"), - "stamp" => $this->response->createStamp(), - "pending" => $emailSource, - "failed" => "0", - "modified" => date("Y-m-d H:i:s", time()), - "status" => "unverified"); - $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array( - "name" => $name, - "language" => $language, - "pending" => $email.":".(is_string_empty($password) ? $this->yellow->user->getUser("hash", $emailSource) : $this->response->createHash($password)), - "failed" => "0", - "modified" => date("Y-m-d H:i:s", time())); - $this->response->status = $this->yellow->user->save($fileNameUser, $emailSource, $settings) ? "ok" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($this->response->status=="ok") { - $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("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array("name" => $name, "language" => $language, "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); - $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "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 change settings - public function processRequestConfigure($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->response->isUserAccess("configure")) { - $this->response->action = "configure"; - $this->response->status = "ok"; - $sitename = trim($this->yellow->page->getRequest("sitename")); - $author = trim($this->yellow->page->getRequest("author")); - $email = trim($this->yellow->page->getRequest("email")); - if ($email!=$this->yellow->system->get("email")) { - if (is_string_empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid"; - } - if ($this->response->status=="ok") { - $fileNameSystem = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - $settings = array("sitename" => $sitename, "author" => $author, "email" => $email); - $file = $this->response->getFileSystem($scheme, $address, $base, $location, $fileNameSystem, $settings); - $this->response->status = (!$file->isError() && $this->yellow->system->save($fileNameSystem, $settings)) ? "done" : "error"; - if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameSystem'!"); - } - if ($this->response->status=="done") { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $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->response->isUserAccess("update")) { - $this->response->action = "update"; - $this->response->status = "ok"; - if ($this->yellow->page->getRequest("option")=="check") { - list($statusCode, $rawData) = $this->response->getUpdateInformation(); - $this->response->status = is_string_empty($rawData) ? "ok" : "updates"; - $this->response->rawDataOutput = $rawData; - if ($statusCode!=200) { - $this->response->status = "error"; - $this->response->rawDataOutput = ""; - } - } else { - $this->response->status = $this->yellow->command("update all")==0 ? "done" : "error"; - } - 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 create page - public function processRequestCreate($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->response->isUserAccess("create", $location) && !is_string_empty($this->yellow->page->getRequest("rawdataedit"))) { - $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); - $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdatasource"); - $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("rawdataendofline"); - $rawData = $this->yellow->page->getRequest("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->errorMessage); - $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->isUserAccess("edit", $location) && !is_string_empty($this->yellow->page->getRequest("rawdataedit"))) { - $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); - $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdataedit"); - $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("rawdataendofline"); - $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)) { - $ok = $this->yellow->toolbox->renameFile($fileName, $page->fileName, true) && - $this->yellow->toolbox->createFile($page->fileName, $page->rawData); - } else { - $ok = $this->yellow->toolbox->renameDirectory(dirname($fileName), dirname($page->fileName), true) && - $this->yellow->toolbox->createFile($page->fileName, $page->rawData); - } - if ($ok) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $this->yellow->page->error(500, "Can't write file '$page->fileName'!"); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - } else { - $this->yellow->page->error(500, $page->errorMessage); - $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->isUserAccess("delete", $location) && is_file($fileName)) { - $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); - $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdatasource"); - $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("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)) { - $ok = $this->response->deleteFileLocation($location, $fileName); - } else { - $ok = $this->response->deleteDirectoryLocation($location, $fileName); - } - if ($ok) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $this->yellow->page->error(500, "Can't delete file '$fileName'!"); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - } else { - $this->yellow->page->error(500, $page->errorMessage); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - } - return $statusCode; - } - - // Process request to restore deleted page - public function processRequestRestore($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->response->isUserAccess("restore", $location) && !is_file($fileName)) { - $page = $this->response->getPageRestore($scheme, $address, $base, $location, $fileName); - if (!$page->isError()) { - if ($this->yellow->lookup->isFileLocation($location)) { - $ok = $this->response->restoreFileLocation($location); - } else { - $ok = $this->response->restoreDirectoryLocation($location); - } - if ($ok) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $this->yellow->page->error(500, "Can't restore file '$fileName'!"); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - } else { - $this->yellow->page->error(500, $page->errorMessage); - $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); - } - } - return $statusCode; - } - - // Process request to show preview - public function processRequestPreview($scheme, $address, $base, $location, $fileName) { - $page = $this->response->getPagePreview($scheme, $address, $base, $location, $fileName, - $this->yellow->page->getRequest("rawdataedit"), $this->yellow->page->getRequest("rawdataendofline")); - $page->headerData = array( - "Cache-Control"=>"no-cache, no-store", - "Content-Type"=>$this->yellow->toolbox->getMimeContentType("a.html"), - "Last-Modified"=>$this->yellow->toolbox->getHttpDateFormatted(time())); - $statusCode = $this->yellow->sendData($page->statusCode, $page->headerData, $page->outputData); - if ($this->yellow->system->get("coreDebugMode")>=1) echo "YellowEdit::processRequestPreview file:$fileName<br/>\n"; - return $statusCode; - } - - // 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->isUserAccess("upload", $location) && 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'!"; - } - $headerData = array( - "Cache-Control"=>"no-cache, no-store", - "Content-Type"=>$this->yellow->toolbox->getMimeContentType("a.json"), - "Last-Modified"=>$this->yellow->toolbox->getHttpDateFormatted(time())); - return $this->yellow->sendData(isset($data["error"]) ? 500 : 200, $headerData, json_encode($data)); - } - - // Check user authentication - public function checkUserAuth($scheme, $address, $base, $location, $fileName) { - $action = $this->yellow->page->getRequest("action"); - $authToken = $this->yellow->toolbox->getCookie("authtoken"); - $csrfToken = $this->yellow->toolbox->getCookie("csrftoken"); - if (is_string_empty($action) || $this->isRequestSameSite("POST", $scheme, $address)) { - if ($action=="login") { - $email = $this->yellow->page->getRequest("email"); - $password = $this->yellow->page->getRequest("password"); - if ($this->response->checkAuthLogin($email, $password)) { - $this->response->createCookies($scheme, $address, $base, $email); - $this->response->userEmail = $email; - $this->response->language = $this->getUserLanguage($email); - } else { - $this->response->userFailedError = "login"; - $this->response->userFailedEmail = $email; - $this->response->userFailedExpire = PHP_INT_MAX; - } - } elseif (!is_string_empty($authToken) && !is_string_empty($csrfToken)) { - $csrfTokenReceived = isset($_POST["csrftoken"]) ? $_POST["csrftoken"] : ""; - $csrfTokenIrrelevant = is_string_empty($action); - if ($this->response->checkAuthToken($authToken, $csrfToken, $csrfTokenReceived, $csrfTokenIrrelevant)) { - $this->response->userEmail = $email = $this->response->getAuthEmail($authToken); - $this->response->language = $this->getUserLanguage($email); - } else { - $this->response->userFailedError = "auth"; - $this->response->userFailedEmail = $this->response->getAuthEmail($authToken); - $this->response->userFailedExpire = $this->response->getAuthExpire($authToken); - } - } - $this->yellow->user->set($this->response->userEmail); - } - return $this->response->isUser(); - } - - // Check user without authentication - public function checkUserUnauth($scheme, $address, $base, $location, $fileName) { - $ok = false; - $action = $this->yellow->page->getRequest("action"); - if (is_string_empty($action) || $action=="signup" || $action=="forgot") { - $ok = true; - } elseif ($this->yellow->page->isRequest("actiontoken")) { - $actionToken = $this->yellow->page->getRequest("actiontoken"); - $email = $this->yellow->page->getRequest("email"); - $action = $this->yellow->page->getRequest("action"); - $expire = $this->yellow->page->getRequest("expire"); - $language = $this->yellow->page->getRequest("language"); - if ($this->response->checkActionToken($actionToken, $email, $action, $expire)) { - $ok = true; - $this->response->language = $this->getActionLanguage($language); - } else { - $this->response->userFailedError = "action"; - $this->response->userFailedEmail = $email; - $this->response->userFailedExpire = $expire; - } - } - return $ok; - } - - // Check user failed - public function checkUserFailed($scheme, $address, $base, $location, $fileName) { - if (!is_string_empty($this->response->userFailedError)) { - if ($this->response->userFailedExpire>time() && $this->yellow->user->isExisting($this->response->userFailedEmail)) { - $email = $this->response->userFailedEmail; - $failed = $this->yellow->user->getUser("failed", $email)+1; - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $status = $this->yellow->user->save($fileNameUser, $email, array("failed" => $failed)) ? "ok" : "error"; - if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - if ($failed==$this->yellow->system->get("editBruteForceProtection")) { - $statusBeforeProtection = $this->yellow->user->getUser("status", $email); - $statusAfterProtection = ($statusBeforeProtection=="active" || $statusBeforeProtection=="inactive") ? "inactive" : "failed"; - if ($status=="ok") { - $status = $this->yellow->user->save($fileNameUser, $email, array("status" => $statusAfterProtection)) ? "ok" : "error"; - if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); - } - if ($status=="ok" && $statusBeforeProtection=="active") { - $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->yellow->user->getUser("status", $email)==$statusExpected ? "ok" : "done"; - } - - // Return user account changes - public function getUserAccount($action, $email, $password) { - $status = null; - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onEditUserAccount")) { - $status = $value["object"]->onEditUserAccount($action, $email, $password); - if (!is_null($status)) break; - } - } - if (is_null($status)) { - $status = "ok"; - if (!is_string_empty($password) && strlenu($password)<$this->yellow->system->get("editUserPasswordMinLength")) $status = "short"; - if (!is_string_empty($password) && $password==$email) $status = "weak"; - if (!is_string_empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) $status = "invalid"; - } - return $status; - } - - // Return user language - public function getUserLanguage($email) { - $language = $this->yellow->user->getUser("language", $email); - if (!$this->yellow->language->isExisting($language)) $language = $this->yellow->system->get("language"); - return $language; - } - - // Return action language - public function getActionLanguage($language) { - if (!$this->yellow->language->isExisting($language)) $language = $this->yellow->system->get("language"); - return $language; - } - - // Check if user account is taken - public function isUserAccountTaken($email) { - $taken = false; - if ($this->yellow->user->isExisting($email)) { - $status = $this->yellow->user->getUser("status", $email); - $reserved = strtotime($this->yellow->user->getUser("modified", $email)) + 60*60*24; - if ($status=="active" || $status=="inactive" || $reserved>time()) $taken = true; - } - return $taken; - } - - // Check if request came from same site - public function isRequestSameSite($method, $scheme, $address) { - $origin = ""; - if (preg_match("#^(\w+)://([^/]+)(.*)$#", $this->yellow->toolbox->getServer("HTTP_REFERER"), $matches)) $origin = "$matches[1]://$matches[2]"; - if ($this->yellow->toolbox->getServer("HTTP_ORIGIN")) $origin = $this->yellow->toolbox->getServer("HTTP_ORIGIN"); - return $this->yellow->toolbox->getServer("REQUEST_METHOD")==$method && $origin=="$scheme://$address"; - } - - // Check if edit location - public function isEditLocation($location) { - $locationLength = strlenu($this->yellow->system->get("editLocation")); - return substru($location, 0, $locationLength)==$this->yellow->system->get("editLocation"); - } -} - -class YellowEditResponse { - public $yellow; // access to API - public $extension; // access to extension - public $userEmail; // user email - public $userFailedError; // error of failed authentication - public $userFailedEmail; // email of failed authentication - public $userFailedExpire; // expiration time of failed authentication - public $rawDataSource; // raw data of page for comparison - public $rawDataEdit; // raw data of page for editing - public $rawDataOutput; // raw data of dynamic output - public $rawDataReadonly; // raw data is read only? (boolean) - public $rawDataEndOfLine; // end of line format for raw data - public $language; // response language - public $action; // response action - public $status; // response status - - public function __construct($yellow) { - $this->yellow = $yellow; - $this->extension = $yellow->extension->get("edit"); - $this->userEmail = ""; - } - - // Process page data - public function processPageData($page) { - if ($this->isUser()) { - if (is_string_empty($this->rawDataSource)) $this->rawDataSource = $page->rawData; - if (is_string_empty($this->rawDataEdit)) $this->rawDataEdit = $page->rawData; - if (is_string_empty($this->rawDataEndOfLine)) $this->rawDataEndOfLine = $this->getEndOfLine($page->rawData); - if ($page->statusCode==404 || $this->yellow->toolbox->isLocationArguments()) { - $this->rawDataEdit = $this->getRawDataGenerated($page); - $this->rawDataReadonly = true; - } - if ($page->statusCode==434 || $page->statusCode==435) { - $this->rawDataEdit = $this->getRawDataNew($page, true); - $this->rawDataReadonly = false; - } - } - if (is_string_empty($this->language)) $this->language = $page->get("language"); - if (is_string_empty($this->action)) $this->action = $this->isUser() ? "none" : "login"; - if (is_string_empty($this->status)) $this->status = "none"; - if ($this->status=="error") $this->action = "error"; - } - - // Return new page - public function getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { - $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine); - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); - $page->parseMeta($rawData); - $this->editContentFile($page, "create", $this->userEmail); - if ($this->yellow->content->find($page->location)) { - $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation")); - $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); - while ($this->yellow->content->find($page->location) || is_string_empty($page->fileName)) { - $page->rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData)); - $page->rawData = $this->yellow->lookup->normaliseLines($page->rawData, $endOfLine); - $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation")); - $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); - if (++$pageCounter>999) break; - } - if ($this->yellow->content->find($page->location) || is_string_empty($page->fileName)) { - $page->error(500, "Page '".$page->get("title")."' is not possible!"); - } - } else { - $page->fileName = $this->getPageNewFile($page->location); - } - if (!$this->isUserAccess("create", $page->location)) { - $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) { - $rawDataSource = $this->yellow->lookup->normaliseLines($rawDataSource, $endOfLine); - $rawDataEdit = $this->yellow->lookup->normaliseLines($rawDataEdit, $endOfLine); - $rawDataFile = $this->yellow->lookup->normaliseLines($rawDataFile, $endOfLine); - $rawData = $this->extension->merge->merge($rawDataSource, $rawDataEdit, $rawDataFile); - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); - $page->parseMeta($rawData); - $pageSource = new YellowPage($this->yellow); - $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName, false); - $pageSource->parseMeta($rawDataSource); - $this->editContentFile($page, "edit", $this->userEmail); - if ($this->isMetaModified($pageSource, $page) && $page->location!=$this->yellow->content->getHomeLocation($page->location)) { - $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation"), true); - $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); - if ($page->location!=$pageSource->location && ($this->yellow->content->find($page->location) || is_string_empty($page->fileName))) { - $page->error(500, "Page '".$page->get("title")."' is not possible!"); - } - } - if (is_string_empty($page->rawData)) $page->error(500, "Page has been modified by someone else!"); - if (!$this->isUserAccess("edit", $page->location) || - !$this->isUserAccess("edit", $pageSource->location)) { - $page->error(500, "Page '".$page->get("title")."' is restricted!"); - } - return $page; - } - - // Return deleted page - public function getPageDelete($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { - $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine); - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); - $page->parseMeta($rawData); - $this->editContentFile($page, "delete", $this->userEmail); - if (!$this->isUserAccess("delete", $page->location)) { - $page->error(500, "Page '".$page->get("title")."' is restricted!"); - } - return $page; - } - - // Return restored page - public function getPageRestore($scheme, $address, $base, $location, $fileName) { - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); - $page->parseMeta(""); - $this->editContentFile($page, "restore", $this->userEmail); - if (!$this->isUserAccess("restore", $page->location)) { - $page->error(500, "Page '".$page->get("title")."' is restricted!"); - } - return $page; - } - - // Return preview page - public function getPagePreview($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { - $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine); - $page = new YellowPage($this->yellow); - $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); - $page->parseMeta($rawData, 200); - $this->yellow->language->set($page->get("language")); - $class = "page-preview layout-".$page->get("layout"); - $output = "<div class=\"".htmlspecialchars($class)."\"><div class=\"content\"><div class=\"main\">"; - if ($this->yellow->system->get("editToolbarButtons")!="none") $output .= "<h1>".$page->getHtml("titleContent")."</h1>\n"; - $output .= $page->getContentHtml(); - $output .= "</div></div></div>"; - $page->statusCode = 200; - $page->outputData = $output; - return $page; - } - - // Return uploaded file - public function getFileUpload($scheme, $address, $base, $pageLocation, $fileNameTemp, $fileNameShort) { - $file = new YellowPage($this->yellow); - $file->setRequestInformation($scheme, $address, $base, "/".$fileNameTemp, $fileNameTemp, false); - $file->parseMeta(null); - $file->set("fileNameShort", $fileNameShort); - $file->set("type", $this->yellow->toolbox->getFileType($fileNameShort)); - if ($file->get("type")=="html" || $file->get("type")=="svg") { - $fileData = $this->yellow->toolbox->readFile($fileNameTemp); - $fileData = $this->yellow->lookup->normaliseData($fileData, $file->get("type")); - if (is_string_empty($fileData) || !$this->yellow->toolbox->createFile($fileNameTemp, $fileData)) { - $file->error(500, "Can't write file '$fileNameTemp'!"); - } - } - $this->editMediaFile($file, "upload", $this->userEmail); - $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 system file - public function getFileSystem($scheme, $address, $base, $pageLocation, $fileNameSystem, $settings) { - $file = new YellowPage($this->yellow); - $file->setRequestInformation($scheme, $address, $base, "/".$fileNameSystem, $fileNameSystem, false); - $file->parseMeta(null); - foreach ($settings as $key=>$value) $file->set($key, $value); - $this->editSystemFile($file, "configure", $this->userEmail); - return $file; - } - - // Return page data including status information - public function getPageData($page) { - $data = array(); - $data["scheme"] = $this->yellow->page->scheme; - $data["address"] = $this->yellow->page->address; - $data["base"] = $this->yellow->page->base; - $data["location"] = $this->yellow->page->location; - if ($this->isUser()) { - $data["title"] = $this->yellow->toolbox->getMetaData($this->rawDataEdit, "title"); - $data["rawDataSource"] = $this->rawDataSource; - $data["rawDataEdit"] = $this->rawDataEdit; - $data["rawDataNew"] = $this->getRawDataNew($page); - $data["rawDataOutput"] = strval($this->rawDataOutput); - $data["rawDataReadonly"] = intval($this->rawDataReadonly); - $data["rawDataEndOfLine"] = $this->rawDataEndOfLine; - } - 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 - public function getSystemData() { - $data = array(); - $data["coreServerScheme"] = $this->yellow->system->get("coreServerScheme"); - $data["coreServerAddress"] = $this->yellow->system->get("coreServerAddress"); - $data["coreServerBase"] = $this->yellow->system->get("coreServerBase"); - $data["coreDebugMode"] = $this->yellow->system->get("coreDebugMode"); - $data = array_merge($data, $this->yellow->system->getSettings("", "Location")); - if ($this->isUser()) { - $data["coreFileSizeMax"] = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize")); - $data["coreProductRelease"] = "Datenstrom Yellow ".YellowCore::RELEASE; - $data["coreExtensions"] = array(); - foreach ($this->yellow->extension->data as $key=>$value) { - $data["coreExtensions"][$key] = $value["class"]; - } - $data["coreLanguages"] = array(); - foreach ($this->yellow->system->getAvailable("language") as $language) { - $data["coreLanguages"][$language] = $this->yellow->language->getTextHtml("languageDescription", $language); - } - $data["editSettingsActions"] = $this->getSettingsActions(); - $data["editUploadExtensions"] = $this->yellow->system->get("editUploadExtensions"); - $data["editKeyboardShortcuts"] = $this->yellow->system->get("editKeyboardShortcuts"); - $data["editToolbarButtons"] = $this->getToolbarButtons(); - $data["editStatusValues"] = $this->getStatusValues(); - $data["emojiToolbarButtons"] = $this->yellow->system->get("emojiToolbarButtons"); - $data["iconToolbarButtons"] = $this->yellow->system->get("iconToolbarButtons"); - if ($this->isUserAccess("configure")) { - $data["sitename"] = $this->yellow->system->get("sitename"); - $data["author"] = $this->yellow->system->get("author"); - $data["email"] = $this->yellow->system->get("email"); - } - } else { - $data["editLoginEmail"] = $this->yellow->page->get("editLoginEmail"); - $data["editLoginPassword"] = $this->yellow->page->get("editLoginPassword"); - $data["editLoginRestriction"] = intval($this->isLoginRestriction()); - } - return $data; - } - - // Return user data - public function getUserData() { - $data = array(); - if ($this->isUser()) { - $data["email"] = $this->userEmail; - $data["name"] = $this->yellow->user->getUser("name", $this->userEmail); - $data["description"] = $this->yellow->user->getUser("description", $this->userEmail); - $data["language"] = $this->yellow->user->getUser("language", $this->userEmail); - $data["status"] = $this->yellow->user->getUser("status", $this->userEmail); - $data["access"] = $this->yellow->user->getUser("access", $this->userEmail); - $data["home"] = $this->yellow->user->getUser("home", $this->userEmail); - } - return $data; - } - - // Return language data - public function getLanguageData() { - $dataLanguage = $this->yellow->language->getSettings("language", "", $this->language); - $dataEdit = $this->yellow->language->getSettings("edit", "", $this->language); - return array_merge($dataLanguage, $dataEdit); - } - - // Return request data - public function getRequestData() { - $data = array(); - foreach ($_REQUEST as $key=>$value) { - if ($key=="password" || $key=="authtoken" || $key=="csrftoken" || $key=="actiontoken" || substru($key, 0, 7)=="rawdata") continue; - $data["request".ucfirst($key)] = trim($value); - } - return $data; - } - - // Return settings actions - public function getSettingsActions() { - $settingsActions = "account"; - if ($this->isUserAccess("configure")) $settingsActions .= ", configure"; - if ($this->isUserAccess("update")) $settingsActions .= ", update"; - return $settingsActions=="account" ? "none" : $settingsActions; - } - - // Return toolbar buttons - public function getToolbarButtons() { - $toolbarButtons = $this->yellow->system->get("editToolbarButtons"); - if ($toolbarButtons=="auto") { - $toolbarButtons = "format, bold, italic, strikethrough, code, separator, list, link, file"; - if ($this->yellow->extension->isExisting("emoji")) $toolbarButtons .= ", emoji"; - if ($this->yellow->extension->isExisting("icon")) $toolbarButtons .= ", icon"; - $toolbarButtons .= ", status, preview"; - } - return $toolbarButtons; - } - - // Return status values - public function getStatusValues() { - $statusValues = ""; - if ($this->yellow->extension->isExisting("private")) $statusValues .= ", private"; - if ($this->yellow->extension->isExisting("draft")) $statusValues .= ", draft"; - $statusValues .= ", unlisted"; - return ltrim($statusValues, ", "); - } - - // Return end of line format - public function getEndOfLine($rawData = "") { - $endOfLine = $this->yellow->system->get("editEndOfLine"); - if ($endOfLine=="auto") { - $rawData = is_string_empty($rawData) ? PHP_EOL : substru($rawData, 0, 4096); - $endOfLine = strposu($rawData, "\r")===false ? "lf" : "crlf"; - } - return $endOfLine; - } - - // Return update information - public function getUpdateInformation() { - $statusCode = 200; - $rawData = ""; - if ($this->yellow->extension->isExisting("update")) { - list($statusCodeCurrent, $settingsCurrent) = $this->yellow->extension->get("update")->getExtensionSettings(false); - list($statusCodeLatest, $settingsLatest) = $this->yellow->extension->get("update")->getExtensionSettings(true); - $statusCode = max($statusCodeCurrent, $statusCodeLatest); - foreach ($settingsCurrent as $key=>$value) { - if ($settingsLatest->isExisting($key)) { - $versionCurrent = $settingsCurrent[$key]->get("version"); - $versionLatest = $settingsLatest[$key]->get("version"); - if (strnatcasecmp($versionCurrent, $versionLatest)<0) { - $rawData .= htmlspecialchars(ucfirst($key)." $versionLatest")."<br />"; - } - } - } - if (!is_string_empty($rawData)) $rawData = "<p>$rawData</p>\n"; - } - return array($statusCode, $rawData); - } - - // Return raw data for generated page - public function getRawDataGenerated($page) { - $title = $page->get("title"); - $text = $this->yellow->language->getText("editDataGenerated", $page->get("language")); - return "---\nTitle: $title\n---\n$text"; - } - - // Return raw data for new page - public function getRawDataNew($page, $customTitle = false) { - $fileName = ""; - 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)."shared/"; - $fileName = $this->yellow->lookup->findFileFromContentLocation($location, true).$this->yellow->system->get("editNewFile"); - $fileName = str_replace("(.*)", $name, $fileName); - if (is_file($fileName)) break; - } - } - if (!is_file($fileName)) { - $name = $this->yellow->lookup->normaliseName($this->yellow->system->get("layout")); - $location = $this->yellow->content->getHomeLocation($page->location)."shared/"; - $fileName = $this->yellow->lookup->findFileFromContentLocation($location, true).$this->yellow->system->get("editNewFile"); - $fileName = str_replace("(.*)", $name, $fileName); - } - if (is_file($fileName)) { - $rawData = $this->yellow->toolbox->readFile($fileName); - $rawData = preg_replace("/@timestamp/i", time(), $rawData); - $rawData = preg_replace("/@datetime/i", date("Y-m-d H:i:s"), $rawData); - $rawData = preg_replace("/@date/i", date("Y-m-d"), $rawData); - $rawData = preg_replace("/@usershort/i", strtok($this->yellow->user->getUser("name", $this->userEmail), " "), $rawData); - $rawData = preg_replace("/@username/i", $this->yellow->user->getUser("name", $this->userEmail), $rawData); - $rawData = preg_replace("/@userlanguage/i", $this->yellow->user->getUser("language", $this->userEmail), $rawData); - } else { - $rawData = "---\nTitle: Page\n---\n"; - } - 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, $editNewLocation, $pageMatchLocation = false) { - $location = is_string_empty($editNewLocation) ? "@title" : $editNewLocation; - $location = preg_replace("/@title/i", $this->getPageNewTitle($rawData), $location); - $location = preg_replace("/@timestamp/i", $this->getPageNewData($rawData, "published", "U"), $location); - $location = preg_replace("/@date/i", $this->getPageNewData($rawData, "published", "Y-m-d"), $location); - $location = preg_replace("/@year/i", $this->getPageNewData($rawData, "published", "Y"), $location); - $location = preg_replace("/@month/i", $this->getPageNewData($rawData, "published", "m"), $location); - $location = preg_replace("/@day/i", $this->getPageNewData($rawData, "published", "d"), $location); - $location = preg_replace("/@tag/i", $this->getPageNewData($rawData, "tag"), $location); - $location = preg_replace("/@author/i", $this->getPageNewData($rawData, "author"), $location); - if (!preg_match("/^\//", $location)) { - if ($this->yellow->lookup->isFileLocation($pageLocation) || !$pageMatchLocation) { - $location = $this->yellow->lookup->getDirectoryLocation($pageLocation).$location; - } else { - $location = $this->yellow->lookup->getDirectoryLocation(rtrim($pageLocation, "/")).$location; - } - } - if (preg_match("/\d/", $location)) { - $locationNew = ""; - $tokens = explode("/", $location); - for ($i=1; $i<count($tokens); ++$i) { - $locationNew .= "/".$this->yellow->lookup->normaliseToken($tokens[$i]); - } - $location = $locationNew; - } - if ($pageMatchLocation) { - $location = rtrim($location, "/").($this->yellow->lookup->isFileLocation($pageLocation) ? "" : "/"); - } - 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 = is_string_empty($titleSlug) ? $title : $titleSlug; - $value = $this->yellow->lookup->normaliseName($value, false, false, true); - return trim(preg_replace("/-+/", "-", $value), "-"); - } - - // Return data for new/modified page - public function getPageNewData($rawData, $key, $dateFormat = "") { - $value = $this->yellow->toolbox->getMetaData($rawData, $key); - if (preg_match("/^(.*?)\,(.*)$/", $value, $matches)) $value = $matches[1]; - if (!is_string_empty($dateFormat)) $value = date($dateFormat, strtotime($value)); - if (is_string_empty($value)) $value = "none"; - $value = $this->yellow->lookup->normaliseName($value, false, false, true); - return trim(preg_replace("/-+/", "-", $value), "-"); - } - - // Return file name for new/modified page - public function getPageNewFile($location, $pageFileName = "", $pagePrefix = "") { - $fileName = $this->yellow->lookup->findFileFromContentLocation($location); - if (!is_string_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 = is_string_empty($pageFileName) ? $fileName : $pageFileName; - } - $prefix = $this->getPageNewPrefix($location, $pageFileName, $pagePrefix); - if ($this->yellow->lookup->isFileLocation($location)) { - if (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("coreContentExtension"); - } - } else { - if (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("coreContentDefaultFile"); - } - } - } - return $fileName; - } - - // Return prefix for new/modified page - public function getPageNewPrefix($location, $pageFileName, $pagePrefix) { - if (is_string_empty($pagePrefix)) { - if ($this->yellow->lookup->isFileLocation($location)) { - if (preg_match("#^(.*)\/(.+?)$#", $pageFileName, $matches)) $pagePrefix = $matches[2]; - } else { - if (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 = is_string_empty($fileNewLocation) ? $this->yellow->system->get("editUploadNewLocation") : $fileNewLocation; - $location = preg_replace("/@timestamp/i", time(), $location); - $location = preg_replace("/@date/i", date("Y-m-d"), $location); - $location = preg_replace("/@type/i", $this->yellow->toolbox->getFileType($fileNameShort), $location); - $location = preg_replace("/@group/i", $this->getFileNewGroup($fileNameShort), $location); - $location = preg_replace("/@folder/i", $this->getFileNewFolder($pageLocation), $location); - $location = preg_replace("/@filename/i", strtoloweru($fileNameShort), $location); - if (!preg_match("/^\//", $location)) { - $location = $this->yellow->system->get("coreMediaLocation").$location; - } - return $location; - } - - // Return group for new file - public function getFileNewGroup($fileNameShort) { - $group = "none"; - $fileType = $this->yellow->toolbox->getFileType($fileNameShort); - $locationMedia = $this->yellow->system->get("coreMediaLocation"); - $locationGroup = $this->yellow->system->get("coreDownloadLocation"); - if (preg_match("/(gif|jpg|png|svg)$/", $fileType)) { - $locationGroup = $this->yellow->system->get("coreImageLocation"); - } - if (preg_match("#^$locationMedia(.+?)\/#", $locationGroup, $matches)) { - $group = strtoloweru($matches[1]); - } - return $group; - } - - // 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) { - $fileText = $fileNumber = $fileExtension = ""; - if (preg_match("/^(.*?)(\d*)(\..*?)?$/", $fileNameShort, $matches)) { - $fileText = $matches[1]; - $fileNumber = is_string_empty($matches[2]) ? "-2" : $matches[2]+1; - $fileExtension = $matches[3]; - } - return $fileText.$fileNumber.$fileExtension; - } - - // Return next title - public function getTitleNext($rawData) { - $titleText = $titleNumber = ""; - if (preg_match("/^(.*?)(\d*)$/", $this->yellow->toolbox->getMetaData($rawData, "title"), $matches)) { - $titleText = $matches[1]; - $titleNumber = is_string_empty($matches[2]) ? " 2" : $matches[2]+1; - } - return $titleText.$titleNumber; - } - - // Send mail to user - public function sendMail($scheme, $address, $base, $email, $action) { - if ($action=="approve") { - $userName = $this->yellow->system->get("author"); - $userEmail = $this->yellow->system->get("email"); - $userLanguage = $this->extension->getUserLanguage($userEmail); - } else { - $userName = $this->yellow->user->getUser("name", $email); - $userEmail = $email; - $userLanguage = $this->extension->getUserLanguage($email); - } - if ($action=="welcome" || $action=="goodbye") { - $url = "$scheme://$address$base/"; - } else { - $expire = time() + 60*60*24; - $actionToken = $this->createActionToken($email, $action, $expire); - $locationArguments = "/action:$action/email:$email/expire:$expire/language:$userLanguage/actiontoken:$actionToken/"; - $url = "$scheme://$address$base".$this->yellow->lookup->normaliseArguments($locationArguments, false, false); - } - $prefix = "edit".ucfirst($action); - $message = $this->yellow->language->getText("{$prefix}Message", $userLanguage); - $message = str_replace("\\n", "\r\n", $message); - $message = preg_replace("/@useraccount/i", $email, $message); - $message = preg_replace("/@usershort/i", strtok($userName, " "), $message); - $message = preg_replace("/@username/i", $userName, $message); - $message = preg_replace("/@userlanguage/i", $userLanguage, $message); - $sitename = $this->yellow->system->get("sitename"); - $siteEmail = $this->yellow->system->get("editSiteEmail"); - $subject = $this->yellow->language->getText("{$prefix}Subject", $userLanguage); - $footer = $this->yellow->language->getText("editMailFooter", $userLanguage); - $footer = str_replace("\\n", "\r\n", $footer); - $footer = preg_replace("/@sitename/i", $sitename, $footer); - $mailHeaders = array( - "To" => $this->yellow->lookup->normaliseAddress("$userName <$userEmail>"), - "From" => $this->yellow->lookup->normaliseAddress("$sitename <$siteEmail>"), - "Subject" => $subject, - "Date" => date(DATE_RFC2822), - "Mime-Version" => "1.0", - "Content-Type" => "text/plain; charset=utf-8", - "X-Request-Url" => "$scheme://$address$base"); - $mailMessage = "$message\r\n\r\n$url\r\n-- \r\n$footer"; - return $this->yellow->toolbox->mail($action, $mailHeaders, $mailMessage); - } - - // Create browser cookies - public function createCookies($scheme, $address, $base, $email) { - $expire = time() + $this->yellow->system->get("editLoginSessionTimeout"); - $authToken = $this->createAuthToken($email, $expire); - $csrfToken = $this->createCsrfToken(); - setcookie("authtoken", $authToken, $expire, "$base/", "", $scheme=="https", true); - setcookie("csrftoken", $csrfToken, $expire, "$base/", "", $scheme=="https", false); - } - - // Destroy browser cookies - public function destroyCookies($scheme, $address, $base) { - setcookie("authtoken", "", 1, "$base/"); - setcookie("csrftoken", "", 1, "$base/"); - } - - // Create authentication token - public function createAuthToken($email, $expire) { - $hash = $this->yellow->user->getUser("hash", $email); - $signature = $this->yellow->toolbox->createHash($hash."auth".$expire, "sha256"); - if (is_string_empty($signature)) $signature = "padd"."error-hash-algorithm-sha256"; - return substrb($signature, 4).$this->yellow->user->getUser("stamp", $email).dechex($expire); - } - - // Create action token - public function createActionToken($email, $action, $expire) { - $hash = $this->yellow->user->getUser("hash", $email); - $signature = $this->yellow->toolbox->createHash($hash.$action.$expire, "sha256"); - if (is_string_empty($signature)) $signature = "padd"."error-hash-algorithm-sha256"; - return substrb($signature, 4); - } - - // 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 (is_string_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; - } - - // Check user authentication from email and password - public function checkAuthLogin($email, $password) { - $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); - $hash = $this->yellow->user->getUser("hash", $email); - return $this->yellow->user->getUser("status", $email)=="active" && - $this->yellow->toolbox->verifyHash($password, $algorithm, $hash); - } - - // Check user authentication from tokens - public function checkAuthToken($authToken, $csrfTokenExpected, $csrfTokenReceived, $csrfTokenIrrelevant) { - $signature = "$5y$".substrb($authToken, 0, 96); - $email = $this->getAuthEmail($authToken); - $expire = $this->getAuthExpire($authToken); - $hash = $this->yellow->user->getUser("hash", $email); - return $expire>time() && $this->yellow->user->getUser("status", $email)=="active" && - $this->yellow->toolbox->verifyHash($hash."auth".$expire, "sha256", $signature) && - ($this->yellow->toolbox->verifyToken($csrfTokenExpected, $csrfTokenReceived) || $csrfTokenIrrelevant); - } - - // Check action token - public function checkActionToken($actionToken, $email, $action, $expire) { - $signature = "$5y$".$actionToken; - $hash = $this->yellow->user->getUser("hash", $email); - return $expire>time() && $this->yellow->user->isExisting($email) && - $this->yellow->toolbox->verifyHash($hash.$action.$expire, "sha256", $signature); - } - - // Return user email from authentication, timing attack safe email lookup - public function getAuthEmail($authToken, $stamp = "") { - $email = ""; - if (is_string_empty($stamp)) $stamp = substrb($authToken, 96, 20); - foreach ($this->yellow->user->settings as $key=>$value) { - if ($this->yellow->toolbox->verifyToken($value["stamp"], $stamp)) $email = $key; - } - return $email; - } - - // Return expiration time from authentication - public function getAuthExpire($authToken) { - return hexdec(substrb($authToken, 96+20)); - } - - // Change content file - public function editContentFile($page, $action, $email) { - if (!$page->isError()) { - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onEditContentFile")) $value["object"]->onEditContentFile($page, $action, $email); - } - } - } - - // Change media file - public function editMediaFile($file, $action, $email) { - if (!$file->isError()) { - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onEditMediaFile")) $value["object"]->onEditMediaFile($file, $action, $email); - } - } - } - - // Change system file - public function editSystemFile($file, $action, $email) { - if (!$file->isError()) { - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onEditSystemFile")) $value["object"]->onEditSystemFile($file, $action, $email); - } - } - } - - // Delete file - public function deleteFileLocation($location, $fileName) { - $rawData = $this->yellow->toolbox->readFile($fileName); - $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalLocation", $location); - $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalFileName", $fileName); - return $this->yellow->toolbox->createFile($fileName, $rawData) && - $this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory")); - } - - // Delete directory - public function deleteDirectoryLocation($location, $fileName) { - $rawData = $this->yellow->toolbox->readFile($fileName); - $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalLocation", $location); - $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalFileName", $fileName); - return $this->yellow->toolbox->createFile($fileName, $rawData) && - $this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->system->get("coreTrashDirectory")); - } - - // Restore deleted file from trash - public function restoreFileLocation($location) { - $fileNameDeleted = $fileNameRestored = ""; - $deleted = 0; - $pathTrash = $this->yellow->system->get("coreTrashDirectory"); - $regex = "/^.*\\".$this->yellow->system->get("coreContentExtension")."$/"; - foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, $regex, false, false) as $entry) { - $rawDataOriginal = $this->yellow->toolbox->readFile($entry); - $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation"); - $fileNameOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalFileName"); - $deletedOriginal = $this->yellow->toolbox->getFileDeleted($entry); - if ($location==$locationOriginal && $deleted<=$deletedOriginal) { - $fileNameDeleted = $entry; - $fileNameRestored = $fileNameOriginal; - $rawDataRestored = $rawDataOriginal; - $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalLocation"); - $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalFileName"); - $deleted = $deletedOriginal; - } - } - return !is_string_empty($fileNameDeleted) && $this->yellow->lookup->isContentFile($fileNameRestored) && - $this->yellow->toolbox->renameFile($fileNameDeleted, $fileNameRestored, true) && - $this->yellow->toolbox->createFile($fileNameRestored, $rawDataRestored); - } - - // Restore deleted directory from trash - public function restoreDirectoryLocation($location) { - $pathDeleted = $fileNameRestored = ""; - $deleted = 0; - $pathTrash = $this->yellow->system->get("coreTrashDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, "/.*/", false, true) as $entry) { - $fileName = $entry."/".$this->yellow->system->get("coreContentDefaultFile"); - if (!is_file($fileName)) continue; - $rawDataOriginal = $this->yellow->toolbox->readFile($fileName); - $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation"); - $fileNameOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalFileName"); - $deletedOriginal = $this->yellow->toolbox->getFileDeleted($entry); - if ($location==$locationOriginal && $deleted<=$deletedOriginal) { - $pathDeleted = $entry; - $fileNameRestored = $fileNameOriginal; - $rawDataRestored = $rawDataOriginal; - $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalLocation"); - $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalFileName"); - $deleted = $deletedOriginal; - } - } - return !is_string_empty($pathDeleted) && $this->yellow->lookup->isContentFile($fileNameRestored) && - $this->yellow->toolbox->renameDirectory($pathDeleted, dirname($fileNameRestored), true) && - $this->yellow->toolbox->createFile($fileNameRestored, $rawDataRestored); - } - - // Check if location has been deleted - public function isDeletedLocation($location) { - $found = false; - $pathTrash = $this->yellow->system->get("coreTrashDirectory"); - $regex = "/^.*\\".$this->yellow->system->get("coreContentExtension")."$/"; - $fileNames = $this->yellow->toolbox->getDirectoryEntries($pathTrash, $regex, false, false); - foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, "/.*/", false, true) as $entry) { - $fileName = $entry."/".$this->yellow->system->get("coreContentDefaultFile"); - if (is_file($fileName)) array_push($fileNames, $fileName); - } - foreach ($fileNames as $fileName) { - $rawDataOriginal = $this->yellow->toolbox->readFile($fileName, 4096); - $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation"); - if ($location==$locationOriginal) { - $found = true; - break; - } - } - return $found; - } - - // Check if meta data has been modified - public function isMetaModified($pageSource, $pageOther) { - return substrb($pageSource->rawData, 0, $pageSource->metaDataOffsetBytes) != - substrb($pageOther->rawData, 0, $pageOther->metaDataOffsetBytes); - } - - // Check if login with restriction - public function isLoginRestriction() { - return $this->yellow->system->get("editLoginRestriction"); - } - - // Check if user is logged in - public function isUser() { - return !is_string_empty($this->userEmail); - } - - // Check if user with access - public function isUserAccess($action, $location = "") { - $userHome = $this->yellow->user->getUser("home", $this->userEmail); - $tokens = preg_split("/\s*,\s*/", $this->yellow->user->getUser("access", $this->userEmail)); - return in_array($action, $tokens) && (is_string_empty($location) || substru($location, 0, strlenu($userHome))==$userHome); - } -} - -class YellowEditMerge { - public $yellow; // access to API - const ADD = "+"; // merge types - 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(YellowEditMerge::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(YellowEditMerge::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]!=YellowEditMerge::REMOVE) { - array_push($diff, array(YellowEditMerge::ADD, $textOther[$textStart+$x], false)); - $lastRemove = -1; - } else { - $diff[$lastRemove] = array(YellowEditMerge::MODIFY, $textOther[$textStart+$x], false); - ++$lastRemove; - if (count($diff)==$lastRemove) $lastRemove = -1; - } - ++$x; - continue; - } - array_push($diff, array(YellowEditMerge::SAME, $textSource[$textStart+$y], false)); - $lastRemove = -1; - ++$x; - ++$y; - } - for ($pos=$sourceEnd;$pos<$sourceSize; ++$pos) { - array_push($diff, array(YellowEditMerge::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==YellowEditMerge::SAME) { - array_push($diff, $diffYours[$posYours]); - } elseif ($typeYours==YellowEditMerge::SAME) { - array_push($diff, $diffMine[$posMine]); - } elseif ($typeMine==YellowEditMerge::ADD && $typeYours==YellowEditMerge::ADD) { - $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false); - } elseif ($typeMine==YellowEditMerge::MODIFY && $typeYours==YellowEditMerge::MODIFY) { - $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false); - } elseif ($typeMine==YellowEditMerge::REMOVE && $typeYours==YellowEditMerge::REMOVE) { - array_push($diff, $diffMine[$posMine]); - } elseif ($typeMine==YellowEditMerge::ADD) { - array_push($diff, $diffMine[$posMine]); - } elseif ($typeYours==YellowEditMerge::ADD) { - array_push($diff, $diffYours[$posYours]); - } else { - $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], true); - } - if ($typeMine==YellowEditMerge::ADD || $typeYours==YellowEditMerge::ADD) { - if ($typeMine==YellowEditMerge::ADD) ++$posMine; - if ($typeYours==YellowEditMerge::ADD) ++$posYours; - } else { - ++$posMine; - ++$posYours; - } - } - for (;$posMine<count($diffMine); ++$posMine) { - array_push($diff, $diffMine[$posMine]); - $typeMine = $diffMine[$posMine][0]; - $typeYours = " "; - } - for (;$posYours<count($diffYours); ++$posYours) { - array_push($diff, $diffYours[$posYours]); - $typeYours = $diffYours[$posYours][0]; - $typeMine = " "; - } - 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 = ""; - $conflict = false; - if (!$showDiff) { - for ($i=0; $i<count($diff); ++$i) { - if ($diff[$i][0]!=YellowEditMerge::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/extensions/generate.php b/system/extensions/generate.php @@ -1,438 +0,0 @@ -<?php -// Generate extension, https://github.com/annaesvensson/yellow-generate - -class YellowGenerate { - const VERSION = "0.8.54"; - public $yellow; // access to API - public $files; // number of files - public $errors; // number of errors - public $locationsArguments; // locations with location arguments detected - public $locationsArgumentsPagination; // locations with pagination arguments detected - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - $this->yellow->system->setDefault("generateStaticUrl", "auto"); - $this->yellow->system->setDefault("generateStaticDirectory", "public/"); - $this->yellow->system->setDefault("generateStaticDefaultFile", "index.html"); - $this->yellow->system->setDefault("generateStaticErrorFile", "404.html"); - } - - // Handle request - public function onRequest($scheme, $address, $base, $location, $fileName) { - return $this->processRequestCache($scheme, $address, $base, $location, $fileName); - } - - // Handle command - public function onCommand($command, $text) { - switch ($command) { - case "generate": $statusCode = $this->processCommandGenerate($command, $text); break; - case "clean": $statusCode = $this->processCommandClean($command, $text); break; - default: $statusCode = 0; - } - return $statusCode; - } - - // Handle command help - public function onCommandHelp() { - return array("generate [directory location]", "clean [directory location]"); - } - - // Process command to generate static website - public function processCommandGenerate($command, $text) { - $statusCode = 0; - list($path, $location) = $this->yellow->toolbox->getTextArguments($text); - if (is_string_empty($location) || substru($location, 0, 1)=="/") { - if ($this->checkStaticSettings()) { - $statusCode = $this->generateStatic($path, $location); - } else { - $statusCode = 500; - $this->files = 0; - $this->errors = 1; - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - echo "ERROR generating files: Please configure GenerateStaticUrl in file '$fileName'!\n"; - } - echo "Yellow $command: $this->files file".($this->files!=1 ? "s" : ""); - echo ", $this->errors error".($this->errors!=1 ? "s" : "")."\n"; - } else { - $statusCode = 400; - echo "Yellow $command: Invalid arguments\n"; - } - return $statusCode; - } - - // Generate static website - public function generateStatic($path, $location) { - $statusCode = 200; - $this->files = $this->errors = 0; - $path = rtrim(is_string_empty($path) ? $this->yellow->system->get("generateStaticDirectory") : $path, "/"); - if (is_string_empty($location)) { - $statusCode = $this->cleanStatic($path, $location); - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("clean"); - } - } - $statusCode = max($statusCode, $this->generateStaticContent($path, $location, "\rGenerating static website", 5, 95)); - $statusCode = max($statusCode, $this->generateStaticMedia($path, $location)); - echo "\rGenerating static website 100%... done\n"; - return $statusCode; - } - - // Generate static content - public function generateStaticContent($path, $locationFilter, $progressText, $increments, $max) { - $statusCode = 200; - $this->locationsArguments = $this->locationsArgumentsPagination = array(); - $staticUrl = $this->yellow->system->get("generateStaticUrl"); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - $locations = $this->getContentLocations(); - $filesEstimated = count($locations); - foreach ($locations as $location) { - echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max/1.5)."%... "; - if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; - $statusCode = max($statusCode, $this->generateStaticFile($path, $location, true)); - } - foreach ($this->locationsArguments as $location) { - echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max/1.5)."%... "; - if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; - $statusCode = max($statusCode, $this->generateStaticFile($path, $location, true)); - } - $filesEstimated = $this->files + count($this->locationsArguments) + count($this->locationsArgumentsPagination); - foreach ($this->locationsArgumentsPagination as $location) { - echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max)."%... "; - if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; - if (substru($location, -1)!=$this->yellow->toolbox->getLocationArgumentsSeparator()) { - $statusCode = max($statusCode, $this->generateStaticFile($path, $location, false, true)); - } - for ($pageNumber=2; $pageNumber<=999; ++$pageNumber) { - $statusCodeLocation = $this->generateStaticFile($path, $location.$pageNumber, false, true); - $statusCode = max($statusCode, $statusCodeLocation); - if ($statusCodeLocation==100) break; - } - } - echo "$progressText ".$this->getProgressPercent(100, 100, $increments, $max)."%... "; - return $statusCode; - } - - // Generate static media - public function generateStaticMedia($path, $locationFilter) { - $statusCode = 200; - if (is_string_empty($locationFilter)) { - foreach ($this->getMediaLocations() as $location) { - $statusCode = max($statusCode, $this->generateStaticFile($path, $location)); - } - foreach ($this->getExtraLocations($path) as $location) { - $statusCode = max($statusCode, $this->generateStaticFile($path, $location)); - } - $statusCode = max($statusCode, $this->generateStaticFile($path, "/error/", false, false, true)); - } - return $statusCode; - } - - // Generate static file - public function generateStaticFile($path, $location, $analyse = false, $probe = false, $error = false) { - $this->yellow->content->pages = array(); - $this->yellow->page = new YellowPage($this->yellow); - $this->yellow->page->fileName = substru($location, 1); - if (!is_readable($this->yellow->page->fileName)) { - ob_start(); - $staticUrl = $this->yellow->system->get("generateStaticUrl"); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - $statusCode = $this->requestStaticFile($scheme, $address, $base, $location); - if ($statusCode<400 || $error) { - $fileData = ob_get_contents(); - $statusCode = $this->saveStaticFile($path, $location, $fileData, $statusCode); - } - ob_end_clean(); - } else { - $statusCode = $this->copyStaticFile($path, $location); - } - if ($statusCode==200 && $analyse) $this->analyseLocations($scheme, $address, $base, $fileData); - if ($statusCode==404 && $probe) $statusCode = 100; - if ($statusCode==404 && $error) $statusCode = 200; - if ($statusCode>=200) ++$this->files; - if ($statusCode>=400) { - ++$this->errors; - echo "\rERROR generating location '$location', ".$this->yellow->page->getStatusCode(true)."\n"; - } - if ($this->yellow->system->get("coreDebugMode")>=1) { - echo "YellowGenerate::generateStaticFile status:$statusCode location:$location<br/>\n"; - } - return $statusCode; - } - - // Request static file - public function requestStaticFile($scheme, $address, $base, $location) { - list($serverName, $serverPort) = $this->yellow->toolbox->getTextList($address, ":", 2); - if (is_string_empty($serverPort)) $serverPort = $scheme=="https" ? 443 : 80; - $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1"; - $_SERVER["SERVER_NAME"] = $serverName; - $_SERVER["SERVER_PORT"] = $serverPort; - $_SERVER["REQUEST_METHOD"] = "GET"; - $_SERVER["REQUEST_SCHEME"] = $scheme; - $_SERVER["REQUEST_URI"] = $base.$location; - $_SERVER["SCRIPT_NAME"] = $base."/yellow.php"; - $_SERVER["REMOTE_ADDR"] = "127.0.0.1"; - $_REQUEST = array(); - return $this->yellow->request(); - } - - // Save static file - public function saveStaticFile($path, $location, $fileData, $statusCode) { - $modified = strtotime($this->yellow->page->getHeader("Last-Modified")); - if ($modified==0) $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName); - if ($statusCode>=301 && $statusCode<=303) { - $fileData = $this->getStaticRedirect($this->yellow->page->getHeader("Location")); - $modified = time(); - } - $fileName = $this->getStaticFile($path, $location, $statusCode); - if (is_file($fileName)) $this->yellow->toolbox->deleteFile($fileName); - if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || - !$this->yellow->toolbox->modifyFile($fileName, $modified)) { - $statusCode = 500; - $this->yellow->page->statusCode = $statusCode; - $this->yellow->page->errorMessage = "Can't write file '$fileName'!"; - } - return $statusCode; - } - - // Copy static file - public function copyStaticFile($path, $location) { - $statusCode = 200; - $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName); - $fileName = $this->getStaticFile($path, $location, $statusCode); - if (is_file($fileName)) $this->yellow->toolbox->deleteFile($fileName); - if (!$this->yellow->toolbox->copyFile($this->yellow->page->fileName, $fileName, true) || - !$this->yellow->toolbox->modifyFile($fileName, $modified)) { - $statusCode = 500; - $this->yellow->page->statusCode = $statusCode; - $this->yellow->page->errorMessage = "Can't write file '$fileName'!"; - } - return $statusCode; - } - - // Analyse locations with arguments - public function analyseLocations($scheme, $address, $base, $rawData) { - preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $rawData, $matches); - foreach ($matches[2] as $match) { - $location = rawurldecode($match); - if (preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1]; - if (preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $tokens)) { - if ($tokens[1]!=$scheme) continue; - if ($tokens[2]!=$address) continue; - $location = $tokens[3]; - } - if (substru($location, 0, strlenu($base))!=$base) continue; - if (substru($location, strlenu($base), 1)!="/") continue; - $location = substru($location, strlenu($base)); - if (!$this->yellow->toolbox->isLocationArguments($location)) continue; - if (!$this->yellow->toolbox->isLocationArgumentsPagination($location)) { - $location = rtrim($location, "/")."/"; - if (!isset($this->locationsArguments[$location])) { - $this->locationsArguments[$location] = $location; - if ($this->yellow->system->get("coreDebugMode")>=2) { - echo "YellowGenerate::analyseLocations detected location:$location<br/>\n"; - } - } - } else { - $location = rtrim($location, "0..9"); - if (!isset($this->locationsArgumentsPagination[$location])) { - $this->locationsArgumentsPagination[$location] = $location; - if ($this->yellow->system->get("coreDebugMode")>=2) { - echo "YellowGenerate::analyseLocations detected location:$location<br/>\n"; - } - } - } - } - } - - // Process command to clean static website - public function processCommandClean($command, $text) { - $statusCode = 0; - list($path, $location) = $this->yellow->toolbox->getTextArguments($text); - if (is_string_empty($location) || substru($location, 0, 1)=="/") { - $statusCode = $this->cleanStatic($path, $location); - echo "Yellow $command: Static website"; - echo " ".($statusCode!=200 ? "not " : "")."cleaned\n"; - } else { - $statusCode = 400; - echo "Yellow $command: Invalid arguments\n"; - } - return $statusCode; - } - - // Clean static website - public function cleanStatic($path, $location) { - $statusCode = 200; - $path = rtrim(is_string_empty($path) ? $this->yellow->system->get("generateStaticDirectory") : $path, "/"); - if (is_string_empty($location)) { - $statusCode = max($statusCode, $this->cleanStaticDirectory($path)); - } else { - if ($this->yellow->lookup->isFileLocation($location)) { - $fileName = $this->getStaticFile($path, $location, $statusCode); - $statusCode = $this->cleanStaticFile($fileName); - } else { - $statusCode = $this->cleanStaticDirectory($path.$location); - } - } - return $statusCode; - } - - // Clean static directory - public function cleanStaticDirectory($path) { - $statusCode = 200; - if (is_dir($path) && $this->checkStaticDirectory($path)) { - if (!$this->yellow->toolbox->deleteDirectory($path)) { - $statusCode = 500; - echo "ERROR cleaning files: Can't delete directory '$path'!\n"; - } - } - return $statusCode; - } - - // Clean static file - public function cleanStaticFile($fileName) { - $statusCode = 200; - if (is_file($fileName)) { - if (!$this->yellow->toolbox->deleteFile($fileName)) { - $statusCode = 500; - echo "ERROR cleaning files: Can't delete file '$fileName'!\n"; - } - } - return $statusCode; - } - - // Process request for cached files - public function processRequestCache($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if (is_dir($this->yellow->system->get("coreCacheDirectory"))) { - $location .= $this->yellow->toolbox->getLocationArguments(); - $fileName = rtrim($this->yellow->system->get("coreCacheDirectory"), "/").$location; - if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->system->get("generateStaticDefaultFile"); - if (is_file($fileName) && is_readable($fileName) && !$this->yellow->lookup->isCommandLine()) { - $statusCode = $this->yellow->sendFile(200, $fileName, true); - } - } - return $statusCode; - } - - // Check static settings - public function checkStaticSettings() { - return preg_match("/^(http|https):/", $this->yellow->system->get("generateStaticUrl")); - } - - // Check static directory - public function checkStaticDirectory($path) { - $ok = false; - if (!is_string_empty($path)) { - if ($path==rtrim($this->yellow->system->get("generateStaticDirectory"), "/")) $ok = true; - if ($path==rtrim($this->yellow->system->get("coreCacheDirectory"), "/")) $ok = true; - if ($path==rtrim($this->yellow->system->get("coreTrashDirectory"), "/")) $ok = true; - if (is_file("$path/".$this->yellow->system->get("generateStaticDefaultFile"))) $ok = true; - if (is_file("$path/yellow.php")) $ok = false; - } - return $ok; - } - - // Return progress in percent - public function getProgressPercent($now, $total, $increments, $max) { - $max = intval($max/$increments) * $increments; - $percent = intval(($max/$total) * $now); - if ($increments>1) $percent = intval($percent/$increments) * $increments; - return min($max, $percent); - } - - // Return static file - public function getStaticFile($path, $location, $statusCode) { - if ($statusCode<400) { - $fileName = $path.$location; - if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->system->get("generateStaticDefaultFile"); - } elseif ($statusCode==404) { - $fileName = $path."/".$this->yellow->system->get("generateStaticErrorFile"); - } else { - $fileName = $path."/error.html"; - } - return $fileName; - } - - // Return static redirect - public function getStaticRedirect($location) { - $output = "<!DOCTYPE html><html>\n<head>\n"; - $output .= "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n"; - $output .= "<meta http-equiv=\"refresh\" content=\"0;url=".htmlspecialchars($location)."\" />\n"; - $output .= "</head>\n</html>"; - return $output; - } - - // Return content locations - public function getContentLocations($includeAll = false) { - $locations = array(); - $staticUrl = $this->yellow->system->get("generateStaticUrl"); - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); - $this->yellow->page->setRequestInformation($scheme, $address, $base, "", "", false); - foreach ($this->yellow->content->index(true, true) as $page) { - if (preg_match("/exclude/i", $page->get("generate")) && !$includeAll) continue; - if ($page->get("status")=="private" || $page->get("status")=="draft") continue; - array_push($locations, $page->location); - } - if (!$this->yellow->content->find("/") && $this->yellow->system->get("coreMultiLanguageMode")) array_unshift($locations, "/"); - return $locations; - } - - // Return media locations - public function getMediaLocations() { - $locations = array(); - $mediaPath = $this->yellow->system->get("coreMediaDirectory"); - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($mediaPath, "/.*/", false, false); - foreach ($fileNames as $fileName) { - array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName)); - } - $extensionPath = $this->yellow->system->get("coreExtensionDirectory"); - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($extensionPath, "/.*/", false, false); - foreach ($fileNames as $fileName) { - array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName)); - } - $themePath = $this->yellow->system->get("coreThemeDirectory"); - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($themePath, "/.*/", false, false); - foreach ($fileNames as $fileName) { - array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName)); - } - return array_diff($locations, $this->getMediaLocationsIgnore()); - } - - // Return media locations to ignore - public function getMediaLocationsIgnore() { - $locations = array(""); - $extensionPath = $this->yellow->system->get("coreExtensionDirectory"); - $extensionDirectoryLength = strlenu($this->yellow->system->get("coreExtensionDirectory")); - if ($this->yellow->extension->isExisting("bundle")) { - foreach ($this->yellow->toolbox->getDirectoryEntries($extensionPath, "/^bundle-(.*)/", false, false) as $entry) { - list($locationsBundle) = $this->yellow->extension->get("bundle")->getBundleInformation($entry); - $locations = array_merge($locations, $locationsBundle); - } - } - if ($this->yellow->extension->isExisting("edit")) { - foreach ($this->yellow->toolbox->getDirectoryEntries($extensionPath, "/^edit\.(.*)/", false, false) as $entry) { - $location = $this->yellow->system->get("coreExtensionLocation").substru($entry, $extensionDirectoryLength); - array_push($locations, $location); - } - } - return array_unique($locations); - } - - // Return extra locations - public function getExtraLocations($path) { - $locations = array(); - $pathIgnore = "($path/|". - $this->yellow->system->get("generateStaticDirectory")."|". - $this->yellow->system->get("coreContentDirectory")."|". - $this->yellow->system->get("coreMediaDirectory")."|". - $this->yellow->system->get("coreSystemDirectory").")"; - $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive(".", "/.*/", false, false); - foreach ($fileNames as $fileName) { - $fileName = substru($fileName, 2); - if (preg_match("#^$pathIgnore#", $fileName) || $fileName=="yellow.php") continue; - array_push($locations, "/".$fileName); - } - return $locations; - } -} diff --git a/system/extensions/image.php b/system/extensions/image.php @@ -1,199 +0,0 @@ -<?php -// Image extension, https://github.com/annaesvensson/yellow-image - -class YellowImage { - const VERSION = "0.8.20"; - public $yellow; // access to API - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - $this->yellow->system->setDefault("imageUploadWidthMax", "1280"); - $this->yellow->system->setDefault("imageUploadHeightMax", "1280"); - $this->yellow->system->setDefault("imageUploadJpgQuality", "80"); - $this->yellow->system->setDefault("imageThumbnailJpgQuality", "80"); - } - - // Handle update - public function onUpdate($action) { - if ($action=="clean") { - $statusCode = 200; - $path = $this->yellow->lookup->findMediaDirectory("coreThumbnailLocation"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) { - if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; - } - if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!"); - } - } - - // Handle page content element - public function onParseContentElement($page, $name, $text, $attributes, $type) { - $output = null; - if ($name=="image" && $type=="inline") { - list($name, $alt, $style, $width, $height) = $this->yellow->toolbox->getTextArguments($text); - if (!preg_match("/^\w+:/", $name)) { - if (is_string_empty($alt)) $alt = $this->yellow->language->getText("imageDefaultAlt"); - if (is_string_empty($width)) $width = "100%"; - if (is_string_empty($height)) $height = $width; - $path = $this->yellow->lookup->findMediaDirectory("coreImageLocation"); - list($src, $width, $height) = $this->getImageInformation($path.$name, $width, $height); - } else { - if (is_string_empty($alt)) $alt = $this->yellow->language->getText("imageDefaultAlt"); - $src = $this->yellow->lookup->normaliseUrl("", "", "", $name); - $width = $height = 0; - } - $output = "<img src=\"".htmlspecialchars($src)."\""; - if ($width && $height) $output .= " width=\"".htmlspecialchars($width)."\" height=\"".htmlspecialchars($height)."\""; - if (!is_string_empty($alt)) $output .= " alt=\"".htmlspecialchars($alt)."\" title=\"".htmlspecialchars($alt)."\""; - if (!is_string_empty($style)) $output .= " class=\"".htmlspecialchars($style)."\""; - $output .= " />"; - } - return $output; - } - - // Handle media file changes - public function onEditMediaFile($file, $action, $email) { - if ($action=="upload") { - $fileName = $file->fileName; - list($widthInput, $heightInput, $orientation, $type) = - $this->yellow->toolbox->detectImageInformation($fileName, $file->get("type")); - $widthMax = $this->yellow->system->get("imageUploadWidthMax"); - $heightMax = $this->yellow->system->get("imageUploadHeightMax"); - if ($type=="gif" || $type=="jpg" || $type=="png") { - if ($widthInput>$widthMax || $heightInput>$heightMax) { - list($widthOutput, $heightOutput) = $this->getImageDimensionsFit($widthInput, $heightInput, $widthMax, $heightMax); - $image = $this->loadImage($fileName, $type); - $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); - $image = $this->orientImage($image, $orientation); - if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) { - $file->error(500, "Can't write file '$fileName'!"); - } - } elseif ($orientation>1) { - $image = $this->loadImage($fileName, $type); - $image = $this->orientImage($image, $orientation); - if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) { - $file->error(500, "Can't write file '$fileName'!"); - } - } - } - } - } - - // Return image information, create thumbnail on demand - public function getImageInformation($fileName, $widthOutput, $heightOutput) { - $fileNameShort = substru($fileName, strlenu($this->yellow->lookup->findMediaDirectory("coreImageLocation"))); - list($widthInput, $heightInput, $orientation, $type) = $this->yellow->toolbox->detectImageInformation($fileName); - $widthOutput = $this->convertValueAndUnit($widthOutput, $widthInput); - $heightOutput = $this->convertValueAndUnit($heightOutput, $heightInput); - if (($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg" || $type=="") { - $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreImageLocation").$fileNameShort; - $width = $widthOutput; - $height = $heightOutput; - } else { - $pathThumb = $this->yellow->lookup->findMediaDirectory("coreThumbnailLocation"); - $fileNameThumb = ltrim(str_replace(array("/", "\\", "."), "-", dirname($fileNameShort)."/".pathinfo($fileName, PATHINFO_FILENAME)), "-"); - $fileNameThumb .= "-".$widthOutput."x".$heightOutput; - $fileNameThumb .= ".".pathinfo($fileName, PATHINFO_EXTENSION); - $fileNameOutput = $pathThumb.$fileNameThumb; - if ($this->isFileNotUpdated($fileName, $fileNameOutput)) { - $image = $this->loadImage($fileName, $type); - $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); - $image = $this->orientImage($image, $orientation); - if (is_file($fileNameOutput)) $this->yellow->toolbox->deleteFile($fileNameOutput); - if (!$this->saveImage($image, $fileNameOutput, $type, $this->yellow->system->get("imageThumbnailJpgQuality")) || - !$this->yellow->toolbox->modifyFile($fileNameOutput, $this->yellow->toolbox->getFileModified($fileName))) { - $this->yellow->page->error(500, "Can't write file '$fileNameOutput'!"); - } - } - $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreThumbnailLocation").$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; - } - - // Orient image automatically - public function orientImage($image, $orientation) { - switch ($orientation) { - case 2: imageflip($image, IMG_FLIP_HORIZONTAL); break; - case 3: $image = imagerotate($image, 180, 0); break; - case 4: imageflip($image, IMG_FLIP_VERTICAL); break; - case 5: $image = imagerotate($image, 90, 0); imageflip($image, IMG_FLIP_VERTICAL); break; - case 6: $image = imagerotate($image, -90, 0); break; - case 7: $image = imagerotate($image, 90, 0); imageflip($image, IMG_FLIP_HORIZONTAL); break; - case 8: $image = imagerotate($image, 90, 0); break; - } - return $image; - } - - // 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); - } - - // Check if file needs to be updated - public function isFileNotUpdated($fileNameInput, $fileNameOutput) { - return $this->yellow->toolbox->getFileModified($fileNameInput)!=$this->yellow->toolbox->getFileModified($fileNameOutput); - } -} diff --git a/system/extensions/install-blog.bin b/system/extensions/install-blog.bin Binary files differ. diff --git a/system/extensions/install-language.bin b/system/extensions/install-language.bin Binary files differ. diff --git a/system/extensions/install-wiki.bin b/system/extensions/install-wiki.bin Binary files differ. diff --git a/system/extensions/install.php b/system/extensions/install.php @@ -1,575 +0,0 @@ -<?php -// Install extension, https://github.com/annaesvensson/yellow-install - -class YellowInstall { - const VERSION = "0.8.95"; - 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) { - return $this->processRequestInstall($scheme, $address, $base, $location, $fileName); - } - - // Handle command - public function onCommand($command, $text) { - return $this->processCommandInstall($command, $text); - } - - // Process request to install website - public function processRequestInstall($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->yellow->lookup->isContentFile($fileName) || is_string_empty($fileName)) { - if ($this->yellow->system->get("updateCurrentRelease")=="none") { - $this->checkServerRequirements(); - $author = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $this->yellow->page->getRequest("author"))); - $email = trim($this->yellow->page->getRequest("email")); - $password = trim($this->yellow->page->getRequest("password")); - $language = trim($this->yellow->page->getRequest("language")); - $extension = trim($this->yellow->page->getRequest("extension")); - $status = trim($this->yellow->page->getRequest("status")); - $statusCode = $this->updateLog(); - $statusCode = max($statusCode, $this->updateLanguages("small")); - $errorMessage = $this->yellow->page->errorMessage; - $this->yellow->content->pages["root/"] = array(); - $this->yellow->page = new YellowPage($this->yellow); - $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); - $this->yellow->page->parseMeta($this->getRawDataInstall(), $statusCode, $errorMessage); - $this->yellow->page->parseContent(); - $this->yellow->page->parsePage(); - if ($status=="install") $status = $this->updateExtensions("small", $extension)==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateUser($email, $password, $author, $language)==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateAuthentication($scheme, $address, $base, $email)==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateContent($language, "installHome", "/")==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateContent($language, "installAbout", "/about/")==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateContent($language, "installDefault", "/shared/page-new-default")==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateContent($language, "installWiki", "/shared/page-new-wiki")==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateContent($language, "installBlog", "/shared/page-new-blog")==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateContent($language, "coreError404", "/shared/page-error-404")==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->updateSettings()==200 ? "ok" : "error"; - if ($status=="ok") $status = $this->removeInstall()==200 ? "done" : "error"; - } else { - $status = $this->removeInstall(true)==200 ? "done" : "error"; - } - if ($status=="done") { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, "/"); - $statusCode = $this->yellow->sendStatus(303, $location); - } else { - $statusCode = $this->yellow->sendData($this->yellow->page->statusCode, $this->yellow->page->headerData, $this->yellow->page->outputData); - } - } - return $statusCode; - } - - // Process command to install website - public function processCommandInstall($command, $text) { - $statusCode = 0; - if ($this->yellow->system->get("updateCurrentRelease")=="none") { - $this->checkCommandRequirements(); - list($installation, $option) = $this->yellow->toolbox->getTextArguments($text); - if (is_string_empty($command)) { - $statusCode = 200; - echo "Datenstrom Yellow is for people who make small websites. https://datenstrom.se/yellow/\n"; - echo "Syntax: php yellow.php\n"; - echo " php yellow.php about [extension]\n"; - echo " php yellow.php serve [url]\n"; - echo " php yellow.php skip installation [option]\n"; - } elseif ($command=="about" || $command=="serve") { - $statusCode = 0; - } elseif ($command=="skip" && $installation=="installation") { - $statusCode = $this->updateLog(); - if ($statusCode==200) $statusCode = $this->updateLanguages($option); - if ($statusCode==200) $statusCode = $this->updateExtensions($option, ""); - if ($statusCode==200) $statusCode = $this->updateSettings(true); - if ($statusCode==200) $statusCode = $this->removeInstall(); - if ($statusCode>=400) { - echo "ERROR installing files: ".$this->yellow->page->errorMessage."\n"; - echo "The installation has not been completed. Please run command again.\n"; - } else { - $extensionsCount = $this->getExtensionsCount(); - echo "Yellow $command: $extensionsCount extension".($extensionsCount!=1 ? "s" : "").", 0 errors\n"; - } - } else { - $statusCode = 500; - echo "The installation has not been completed. Please type 'php yellow.php serve' or 'php yellow.php skip installation`.\n"; - } - } else { - $statusCode = $this->removeInstall(true); - if ($statusCode==200) $statusCode = 0; - if ($statusCode>=400) { - echo "ERROR installing files: ".$this->yellow->page->errorMessage."\n"; - echo "Detected ZIP files, 0 extensions installed. Please run command again.\n"; - } - } - return $statusCode; - } - - // Update log file - public function updateLog() { - $statusCode = 200; - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreWebsiteFile"); - if (!is_file($fileName)) { - list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation(); - $product = "Datenstrom Yellow ".YellowCore::RELEASE; - $this->yellow->toolbox->log("info", "Install $product, PHP ".PHP_VERSION.", $name $version, $os"); - foreach ($this->yellow->extension->data as $key=>$value) { - if ($key=="install") continue; - $this->yellow->toolbox->log("info", "Install extension '".ucfirst($key)." $value[version]'"); - } - if (!is_file($fileName)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - } - return $statusCode; - } - - // Update languages - public function updateLanguages($option) { - $statusCode = 200; - $path = $this->yellow->system->get("coreExtensionDirectory")."install-language.bin"; - $zip = new ZipArchive(); - if ($zip->open($path)===true) { - $pathBase = ""; - if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1]; - $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateExtensionFile")); - foreach ($this->getLanguageExtensionsRequired($fileData, $option) as $extension) { - $fileDataPhp = $zip->getFromName($pathBase."translations/$extension/$extension.php"); - $fileDataIni = $zip->getFromName($pathBase."translations/$extension/extension.ini"); - $statusCode = max($statusCode, $this->updateLanguageArchive($fileDataPhp, $fileDataIni, $pathBase, "install")); - } - $this->yellow->extension->load($this->yellow->system->get("coreExtensionDirectory")); - $this->yellow->language->load($this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile")); - $zip->close(); - } else { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't open file '$path'!"); - } - return $statusCode; - } - - // Update language archive - public function updateLanguageArchive($fileDataPhp, $fileDataIni, $pathBase, $action) { - $statusCode = 200; - if ($this->yellow->extension->isExisting("update")) { - $settings = $this->yellow->toolbox->getTextSettings($fileDataIni, ""); - $extension = lcfirst($settings->get("extension")); - $version = $settings->get("version"); - $modified = strtotime($settings->get("published")); - $fileNamePhp = $this->yellow->system->get("coreExtensionDirectory").$extension.".php"; - if (!is_string_empty($extension) && !is_string_empty($version) && !is_file($fileNamePhp)) { - $statusCode = max($statusCode, $this->yellow->extension->get("update")->updateExtensionSettings($extension, $action, $settings)); - $statusCode = max($statusCode, $this->yellow->extension->get("update")->updateExtensionFile( - $fileNamePhp, $fileDataPhp, $modified, 0, 0, "create", $extension)); - $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); - } - } - return $statusCode; - } - - // Update extensions - public function updateExtensions($option, $extension) { - $statusCode = 200; - if ($this->yellow->extension->isExisting("update")) { - if (!is_string_empty($option)) { - if ($option=="medium" || $option=="large") { - $path = $this->yellow->system->get("coreExtensionDirectory"); - $fileData = $this->yellow->toolbox->readFile($path.$this->yellow->system->get("updateAvailableFile")); - $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); - $extensions = $this->getAvailableExtensionsRequired($settings, $option); - $statusCode = $this->downloadExtensionsAvailable($settings, $extensions); - $path = $this->yellow->system->get("coreExtensionDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^install-.*\.bin$/", true, false) as $entry) { - if (basename($entry)=="install-language.bin") continue; - if (preg_match("/^install-(.*?)\.bin/", basename($entry), $matches) && !in_array($matches[1], $extensions)) continue; - $statusCode = max($statusCode, $this->yellow->extension->get("update")->updateExtensionArchive($entry, "install")); - } - } - if (!($option=="small" || $option=="medium" || $option=="large")) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Option '$option' not supported!"); - } - } - if (!is_string_empty($extension)) { - $path = $this->yellow->system->get("coreExtensionDirectory")."install-".$extension.".bin"; - if (is_file($path)) { - $statusCode = $this->yellow->extension->get("update")->updateExtensionArchive($path, "install"); - } - } - } - return $statusCode; - } - - // Update user - public function updateUser($email, $password, $name, $language) { - $statusCode = 200; - if ($this->yellow->extension->isExisting("edit") && !is_string_empty($email) && !is_string_empty($password)) { - if (is_string_empty($name)) $name = $this->yellow->system->get("sitename"); - $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); - $settings = array( - "name" => $name, - "description" => $this->yellow->language->getText("editUserDescription", $language), - "language" => $language, - "access" => "create, edit, delete, restore, upload, configure, update", - "home" => "/", - "hash" => $this->yellow->extension->get("edit")->response->createHash($password), - "stamp" => $this->yellow->extension->get("edit")->response->createStamp(), - "pending" => "none", - "failed" => "0", - "modified" => date("Y-m-d H:i:s", time()), - "status" => "active"); - if (!$this->yellow->user->save($fileNameUser, $email, $settings)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileNameUser'!"); - } - $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", "Add user '".strtok($name, " ")."'"); - } - return $statusCode; - } - - // Update authentication - public function updateAuthentication($scheme, $address, $base, $email) { - if ($this->yellow->extension->isExisting("edit") && $this->yellow->user->isExisting($email)) { - $base = rtrim($base.$this->yellow->system->get("editLocation"), "/"); - $this->yellow->extension->get("edit")->response->createCookies($scheme, $address, $base, $email); - } - return 200; - } - - // Update content - public function updateContent($language, $name, $location) { - $statusCode = 200; - $fileName = $this->yellow->lookup->findFileFromContentLocation($location); - $fileData = str_replace("\r\n", "\n", $this->yellow->toolbox->readFile($fileName)); - if (!is_string_empty($fileData) && $language!="en") { - $titleOld = "Title: ".$this->yellow->language->getText("{$name}Title", "en")."\n"; - $titleNew = "Title: ".$this->yellow->language->getText("{$name}Title", $language)."\n"; - $fileData = str_replace($titleOld, $titleNew, $fileData); - $textOld = str_replace("\\n", "\n", $this->yellow->language->getText("{$name}Text", "en")); - $textNew = str_replace("\\n", "\n", $this->yellow->language->getText("{$name}Text", $language)); - $fileData = str_replace($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($skipInstallation = false) { - $statusCode = 200; - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - if (!$this->yellow->system->save($fileName, $this->getSystemSettings($skipInstallation))) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - $language = $this->yellow->system->get("language"); - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile"); - $fileData = $this->yellow->toolbox->readFile($fileName); - if (strposu($fileData, "Language:")===false) { - if (!is_string_empty($fileData)) $fileData .= "\n"; - $fileData .= "Language: $language\n"; - $fileData .= "media/images/photo.jpg: ".$this->yellow->language->getText("installExampleImage", $language)."\n"; - if (!$this->yellow->toolbox->createFile($fileName, $fileData)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - } - return $statusCode; - } - - // Remove files used by installation - public function removeInstall($log = false) { - $statusCode = 200; - if (function_exists("opcache_reset")) opcache_reset(); - $path = $this->yellow->system->get("coreExtensionDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^install-.*\.bin$/", true, false) as $entry) { - if (!$this->yellow->toolbox->deleteFile($entry)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't delete file '$entry'!"); - } - } - $fileName = $this->yellow->system->get("coreExtensionDirectory")."install.php"; - if ($statusCode==200 && !$this->yellow->toolbox->deleteFile($fileName)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); - } - if ($statusCode==200) unset($this->yellow->extension->data["install"]); - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile"); - $fileData = $this->yellow->toolbox->readFile($fileName); - $fileDataNew = $this->yellow->toolbox->unsetTextSettings($fileData, "extension", "install"); - if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - if ($log) $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", "Uninstall extension 'Install ".YellowInstall::VERSION."'"); - return $statusCode; - } - - // Check web server requirements - public function checkServerRequirements() { - if ($this->yellow->system->get("coreDebugMode")>=1) { - list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation(); - echo "YellowInstall::checkServerRequirements for $name $version, $os<br/>\n"; - } - if (!$this->checkServerComplete()) $this->yellow->exitFatalError("Datenstrom Yellow requires complete upload!"); - if (!$this->checkServerWrite()) $this->yellow->exitFatalError("Datenstrom Yellow requires write access!"); - if (!$this->checkServerConfiguration()) $this->yellow->exitFatalError("Datenstrom Yellow requires configuration file!"); - if (!$this->checkServerRewrite()) $this->yellow->exitFatalError("Datenstrom Yellow requires rewrite support!"); - } - - // Check command line requirements - public function checkCommandRequirements() { - if ($this->yellow->system->get("coreDebugMode")>=1) { - list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation(); - echo "YellowInstall::checkCommandRequirements for $name $version, $os<br/>\n"; - } - if (!$this->checkServerComplete()) $this->yellow->exitFatalError("Datenstrom Yellow requires complete upload!"); - if (!$this->checkServerWrite()) $this->yellow->exitFatalError("Datenstrom Yellow requires write access!"); - } - - // Check web server complete upload - public function checkServerComplete() { - $complete = true; - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile"); - $fileData = $this->yellow->toolbox->readFile($fileName); - $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); - $fileNames = array($fileName); - foreach ($settings as $extension=>$block) { - foreach ($block as $key=>$value) { - if (strposu($key, "/")) { - list($entry, $flags) = $this->yellow->toolbox->getTextList($value, ",", 2); - if (!preg_match("/create/i", $flags)) continue; - if (preg_match("/delete/i", $flags)) continue; - if (preg_match("/additional/i", $flags)) continue; - array_push($fileNames, $key); - } - } - } - foreach ($fileNames as $fileName) { - if (!is_file($fileName) || filesize($fileName)==0) { - $complete = false; - if ($this->yellow->system->get("coreDebugMode")>=1) { - echo "YellowInstall::checkServerComplete detected missing file:$fileName<br/>\n"; - } - } - } - return $complete; - } - - // Check web server write access - public function checkServerWrite() { - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - return $this->yellow->system->save($fileName, array()); - } - - // Check web server configuration file - public function checkServerConfiguration() { - list($name) = $this->yellow->toolbox->detectServerInformation(); - return strtoloweru($name)!="apache" || is_file(".htaccess"); - } - - // Check web server rewrite support - public function checkServerRewrite() { - $rewrite = true; - if (!$this->isServerBuiltin()) { - $curlHandle = curl_init(); - list($scheme, $address, $base) = $this->yellow->lookup->getRequestInformation(); - $location = $this->yellow->system->get("coreThemeLocation").$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; YellowInstall/".YellowInstall::VERSION).")"; - curl_setopt($curlHandle, CURLOPT_NOBODY, 1); - curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); - curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, false); - curl_exec($curlHandle); - $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); - curl_close($curlHandle); - if ($statusCode!=200) { - $rewrite = false; - if ($this->yellow->system->get("coreDebugMode")>=1 && !$rewrite) { - echo "YellowInstall::checkServerRewrite detected failed url:$url<br/>\n"; - } - } - } - return $rewrite; - } - - // Download available extension files - public function downloadExtensionsAvailable($settings, $extensions) { - $statusCode = 200; - if ($this->yellow->extension->isExisting("update")) { - $path = $this->yellow->system->get("coreExtensionDirectory"); - $extensionsNow = 0; - $extensionsTotal = count($extensions); - $curlHandle = curl_init(); - foreach ($extensions as $extension) { - echo "\rDownloading available extensions ".$this->getProgressPercent(++$extensionsNow, $extensionsTotal, 5, 95)."%... "; - $fileName = $path."install-".$this->yellow->lookup->normaliseName($extension, true, false, true).".bin"; - if (is_file($fileName)) continue; - $url = $settings[$extension]->get("downloadUrl"); - curl_setopt($curlHandle, CURLOPT_URL, $this->yellow->extension->get("update")->getExtensionDownloadUrl($url)); - curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowInstall/".YellowInstall::VERSION).")"; - curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); - $fileData = curl_exec($curlHandle); - $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); - $redirectUrl = ($statusCode>=300 && $statusCode<=399) ? curl_getinfo($curlHandle, CURLINFO_REDIRECT_URL) : ""; - if ($statusCode==0) { - $statusCode = 450; - $this->yellow->page->error($statusCode, "Can't connect to the update server!"); - } - if ($statusCode!=450 && $statusCode!=200) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't download file '$url'!"); - } - if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileName, $fileData)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - if ($this->yellow->system->get("coreDebugMode")>=2 && !is_string_empty($redirectUrl)) { - echo "YellowInstall::downloadExtensionsAvailable redirected to url:$redirectUrl<br/>\n"; - } - if ($this->yellow->system->get("coreDebugMode")>=2) { - echo "YellowInstall::downloadExtensionsAvailable status:$statusCode url:$url<br/>\n"; - } - if ($statusCode!=200) break; - } - curl_close($curlHandle); - echo "\rDownloading available extensions 100%... done\n"; - } - return $statusCode; - } - - // Return available extensions required - public function getAvailableExtensionsRequired($settings, $option) { - $extensions = array(); - if ($option=="medium") { - $text = "help highlight search toc"; - $extensions = array_unique(array_filter($this->yellow->toolbox->getTextArguments($text), "strlen")); - } elseif ($option=="large") { - foreach ($settings as $key=>$value) { - if (preg_match("/language/i", $value->get("tag"))) continue; - array_push($extensions, strtoloweru($key)); - } - } - return $extensions; - } - - // Return language extensions required - public function getLanguageExtensionsRequired($fileData, $option) { - $extensions = array(); - $languages = array(); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!is_string_empty($matches[1]) && !is_string_empty($matches[2]) && strposu($matches[1], "/")) { - $extension = basename($matches[1]); - $extension = $this->yellow->lookup->normaliseName($extension, true, true); - list($entry, $flags) = $this->yellow->toolbox->getTextList($matches[2], ",", 2); - $arguments = preg_split("/\s*,\s*/", trim($flags)); - $language = array_pop($arguments); - if (preg_match("/^(.*)\.php$/", basename($entry))) { - $languages[$language] = $extension; - } - } - } - } - if ($option=="large") { - foreach ($languages as $language=>$extension) { - array_push($extensions, $extension); - } - } else { - foreach ($this->getSystemLanguages("en, de, sv") as $language) { - if (isset($languages[$language])) array_push($extensions, $languages[$language]); - } - $extensions = array_slice($extensions, 0, 3); - } - return $extensions; - } - - // Return extensions installed - public function getExtensionsCount() { - $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile"); - $fileData = $this->yellow->toolbox->readFile($fileNameCurrent); - $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); - return count($settings); - } - - // Return system languages - public function getSystemLanguages($languagesDefault) { - $languages = array(); - foreach (preg_split("/\s*,\s*/", $this->yellow->toolbox->getServer("HTTP_ACCEPT_LANGUAGE")) as $string) { - list($language, $dummy) = $this->yellow->toolbox->getTextList($string, ";", 2); - if (!is_string_empty($language)) array_push($languages, $language); - } - foreach (preg_split("/\s*,\s*/", $languagesDefault) as $language) { - if (!is_string_empty($language)) array_push($languages, $language); - } - return array_unique($languages); - } - - // Return system settings - public function getSystemSettings($skipInstallation) { - $settings = array(); - foreach ($_REQUEST as $key=>$value) { - if (!$this->yellow->system->isExisting($key)) continue; - if ($key=="password" || $key=="status") continue; - $settings[$key] = trim($value); - } - if ($this->yellow->system->get("sitename")=="Datenstrom Yellow") $settings["sitename"] = $this->yellow->toolbox->detectServerSitename(); - if ($this->yellow->system->get("generateStaticUrl")=="auto" && getenv("URL")!==false) $settings["generateStaticUrl"] = getenv("URL"); - if ($this->yellow->system->get("generateStaticUrl")=="auto" && $skipInstallation) $settings["generateStaticUrl"] = "http://localhost:8000/"; - if ($this->yellow->system->get("coreTimezone")=="UTC") $settings["coreTimezone"] = $this->yellow->toolbox->detectServerTimezone(); - if ($this->yellow->system->get("updateEventPending")=="none") $settings["updateEventPending"] = "website/install"; - $settings["updateCurrentRelease"] = YellowCore::RELEASE; - return $settings; - } - - // Return raw data for install page - public function getRawDataInstall() { - $languages = $this->yellow->system->getAvailable("language"); - $language = $this->yellow->toolbox->detectBrowserLanguage($languages, $this->yellow->system->get("language")); - $this->yellow->language->set($language); - $rawData = "---\nTitle:".$this->yellow->language->getText("installTitle")."\nLanguage:$language\nNavigation:navigation\nHeader:none\nFooter:none\nSidebar:none\n---\n"; - $rawData .= "<form class=\"install-form\" action=\"".$this->yellow->page->getLocation(true)."\" method=\"post\">\n"; - $rawData .= "<p><label for=\"author\">".$this->yellow->language->getText("editSignupName")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"author\" id=\"author\" value=\"\"></p>\n"; - $rawData .= "<p><label for=\"email\">".$this->yellow->language->getText("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->language->getText("editSignupPassword")."</label><br /><input class=\"form-control\" type=\"password\" maxlength=\"64\" name=\"password\" id=\"password\" value=\"\"></p>\n"; - $rawData .= "<p>".$this->yellow->language->getText("installLanguage")."</p>\n<p>"; - foreach ($languages as $language) { - $checked = $language==$this->yellow->language->language ? " checked=\"checked\"" : ""; - $rawData .= "<label for=\"{$language}-language\"><input type=\"radio\" name=\"language\" id=\"{$language}-language\" value=\"$language\"$checked> ".$this->yellow->language->getTextHtml("languageDescription", $language)."</label><br />"; - } - $rawData .= "</p>\n"; - $rawData .= "<p>".$this->yellow->language->getText("installExtension")."</p>\n<p>"; - foreach (array("website", "wiki", "blog") as $extension) { - $checked = $extension=="website" ? " checked=\"checked\"" : ""; - $rawData .= "<label for=\"{$extension}-extension\"><input type=\"radio\" name=\"extension\" id=\"{$extension}-extension\" value=\"$extension\"$checked> ".$this->yellow->language->getTextHtml("installExtension".ucfirst($extension))."</label><br />"; - } - $rawData .= "</p>\n"; - $rawData .= "<input class=\"btn\" type=\"submit\" value=\"".$this->yellow->language->getText("installButton")."\" />\n"; - $rawData .= "<input type=\"hidden\" name=\"status\" value=\"install\" />\n"; - $rawData .= "</form>\n"; - return $rawData; - } - - // Return progress in percent - public function getProgressPercent($now, $total, $increments, $max) { - $max = intval($max/$increments) * $increments; - $percent = intval(($max/$total) * $now); - if ($increments>1) $percent = intval($percent/$increments) * $increments; - return min($max, $percent); - } - - // Check if running built-in web server - public function isServerBuiltin() { - list($name) = $this->yellow->toolbox->detectServerInformation(); - return strtoloweru($name)=="built-in"; - } -} diff --git a/system/extensions/markdown.php b/system/extensions/markdown.php @@ -1,4080 +0,0 @@ -<?php -// Markdown extension, https://github.com/annaesvensson/yellow-markdown - -class YellowMarkdown { - const VERSION = "0.8.28"; - 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 YellowMarkdownParser($this->yellow, $page); - $text = $markdown->transform($text); - $text = $this->yellow->lookup->normaliseData($text, "html"); - return $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.9.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|null - */ - public $url_filter_func = null; - - /** - * Optional header id="" generation callback function. - * @var callable|null - */ - public $header_id_func = null; - - /** - * Optional function for converting code block content to HTML - * @var callable|null - */ - public $code_block_content_func = null; - - /** - * Optional function for converting code span content to HTML. - * @var callable|null - */ - 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) { - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] === '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - // If the URL was of the form <s p a c e s> it got caught by the HTML - // tag parser and hashed. Need to reverse the process before using - // the URL. - $unhashed = $this->unhash($url); - if ($unhashed !== $url) - $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); - - $url = $this->encodeURLAttribute($url); - - $result = "<a href=\"$url\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text</a>"; - - return $this->hashPart($result); - } - - /** - * Turn Markdown image shortcuts into <img> tags. - * @param string $text - * @return string - */ - protected function doImages($text) { - // First, handle reference-style labeled images: ![alt text][id] - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - ('.$this->nested_brackets_re.') # alt text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - - ) - }xs', - array($this, '_doImages_reference_callback'), $text); - - // Next, handle inline images: ![alt text](url "optional title") - // Don't forget: encode * and _ - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - ('.$this->nested_brackets_re.') # alt text = $2 - \] - \s? # One optional whitespace character - \( # literal paren - [ \n]* - (?: - <(\S*)> # src url = $3 - | - ('.$this->nested_url_parenthesis_re.') # src url = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # title = $7 - \6 # matching quote - [ \n]* - )? # title is optional - \) - ) - }xs', - array($this, '_doImages_inline_callback'), $text); - - return $text; - } - - /** - * Callback to parse references image tags - * @param array $matches - * @return string - */ - protected function _doImages_reference_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower($matches[3]); - - if ($link_id == "") { - $link_id = strtolower($alt_text); // for shortcut links like ![this][]. - } - - $alt_text = $this->encodeAttribute($alt_text); - if (isset($this->urls[$link_id])) { - $url = $this->encodeURLAttribute($this->urls[$link_id]); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($this->titles[$link_id])) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - $result .= $this->empty_element_suffix; - $result = $this->hashPart($result); - } else { - // If there's no such link ID, leave intact: - $result = $whole_match; - } - - return $result; - } - - /** - * Callback to parse inline image tags - * @param array $matches - * @return string - */ - protected function _doImages_inline_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $url = $matches[3] == '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - - $alt_text = $this->encodeAttribute($alt_text); - $url = $this->encodeURLAttribute($url); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; // $title already quoted - } - $result .= $this->empty_element_suffix; - - return $this->hashPart($result); - } - - /** - * Parse Markdown heading elements to HTML - * @param string $text - * @return string - */ - protected function doHeaders($text) { - /** - * Setext-style headers: - * Header 1 - * ======== - * - * Header 2 - * -------- - */ - $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', - array($this, '_doHeaders_callback_setext'), $text); - - /** - * atx-style headers: - * # Header 1 - * ## Header 2 - * ## Header 2 with closing hashes ## - * ... - * ###### Header 6 - */ - $text = preg_replace_callback('{ - ^(\#{1,6}) # $1 = string of #\'s - [ ]* - (.+?) # $2 = Header text - [ ]* - \#* # optional closing #\'s (not counted) - \n+ - }xm', - array($this, '_doHeaders_callback_atx'), $text); - - return $text; - } - - /** - * Setext header parsing callback - * @param array $matches - * @return string - */ - protected function _doHeaders_callback_setext($matches) { - // Terrible hack to check we haven't found an empty list item. - if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) { - return $matches[0]; - } - - $level = $matches[2][0] == '=' ? 1 : 2; - - // ID attribute generation - $idAtt = $this->_generateIdFromHeaderValue($matches[1]); - - $block = "<h$level$idAtt>".$this->runSpanGamut($matches[1])."</h$level>"; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - /** - * ATX header parsing callback - * @param array $matches - * @return string - */ - protected function _doHeaders_callback_atx($matches) { - // ID attribute generation - $idAtt = $this->_generateIdFromHeaderValue($matches[2]); - - $level = strlen($matches[1]); - $block = "<h$level$idAtt>".$this->runSpanGamut($matches[2])."</h$level>"; - return "\n" . $this->hashBlock($block) . "\n\n"; - } - - /** - * If a header_id_func property is set, we can use it to automatically - * generate an id attribute. - * - * This method returns a string in the form id="foo", or an empty string - * otherwise. - * @param string $headerValue - * @return string - */ - protected function _generateIdFromHeaderValue($headerValue) { - if (!is_callable($this->header_id_func)) { - return ""; - } - - $idValue = call_user_func($this->header_id_func, $headerValue); - if (!$idValue) { - return ""; - } - - return ' id="' . $this->encodeAttribute($idValue) . '"'; - } - - /** - * Form HTML ordered (numbered) and unordered (bulleted) lists. - * @param string $text - * @return string - */ - protected function doLists($text) { - $less_than_tab = $this->tab_width - 1; - - // Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - - $markers_relist = array( - $marker_ul_re => $marker_ol_re, - $marker_ol_re => $marker_ul_re, - ); - - foreach ($markers_relist as $marker_re => $other_marker_re) { - // Re-usable pattern to match any entirel ul or ol list: - $whole_list_re = ' - ( # $1 = whole list - ( # $2 - ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces - ('.$marker_re.') # $4 = first list item marker - [ ]+ - ) - (?s:.+?) - ( # $5 - \z - | - \n{2,} - (?=\S) - (?! # Negative lookahead for another list item marker - [ ]* - '.$marker_re.'[ ]+ - ) - | - (?= # Lookahead for another kind of list - \n - \3 # Must have the same indentation - '.$other_marker_re.'[ ]+ - ) - ) - ) - '; // mx - - // We use a different prefix before nested lists than top-level lists. - //See extended comment in _ProcessListItems(). - - if ($this->list_level) { - $text = preg_replace_callback('{ - ^ - '.$whole_list_re.' - }mx', - array($this, '_doLists_callback'), $text); - } else { - $text = preg_replace_callback('{ - (?:(?<=\n)\n|\A\n?) # Must eat the newline - '.$whole_list_re.' - }mx', - array($this, '_doLists_callback'), $text); - } - } - - return $text; - } - - /** - * List parsing callback - * @param array $matches - * @return string - */ - protected function _doLists_callback($matches) { - // Re-usable patterns to match list item bullets and number markers: - $marker_ul_re = '[*+-]'; - $marker_ol_re = '\d+[\.]'; - $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; - $marker_ol_start_re = '[0-9]+'; - - $list = $matches[1]; - $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; - - $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); - - $list .= "\n"; - $result = $this->processListItems($list, $marker_any_re); - - $ol_start = 1; - if ($this->enhanced_ordered_list) { - // Get the start number for ordered list. - if ($list_type == 'ol') { - $ol_start_array = array(); - $ol_start_check = preg_match("/$marker_ol_start_re/", $matches[4], $ol_start_array); - if ($ol_start_check){ - $ol_start = $ol_start_array[0]; - } - } - } - - if ($ol_start > 1 && $list_type == 'ol'){ - $result = $this->hashBlock("<$list_type start=\"$ol_start\">\n" . $result . "</$list_type>"); - } else { - $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>"); - } - return "\n". $result ."\n\n"; - } - - /** - * Nesting tracker for list levels - * @var integer - */ - protected $list_level = 0; - - /** - * Process the contents of a single ordered or unordered list, splitting it - * into individual list items. - * @param string $list_str - * @param string $marker_any_re - * @return string - */ - protected function processListItems($list_str, $marker_any_re) { - /** - * The $this->list_level global keeps track of when we're inside a list. - * Each time we enter a list, we increment it; when we leave a list, - * we decrement. If it's zero, we're not in a list anymore. - * - * We do this because when we're not inside a list, we want to treat - * something like this: - * - * I recommend upgrading to version - * 8. Oops, now this line is treated - * as a sub-list. - * - * As a single paragraph, despite the fact that the second line starts - * with a digit-period-space sequence. - * - * Whereas when we're inside a list (or sub-list), that line will be - * treated as the start of a sub-list. What a kludge, huh? This is - * an aspect of Markdown's syntax that's hard to parse perfectly - * without resorting to mind-reading. Perhaps the solution is to - * change the syntax rules such that sub-lists must start with a - * starting cardinal number; e.g. "1." or "a.". - */ - $this->list_level++; - - // Trim trailing blank lines: - $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); - - $list_str = preg_replace_callback('{ - (\n)? # leading line = $1 - (^[ ]*) # leading whitespace = $2 - ('.$marker_any_re.' # list marker and space = $3 - (?:[ ]+|(?=\n)) # space only required if item is not empty - ) - ((?s:.*?)) # list item text = $4 - (?:(\n+(?=\n))|\n) # tailing blank line = $5 - (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) - }xm', - array($this, '_processListItems_callback'), $list_str); - - $this->list_level--; - return $list_str; - } - - /** - * List item parsing callback - * @param array $matches - * @return string - */ - protected function _processListItems_callback($matches) { - $item = $matches[4]; - $leading_line =& $matches[1]; - $leading_space =& $matches[2]; - $marker_space = $matches[3]; - $tailing_blank_line =& $matches[5]; - - if ($leading_line || $tailing_blank_line || - preg_match('/\n{2,}/', $item)) - { - // Replace marker with the appropriate whitespace indentation - $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; - $item = $this->runBlockGamut($this->outdent($item)."\n"); - } else { - // Recursion for sub-lists: - $item = $this->doLists($this->outdent($item)); - $item = $this->formParagraphs($item, false); - } - - return "<li>" . $item . "</li>\n"; - } - - /** - * Process Markdown `<pre><code>` blocks. - * @param string $text - * @return string - */ - protected function doCodeBlocks($text) { - $text = preg_replace_callback('{ - (?:\n\n|\A\n?) - ( # $1 = the code block -- one or more lines, starting with a space/tab - (?> - [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces - .*\n+ - )+ - ) - ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc - }xm', - array($this, '_doCodeBlocks_callback'), $text); - - return $text; - } - - /** - * Code block parsing callback - * @param array $matches - * @return string - */ - protected function _doCodeBlocks_callback($matches) { - $codeblock = $matches[1]; - - $codeblock = $this->outdent($codeblock); - if (is_callable($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 (is_callable($this->code_span_content_func)) { - $code = call_user_func($this->code_span_content_func, $code); - } else { - $code = htmlspecialchars(trim($code), ENT_NOQUOTES); - } - return $this->hashPart("<code>$code</code>"); - } - - /** - * Define the emphasis operators with their regex matches - * @var array - */ - protected $em_relist = array( - '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?![\.,:;]?\s)', - '*' => '(?<![\s*])\*(?!\*)', - '_' => '(?<![\s_])_(?!_)', - ); - - /** - * Define the strong operators with their regex matches - * @var array - */ - protected $strong_relist = array( - '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?![\.,:;]?\s)', - '**' => '(?<![\s*])\*\*(?!\*)', - '__' => '(?<![\s_])__(?!_)', - ); - - /** - * Define the emphasis + strong operators with their regex matches - * @var array - */ - protected $em_strong_relist = array( - '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?![\.,:;]?\s)', - '***' => '(?<![\s*])\*\*\*(?!\*)', - '___' => '(?<![\s_])___(?!_)', - ); - - /** - * Container for prepared regular expressions - * @var array - */ - protected $em_strong_prepared_relist; - - /** - * Prepare regular expressions for searching emphasis tokens in any - * context. - * @return void - */ - protected function prepareItalicsAndBold() { - foreach ($this->em_relist as $em => $em_re) { - foreach ($this->strong_relist as $strong => $strong_re) { - // Construct list of allowed token expressions. - $token_relist = array(); - if (isset($this->em_strong_relist["$em$strong"])) { - $token_relist[] = $this->em_strong_relist["$em$strong"]; - } - $token_relist[] = $em_re; - $token_relist[] = $strong_re; - - // Construct master expression from list. - $token_re = '{(' . implode('|', $token_relist) . ')}'; - $this->em_strong_prepared_relist["$em$strong"] = $token_re; - } - } - } - - /** - * Convert Markdown italics (emphasis) and bold (strong) to HTML - * @param string $text - * @return string - */ - protected function doItalicsAndBold($text) { - if ($this->in_emphasis_processing) { - return $text; // avoid reentrency - } - $this->in_emphasis_processing = true; - - $token_stack = array(''); - $text_stack = array(''); - $em = ''; - $strong = ''; - $tree_char_em = false; - - while (1) { - // Get prepared regular expression for seraching emphasis tokens - // in current context. - $token_re = $this->em_strong_prepared_relist["$em$strong"]; - - // Each loop iteration search for the next emphasis token. - // Each token is then passed to handleSpanToken. - $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); - $text_stack[0] .= $parts[0]; - $token =& $parts[1]; - $text =& $parts[2]; - - if (empty($token)) { - // Reached end of text span: empty stack without emitting. - // any more emphasis. - while ($token_stack[0]) { - $text_stack[1] .= array_shift($token_stack); - $text_stack[0] .= array_shift($text_stack); - } - break; - } - - $token_len = strlen($token); - if ($tree_char_em) { - // Reached closing marker while inside a three-char emphasis. - if ($token_len == 3) { - // Three-char closing marker, close em and strong. - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<strong><em>$span</em></strong>"; - $text_stack[0] .= $this->hashPart($span); - $em = ''; - $strong = ''; - } else { - // Other closing marker: close one em or strong and - // change current token state to match the other - $token_stack[0] = str_repeat($token[0], 3-$token_len); - $tag = $token_len == 2 ? "strong" : "em"; - $span = $text_stack[0]; - $span = $this->runSpanGamut($span); - $span = "<$tag>$span</$tag>"; - $text_stack[0] = $this->hashPart($span); - $$tag = ''; // $$tag stands for $em or $strong - } - $tree_char_em = false; - } else if ($token_len == 3) { - if ($em) { - // Reached closing marker for both em and strong. - // Closing strong marker: - for ($i = 0; $i < 2; ++$i) { - $shifted_token = array_shift($token_stack); - $tag = strlen($shifted_token) == 2 ? "strong" : "em"; - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<$tag>$span</$tag>"; - $text_stack[0] .= $this->hashPart($span); - $$tag = ''; // $$tag stands for $em or $strong - } - } else { - // Reached opening three-char emphasis marker. Push on token - // stack; will be handled by the special condition above. - $em = $token[0]; - $strong = "$em$em"; - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $tree_char_em = true; - } - } else if ($token_len == 2) { - if ($strong) { - // Unwind any dangling emphasis marker: - if (strlen($token_stack[0]) == 1) { - $text_stack[1] .= array_shift($token_stack); - $text_stack[0] .= array_shift($text_stack); - $em = ''; - } - // Closing strong marker: - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<strong>$span</strong>"; - $text_stack[0] .= $this->hashPart($span); - $strong = ''; - } else { - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $strong = $token; - } - } else { - // Here $token_len == 1 - if ($em) { - if (strlen($token_stack[0]) == 1) { - // Closing emphasis marker: - array_shift($token_stack); - $span = array_shift($text_stack); - $span = $this->runSpanGamut($span); - $span = "<em>$span</em>"; - $text_stack[0] .= $this->hashPart($span); - $em = ''; - } else { - $text_stack[0] .= $token; - } - } else { - array_unshift($token_stack, $token); - array_unshift($text_stack, ''); - $em = $token; - } - } - } - $this->in_emphasis_processing = false; - return $text_stack[0]; - } - - /** - * Parse Markdown blockquotes to HTML - * @param string $text - * @return string - */ - protected function doBlockQuotes($text) { - $text = preg_replace_callback('/ - ( # Wrap whole match in $1 - (?> - ^[ ]*>[ ]? # ">" at the start of a line - .+\n # rest of the first line - (.+\n)* # subsequent consecutive lines - \n* # blanks - )+ - ) - /xm', - array($this, '_doBlockQuotes_callback'), $text); - - return $text; - } - - /** - * Blockquote parsing callback - * @param array $matches - * @return string - */ - protected function _doBlockQuotes_callback($matches) { - $bq = $matches[1]; - // trim one level of quoting - trim whitespace-only lines - $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); - $bq = $this->runBlockGamut($bq); // recurse - - $bq = preg_replace('/^/m', " ", $bq); - // These leading spaces cause problem with <pre> content, - // so we need to fix that: - $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx', - array($this, '_doBlockQuotes_callback2'), $bq); - - return "\n" . $this->hashBlock("<blockquote>\n$bq\n</blockquote>") . "\n\n"; - } - - /** - * Blockquote parsing callback - * @param array $matches - * @return string - */ - protected function _doBlockQuotes_callback2($matches) { - $pre = $matches[1]; - $pre = preg_replace('/^ /m', '', $pre); - return $pre; - } - - /** - * Parse paragraphs - * - * @param string $text String to process in paragraphs - * @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags - * @return string - */ - protected function formParagraphs($text, $wrap_in_p = true) { - // Strip leading and trailing lines: - $text = preg_replace('/\A\n+|\n+\z/', '', $text); - - $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); - - // Wrap <p> tags and unhashify HTML blocks - foreach ($grafs as $key => $value) { - if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { - // Is a paragraph. - $value = $this->runSpanGamut($value); - if ($wrap_in_p) { - $value = preg_replace('/^([ ]*)/', "<p>", $value); - $value .= "</p>"; - } - $grafs[$key] = $this->unhash($value); - } else { - // Is a block. - // Modify elements of @grafs in-place... - $graf = $value; - $block = $this->html_hashes[$graf]; - $graf = $block; -// if (preg_match('{ -// \A -// ( # $1 = <div> tag -// <div \s+ -// [^>]* -// \b -// markdown\s*=\s* ([\'"]) # $2 = attr quote char -// 1 -// \2 -// [^>]* -// > -// ) -// ( # $3 = contents -// .* -// ) -// (</div>) # $4 = closing tag -// \z -// }xs', $block, $matches)) -// { -// list(, $div_open, , $div_content, $div_close) = $matches; -// -// // We can't call Markdown(), because that resets the hash; -// // that initialization code should be pulled into its own sub, though. -// $div_content = $this->hashHTMLBlocks($div_content); -// -// // Run document gamut methods on the content. -// foreach ($this->document_gamut as $method => $priority) { -// $div_content = $this->$method($div_content); -// } -// -// $div_open = preg_replace( -// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); -// -// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; -// } - $grafs[$key] = $graf; - } - } - - return implode("\n\n", $grafs); - } - - /** - * Encode text for a double-quoted HTML attribute. This function - * is *not* suitable for attributes enclosed in single quotes. - * @param string $text - * @return string - */ - protected function encodeAttribute($text) { - $text = $this->encodeAmpsAndAngles($text); - $text = str_replace('"', '&quot;', $text); - return $text; - } - - /** - * Encode text for a double-quoted HTML attribute containing a URL, - * applying the URL filter if set. Also generates the textual - * representation for the URL (removing mailto: or tel:) storing it in $text. - * This function is *not* suitable for attributes enclosed in single quotes. - * - * @param string $url - * @param string $text Passed by reference - * @return string URL - */ - protected function encodeURLAttribute($url, &$text = null) { - if (is_callable($this->url_filter_func)) { - $url = call_user_func($this->url_filter_func, $url); - } - - if (preg_match('{^mailto:}i', $url)) { - $url = $this->encodeEntityObfuscatedAttribute($url, $text, 7); - } else if (preg_match('{^tel:}i', $url)) { - $url = $this->encodeAttribute($url); - $text = substr($url, 4); - } else { - $url = $this->encodeAttribute($url); - $text = $url; - } - - return $url; - } - - /** - * Smart processing for ampersands and angle brackets that need to - * be encoded. Valid character entities are left alone unless the - * no-entities mode is set. - * @param string $text - * @return string - */ - protected function encodeAmpsAndAngles($text) { - if ($this->no_entities) { - $text = str_replace('&', '&amp;', $text); - } else { - // Ampersand-encoding based entirely on Nat Irons's Amputator - // MT plugin: <http://bumppo.net/projects/amputator/> - $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', - '&amp;', $text); - } - // Encode remaining <'s - $text = str_replace('<', '&lt;', $text); - - return $text; - } - - /** - * Parse Markdown automatic links to anchor HTML tags - * @param string $text - * @return string - */ - protected function doAutoLinks($text) { - $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i', - array($this, '_doAutoLinks_url_callback'), $text); - - // Email addresses: <address@domain.foo> - $text = preg_replace_callback('{ - < - (?:mailto:)? - ( - (?: - [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ - | - ".*?" - ) - \@ - (?: - [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ - | - \[[\d.a-fA-F:]+\] # IPv4 & IPv6 - ) - ) - > - }xi', - array($this, '_doAutoLinks_email_callback'), $text); - - return $text; - } - - /** - * Parse URL callback - * @param array $matches - * @return string - */ - protected function _doAutoLinks_url_callback($matches) { - $url = $this->encodeURLAttribute($matches[1], $text); - $link = "<a href=\"$url\">$text</a>"; - return $this->hashPart($link); - } - - /** - * Parse email address callback - * @param array $matches - * @return string - */ - protected function _doAutoLinks_email_callback($matches) { - $addr = $matches[1]; - $url = $this->encodeURLAttribute("mailto:$addr", $text); - $link = "<a href=\"$url\">$text</a>"; - return $this->hashPart($link); - } - - /** - * Input: some text to obfuscate, e.g. "mailto:foo@example.com" - * - * Output: the same text but with most characters encoded as either a - * decimal or hex entity, in the hopes of foiling most address - * harvesting spam bots. E.g.: - * - * &#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111; - * &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111; - * &#x6d; - * - * Note: the additional output $tail is assigned the same value as the - * ouput, minus the number of characters specified by $head_length. - * - * Based by a filter by Matthew Wickline, posted to BBEdit-Talk. - * With some optimizations by Milian Wolff. Forced encoding of HTML - * attribute special characters by Allan Odgaard. - * - * @param string $text - * @param string $tail Passed by reference - * @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 Passed by reference - * @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. - * @var string - */ - public $fn_link_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. - * Optionally use '^^' and '%%' to refer to the footnote number and - * reference number respectively. {@see parseFootnotePlaceholders()} - * @var string - */ - public $fn_backlink_html = '&#8617;&#xFE0E;'; - - /** - * Optional title and aria-label attributes for footnote backlinks for - * added accessibility (to ensure backlink uniqueness). - * Use '^^' and '%%' to refer to the footnote number and reference number - * respectively. {@see parseFootnotePlaceholders()} - * @var string - */ - public $fn_backlink_title = ""; - public $fn_backlink_label = ""; - - /** - * 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; - - /** - * Determines whether footnotes should be appended to the end of the document. - * If true, footnote html can be retrieved from $this->footnotes_assembled. - * @var boolean - */ - public $omit_footnotes = false; - - - /** - * After parsing, the HTML for the list of footnotes appears here. - * This is available only if $omit_footnotes == true. - * - * Note: when placing the content of `footnotes_assembled` on the page, - * consider adding the attribute `role="doc-endnotes"` to the `div` or - * `section` that will enclose the list of footnotes so they are - * reachable to accessibility tools the same way they would be with the - * default HTML output. - * @var null|string - */ - public $footnotes_assembled = null; - - /** - * 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; - - /** - * Ref attribute for links - * @var array - */ - protected $ref_attr = array(); - - /** - * 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; - $this->footnotes_assembled = null; - - 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 = ''; - - if ( ! $this->omit_footnotes ) - $this->footnotes_assembled = null; - - 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 === false || $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. - - // 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 bool $md_attr Handle `markdown="1"` attribute - * @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. - $base_tag_name_re = ''; - - // 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. - $mode = $attr_m[2] . $attr_m[3]; - $span_mode = $mode === 'span' || ($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 .= (string) $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) { - $link_text = $this->runSpanGamut($matches[2]); - $url = $matches[3] === '' ? $matches[4] : $matches[3]; - $title =& $matches[7]; - $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); - - // if the URL was of the form <s p a c e s> it got caught by the HTML - // tag parser and hashed. Need to reverse the process before using the URL. - $unhashed = $this->unhash($url); - if ($unhashed !== $url) - $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); - - $url = $this->encodeURLAttribute($url); - - $result = "<a href=\"$url\""; - if (isset($title)) { - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - $result .= $attr; - - $link_text = $this->runSpanGamut($link_text); - $result .= ">$link_text</a>"; - - return $this->hashPart($result); - } - - /** - * Turn Markdown image shortcuts into <img> tags. - * @param string $text - * @return string - */ - protected function doImages($text) { - // First, handle reference-style labeled images: ![alt text][id] - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - (' . $this->nested_brackets_re . ') # alt text = $2 - \] - - [ ]? # one optional space - (?:\n[ ]*)? # one optional newline followed by spaces - - \[ - (.*?) # id = $3 - \] - - ) - }xs', - array($this, '_doImages_reference_callback'), $text); - - // Next, handle inline images: ![alt text](url "optional title") - // Don't forget: encode * and _ - $text = preg_replace_callback('{ - ( # wrap whole match in $1 - !\[ - (' . $this->nested_brackets_re . ') # alt text = $2 - \] - \s? # One optional whitespace character - \( # literal paren - [ \n]* - (?: - <(\S*)> # src url = $3 - | - (' . $this->nested_url_parenthesis_re . ') # src url = $4 - ) - [ \n]* - ( # $5 - ([\'"]) # quote char = $6 - (.*?) # title = $7 - \6 # matching quote - [ \n]* - )? # title is optional - \) - (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes - ) - }xs', - array($this, '_doImages_inline_callback'), $text); - - return $text; - } - - /** - * Callback for referenced images - * @param array $matches - * @return string - */ - protected function _doImages_reference_callback($matches) { - $whole_match = $matches[1]; - $alt_text = $matches[2]; - $link_id = strtolower($matches[3]); - - if ($link_id === "") { - $link_id = strtolower($alt_text); // for shortcut links like ![this][]. - } - - $alt_text = $this->encodeAttribute($alt_text); - if (isset($this->urls[$link_id])) { - $url = $this->encodeURLAttribute($this->urls[$link_id]); - $result = "<img src=\"$url\" alt=\"$alt_text\""; - if (isset($this->titles[$link_id])) { - $title = $this->titles[$link_id]; - $title = $this->encodeAttribute($title); - $result .= " title=\"$title\""; - } - if (isset($this->ref_attr[$link_id])) { - $result .= $this->ref_attr[$link_id]; - } - $result .= $this->empty_element_suffix; - $result = $this->hashPart($result); - } - else { - // If there's no such link ID, leave intact: - $result = $whole_match; - } - - return $result; - } - - /** - * Callback for inline images - * @param array $matches - * @return string - */ - protected function _doImages_inline_callback($matches) { - $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) { - - $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 ) ) { - $this->_doFootnotes(); - if ( ! $this->omit_footnotes ) { - $text .= "\n\n"; - $text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n"; - $text .= "<hr" . $this->empty_element_suffix . "\n"; - $text .= $this->footnotes_assembled; - $text .= "</div>"; - } - } - return $text; - } - - - /** - * Generates the HTML for footnotes. Called by appendFootnotes, even if - * footnotes are not being appended. - * @return void - */ - protected function _doFootnotes() { - $attr = array(); - if ($this->fn_backlink_class !== "") { - $class = $this->fn_backlink_class; - $class = $this->encodeAttribute($class); - $attr['class'] = " class=\"$class\""; - } - $attr['role'] = " role=\"doc-backlink\""; - $num = 0; - - $text = "<ol>\n\n"; - 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); - - $num++; - $note_id = $this->encodeAttribute($note_id); - - // Prepare backlink, multiple backlinks if multiple references - // Do not create empty backlinks if the html is blank - $backlink = ""; - if (!empty($this->fn_backlink_html)) { - for ($ref_num = 1; $ref_num <= $ref_count; ++$ref_num) { - if (!empty($this->fn_backlink_title)) { - $attr['title'] = ' title="' . $this->encodeAttribute($this->fn_backlink_title) . '"'; - } - if (!empty($this->fn_backlink_label)) { - $attr['label'] = ' aria-label="' . $this->encodeAttribute($this->fn_backlink_label) . '"'; - } - $parsed_attr = $this->parseFootnotePlaceholders( - implode('', $attr), - $num, - $ref_num - ); - $backlink_text = $this->parseFootnotePlaceholders( - $this->fn_backlink_html, - $num, - $ref_num - ); - $ref_count_mark = $ref_num > 1 ? $ref_num : ''; - $backlink .= " <a href=\"#fnref$ref_count_mark:$note_id\"$parsed_attr>$backlink_text</a>"; - } - $backlink = trim($backlink); - } - - // Add backlink to last paragraph; create new paragraph if needed. - if (!empty($backlink)) { - if (preg_match('{</p>$}', $footnote)) { - $footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>"; - } else { - $footnote .= "\n\n<p>$backlink</p>"; - } - } - - $text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n"; - $text .= $footnote . "\n"; - $text .= "</li>\n\n"; - } - $text .= "</ol>\n"; - - $this->footnotes_assembled = $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] . "]"; - } - - /** - * Build footnote label by evaluating any placeholders. - * - ^^ footnote number - * - %% footnote reference number (Nth reference to footnote number) - * @param string $label - * @param int $footnote_number - * @param int $reference_number - * @return string - */ - protected function parseFootnotePlaceholders($label, $footnote_number, $reference_number) { - return str_replace( - array('^^', '%%'), - array($footnote_number, $reference_number), - $label - ); - } - - - /** - * 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>"); - } - $desc = $this->encodeAttribute($desc); - return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>"); - } - return $matches[0]; - } -} - -// Markdown parser, Copyright Datenstrom, License GPLv2 - -class YellowMarkdownParser extends MarkdownExtraParser { - public $yellow; // access to API - public $page; // access to page - public $idAttributes; // id attributes - public $noticeLevel; // recursive level - - public function __construct($yellow, $page) { - $this->yellow = $yellow; - $this->page = $page; - $this->idAttributes = array(); - $this->noticeLevel = 0; - $this->url_filter_func = function($url) use ($yellow, $page) { - return $yellow->lookup->normaliseLocation($url, $page->getPage("main")->location); - }; - $this->span_gamut += array("doStrikethrough" => 55); - $this->block_gamut += array("doNoticeBlocks" => 65); - $this->document_gamut += array("doFootnotesLinks" => 55); - $this->escape_chars .= "~"; - parent::__construct(); - } - - // Handle striketrough - public function doStrikethrough($text) { - $parts = preg_split("/(?<![~])(~~)(?![~])/", $text, -1, PREG_SPLIT_DELIM_CAPTURE); - if (count($parts)>3) { - $text = ""; - $open = false; - foreach ($parts as $part) { - if ($part=="~~") { - $text .= $open ? "</del>" : "<del>"; - $open = !$open; - } else { - $text .= $part; - } - } - if ($open) $text .= "</del>"; - } - return $text; - } - - // 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->parseContentElement($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->parseContentElement($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->parseContentElement("", $matches[1], "", "symbol"); - return is_null($output) ? $matches[0] : $this->hashPart($output); - } - - // Handle fenced code blocks - public function _doFencedCodeBlocks_callback($matches) { - $name = $this->getBlockName($matches[2], $matches[3]); - $text = $matches[4]; - $attributes = $matches[3]; - $output = $this->page->parseContentElement($name, $text, $attributes, "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 (is_string_empty($attr) && $level>=2) $attr = $this->getIdAttribute($text); - $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; - return "\n".$this->hashBlock($output)."\n\n"; - } - - // 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 (is_string_empty($attr) && $level>=2) $attr = $this->getIdAttribute($text); - $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; - return "\n".$this->hashBlock($output)."\n\n"; - } - - // Handle inline links - public function _doAnchors_inline_callback($matches) { - $url = $matches[3]=="" ? $matches[4] : $matches[3]; - $text = $matches[2]; - $title = isset($matches[7]) ? $matches[7] : ""; - $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); - $output = "<a href=\"".$this->encodeURLAttribute($url)."\""; - if (!is_string_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) { - $src = $matches[3]=="" ? $matches[4] : $matches[3]; - if (!preg_match("/^\w+:/", $src)) { - $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreImageLocation").$src; - } - $alt = $matches[2]; - $title = isset($matches[7]) ? $matches[7] : $matches[2]; - $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); - $output = "<img src=\"".$this->encodeURLAttribute($src)."\""; - if (!is_string_empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\""; - if (!is_string_empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; - $output .= $attr; - $output .= $this->empty_element_suffix; - return $this->hashPart($output); - } - - // Handle lists, task list - public function _processListItems_callback($matches) { - $attr = ""; - $item = $matches[4]; - $leadingLine = $matches[1]; - $tailingLine = $matches[5]; - if ($leadingLine || $tailingLine || preg_match('/\n{2,}/', $item)) - { - $item = $matches[2].str_repeat(' ', strlen($matches[3])).$item; - $item = $this->runBlockGamut($this->outdent($item)."\n"); - } else { - $item = $this->doLists($this->outdent($item)); - $item = $this->formParagraphs($item, false); - $token = substr($item, 0, 4); - if ($token=="[ ] " || $token=="[x] ") { - $attr = " class=\"task-list-item\""; - $item = ($token=='[ ] ' ? "<input type=\"checkbox\" disabled=\"disabled\" /> " : - "<input type=\"checkbox\" disabled=\"disabled\" checked=\"checked\" /> ").substr($item, 4); - } - } - return "<li$attr>".$item."</li>\n"; - } - - // Handle blockquotes, CommonMark compatible - public function doBlockQuotes($text) { - return preg_replace_callback("/((?>^[ ]*>[ ]?.+\n(.+\n)*)+)/m", array($this, "_doBlockQuotes_callback"), $text); - } - - // Handle notice blocks - public function doNoticeBlocks($text) { - return preg_replace_callback("/((?>^[ ]*!(?!\[)[ ]?.+\n(.+\n)*)+)/m", array($this, "_doNoticeBlocks_callback"), $text); - } - - // Handle notice blocks over multiple lines - public function _doNoticeBlocks_callback($matches) { - $name = $attributes = $attr = ""; - $text = preg_replace("/^[ ]*![ ]?/m", "", $matches[1]); - if (preg_match("/^[ ]*".$this->id_class_attr_catch_re."[ ]*\n([\S\s]*)$/m", $text, $parts)) { - $name = $this->getBlockName("", $parts[1]); - $text = $parts[2]; - $attributes = $parts[1]; - $attr = $this->doExtraAttributes("div", $parts[1]); - } elseif ($this->noticeLevel==0) { - $level = strspn(str_replace(array("![", " "), "", $matches[1]), "!"); - $attr = " class=\"notice$level\""; - } - if (!is_string_empty($text)) { - ++$this->noticeLevel; - $output = $this->page->parseContentElement($name, "[--notice--]", $attributes, "notice"); - if (!is_null($output) && preg_match("/^(.+)(\[--notice--\])(.+)$/s", $output, $parts)) { - $output = $parts[1].$this->runBlockGamut($text).$parts[3]; - } else { - $output = "<div$attr>\n".$this->runBlockGamut($text)."\n</div>"; - } - --$this->noticeLevel; - } else { - $output = "<div$attr></div>"; - } - return "\n".$this->hashBlock($output)."\n\n"; - } - - // Handle footnotes links, normalise ids and links - public function doFootnotesLinks($text) { - if (!is_null($this->footnotes_assembled)) { - $callbackId = function ($matches) { - $id = str_replace(":", "-", $matches[2]); - return "<$matches[1] id=\"$id\" $matches[3]>"; - }; - $text = preg_replace_callback("/<(li|sup) id=\"(fn:\d+)\"(.*?)>/", $callbackId, $text); - $text = preg_replace_callback("/<(li|sup) id=\"(fnref\d*:\d+)\"(.*?)>/", $callbackId, $text); - $callbackHref = function ($matches) { - $href = $this->page->base.$this->page->location.str_replace(":", "-", $matches[2]); - return "<$matches[1] href=\"$href\" $matches[3]>"; - }; - $text = preg_replace_callback("/<(a) href=\"(#fn:\d+)\"(.*?)>/", $callbackHref, $text); - $text = preg_replace_callback("/<(a) href=\"(#fnref\d*:\d+)\"(.*?)>/", $callbackHref, $text); - } - return $text; - } - - // Return suitable name for code block or notice block - public function getBlockName($language, $attributes) { - if (!is_string_empty($language)) { - $name = ltrim($language, "."); - } else { - $name = ""; - foreach (explode(" ", $attributes) as $token) { - if (substru($token, 0, 1)==".") { $name = substru($token, 1); break; } - } - } - return $name; - } - - // Return unique id attribute - public function getIdAttribute($text) { - $attr = ""; - $text = $this->yellow->lookup->normaliseName($text, true, false, true); - $text = trim(preg_replace("/-+/", "-", $text), "-"); - if (!isset($this->idAttributes[$text])) { - $this->idAttributes[$text] = $text; - $attr = " id=\"$text\""; - } - return $attr; - } -} diff --git a/system/extensions/serve.php b/system/extensions/serve.php @@ -1,61 +0,0 @@ -<?php -// Serve extension, https://github.com/annaesvensson/yellow-serve - -class YellowServe { - const VERSION = "0.8.24"; - public $yellow; // access to API - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - } - - // Handle command - public function onCommand($command, $text) { - switch ($command) { - case "serve": $statusCode = $this->processCommandServe($command, $text); break; - default: $statusCode = 0; - } - return $statusCode; - } - - // Handle command help - public function onCommandHelp() { - return "serve [url]"; - } - - // Process command to start web server - public function processCommandServe($command, $text) { - list($url) = $this->yellow->toolbox->getTextArguments($text); - if (is_string_empty($url)) $url = "http://localhost:8000/"; - list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($url); - if ($scheme=="http" && !is_string_empty($address) && is_string_empty($base)) { - if (!preg_match("/\:\d+$/", $address)) $address .= ":8000"; - if ($this->checkServerSettings("$scheme://$address/")) { - echo "Starting web server. Open a web browser and go to $scheme://$address/\n"; - echo "Press Ctrl+C to quit...\n"; - exec(PHP_BINARY." -S $address yellow.php 2>&1", $outputLines, $returnStatus); - $statusCode = $returnStatus!=0 ? 500 : 200; - if ($statusCode!=200) { - $output = !is_array_empty($outputLines) ? end($outputLines) : "Please check arguments!"; - if (preg_match("/^\[(.*?)\]\s*(.*)$/", $output, $matches)) $output = $matches[2]; - echo "ERROR starting web server: $output\n"; - } - } else { - $statusCode = 400; - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - echo "ERROR starting web server: Please configure `CoreServerUrl: auto` in file '$fileName'!\n"; - } - } else { - $statusCode = 400; - echo "Yellow $command: Invalid arguments\n"; - } - return $statusCode; - } - - // Check server settings - public function checkServerSettings($url) { - return $this->yellow->system->get("coreServerUrl")=="auto" || - $this->yellow->system->get("coreServerUrl")==$url; - } -} diff --git a/system/extensions/stockholm.php b/system/extensions/stockholm.php @@ -1,22 +0,0 @@ -<?php -// Stockholm extension, https://github.com/annaesvensson/yellow-stockholm - -class YellowStockholm { - const VERSION = "0.8.14"; - public $yellow; // access to API - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - } - - // Handle update - public function onUpdate($action) { - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - if ($action=="install") { - $this->yellow->system->save($fileName, array("theme" => "stockholm")); - } elseif ($action=="uninstall" && $this->yellow->system->get("theme")=="stockholm") { - $this->yellow->system->save($fileName, array("theme" => $this->yellow->system->getDifferent("theme"))); - } - } -} diff --git a/system/extensions/update-available.ini b/system/extensions/update-available.ini @@ -44,16 +44,16 @@ system/themes/berlin-opensans-light.woff: berlin-opensans-light.woff, create, up system/themes/berlin-opensans-regular.woff: berlin-opensans-regular.woff, create, update, careful Extension: Blog -Version: 0.8.31 +Version: 0.9.1 Description: Blog for your website. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-blog/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-blog DocumentationLanguage: en, de, sv -Published: 2024-04-01 18:52:01 +Published: 2024-04-04 17:13:35 Status: available -system/extensions/blog.php: blog.php, create, update +system/workers/blog.php: blog.php, create, update system/layouts/blog.html: blog.html, create, update, careful system/layouts/blog-start.html: blog-start.html, create, update, careful content/shared/page-new-blog.md: page-new-blog.md, create, optional @@ -74,27 +74,27 @@ Status: available system/extensions/breadcrumb.php: breadcrumb.php, create, update Extension: Bundle -Version: 0.8.32 +Version: 0.9.1 Description: Bundle website files. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-bundle/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-bundle DocumentationLanguage: en, de, sv -Published: 2024-03-28 14:29:53 +Published: 2024-04-04 16:35:34 Status: available -system/extensions/bundle.php: bundle.php, create, update +system/workers/bundle.php: bundle.php, create, update Extension: Catalan -Version: 0.8.44 +Version: 0.9.1 Description: Catalan language. Translator: Andreu Ferrer Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/catalan.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/catalan -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:23:22 Status: available -system/extensions/catalan.php: catalan.php, create, update +system/workers/catalan.php: catalan.php, create, update Extension: Check Version: 0.8.2 @@ -109,15 +109,15 @@ Status: available system/extensions/check.php: check.php, create, update Extension: Chinese -Version: 0.8.44 +Version: 0.9.1 Description: Chinese language. Translator: Hyson Lee Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/chinese.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/chinese -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:23:33 Status: available -system/extensions/chinese.php: chinese.php, create, update +system/workers/chinese.php: chinese.php, create, update Extension: Contact Version: 0.8.25 @@ -148,16 +148,16 @@ system/themes/copenhagen.css: copenhagen.css, create, update, careful system/themes/copenhagen.png: copenhagen.png, create Extension: Core -Version: 0.8.133 +Version: 0.9.1 Description: Core functionality of your website. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-core/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-core DocumentationLanguage: en, de, sv -Published: 2024-04-03 21:29:08 +Published: 2024-04-04 14:38:12 Status: available -system/extensions/core.php: core.php, create, update +system/workers/core.php: core.php, create, update system/layouts/default.html: default.html, create, update, careful system/layouts/error.html: error.html, create, update, careful system/layouts/header.html: header.html, create, update, careful @@ -166,26 +166,26 @@ system/layouts/navigation.html: navigation.html, create, update, careful system/layouts/pagination.html: pagination.html, create, update, careful Extension: Czech -Version: 0.8.44 +Version: 0.9.1 Description: Czech language. Translator: Ufo Vyhuleny Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/czech.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/czech -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:23:36 Status: available -system/extensions/czech.php: czech.php, create, update +system/workers/czech.php: czech.php, create, update Extension: Danish -Version: 0.8.44 +Version: 0.9.1 Description: Danish language. Translator: David Garcia Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/danish.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/danish -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:23:38 Status: available -system/extensions/danish.php: danish.php, create, update +system/workers/danish.php: danish.php, create, update Extension: Draft Version: 0.8.18 @@ -200,31 +200,31 @@ Status: available system/extensions/draft.php: draft.php, create, update Extension: Dutch -Version: 0.8.44 +Version: 0.9.1 Description: Dutch language. Translator: Robin Vannieuwenhuijse Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/dutch.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/dutch -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:23:42 Status: available -system/extensions/dutch.php: dutch.php, create, update +system/workers/dutch.php: dutch.php, create, update Extension: Edit -Version: 0.8.79 +Version: 0.9.1 Description: Edit your website in a web browser. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-edit/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-edit DocumentationLanguage: en, de, sv -Published: 2024-04-03 19:54:42 +Published: 2024-04-04 14:52:31 Status: available -system/extensions/edit.php: edit.php, create, update -system/extensions/edit.css: edit.css, create, update -system/extensions/edit.js: edit.js, create, update -system/extensions/edit-stack.svg: edit-stack.svg, create, update -system/extensions/edit.woff: edit.woff, delete +system/workers/edit.php: edit.php, create, update +system/workers/edit.css: edit.css, create, update +system/workers/edit.js: edit.js, create, update +system/workers/edit-stack.svg: edit-stack.svg, create, update +system/workers/edit.woff: edit.woff, delete content/shared/page-new-default.md: page-new-default.md, create, optional Extension: Emoji @@ -250,15 +250,15 @@ system/extensions/emoji-extra7-stack.svg: emoji-extra7-stack.svg, create, update system/extensions/emoji-flags-stack.svg: emoji-flags-stack.svg, create, update Extension: English -Version: 0.8.44 +Version: 0.9.1 Description: English language. Translator: Mark Seuffert Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/english.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/english -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:20:39 Status: available -system/extensions/english.php: english.php, create, update +system/workers/english.php: english.php, create, update Extension: Feed Version: 0.8.25 @@ -275,15 +275,15 @@ system/layouts/feed.html: feed.html, create, update, careful content/feed/page.md: page.md, create, optional Extension: French -Version: 0.8.44 +Version: 0.9.1 Description: French language. Translator: Juh Nibreh Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/french.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/french -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:23:46 Status: available -system/extensions/french.php: french.php, create, update +system/workers/french.php: french.php, create, update Extension: Gallery Version: 0.8.19 @@ -304,27 +304,27 @@ system/extensions/gallery-default-skin.svg: gallery-default-skin.svg, create, up system/extensions/gallery-preloader.gif: gallery-preloader.gif, create, update Extension: Generate -Version: 0.8.54 +Version: 0.9.1 Description: Generate a static website. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-generate/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-generate DocumentationLanguage: en, de, sv -Published: 2024-03-29 20:45:42 +Published: 2024-04-04 14:55:02 Status: available -system/extensions/generate.php: generate.php, create, update +system/workers/generate.php: generate.php, create, update Extension: German -Version: 0.8.44 +Version: 0.9.1 Description: German language. Translator: David Fehrmann Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/german.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/german -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:20:49 Status: available -system/extensions/german.php: german.php, create, update +system/workers/german.php: german.php, create, update Extension: Googlecalendar Version: 0.8.18 @@ -384,37 +384,37 @@ media/images/language-en.png: language-en.png, create, optional media/images/language-sv.png: language-sv.png, create, optional Extension: Highlight -Version: 0.8.18 +Version: 0.9.1 Description: Highlight source code. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-highlight/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-highlight DocumentationLanguage: en, de, sv -Published: 2024-04-03 10:13:46 +Published: 2024-04-04 16:36:43 Status: available -system/extensions/highlight.php: highlight.php, create, update -system/extensions/highlight.css: highlight.css, create, update -system/extensions/highlight-cpp.json: highlight-cpp.json, create, update -system/extensions/highlight-css.json: highlight-css.json, create, update -system/extensions/highlight-javascript.json: highlight-javascript.json, create, update -system/extensions/highlight-json.json: highlight-json.json, create, update -system/extensions/highlight-lua.json: highlight-lua.json, create, update -system/extensions/highlight-php.json: highlight-php.json, create, update -system/extensions/highlight-python.json: highlight-python.json, create, update -system/extensions/highlight-xml.json: highlight-xml.json, create, update -system/extensions/highlight-yaml.json: highlight-yaml.json, create, update +system/workers/highlight.php: highlight.php, create, update +system/workers/highlight.css: highlight.css, create, update +system/workers/highlight-cpp.json: highlight-cpp.json, create, update +system/workers/highlight-css.json: highlight-css.json, create, update +system/workers/highlight-javascript.json: highlight-javascript.json, create, update +system/workers/highlight-json.json: highlight-json.json, create, update +system/workers/highlight-lua.json: highlight-lua.json, create, update +system/workers/highlight-php.json: highlight-php.json, create, update +system/workers/highlight-python.json: highlight-python.json, create, update +system/workers/highlight-xml.json: highlight-xml.json, create, update +system/workers/highlight-yaml.json: highlight-yaml.json, create, update Extension: Hungarian -Version: 0.8.44 +Version: 0.9.1 Description: Hungarian language. Translator: Ádám Tuba Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/hungarian.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/hungarian -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:23:50 Status: available -system/extensions/hungarian.php: hungarian.php, create, update +system/workers/hungarian.php: hungarian.php, create, update Extension: Icon Version: 0.8.15 @@ -431,16 +431,16 @@ system/extensions/icon.css: icon.css, create, update system/extensions/icon.woff: icon.woff, create, update Extension: Image -Version: 0.8.20 +Version: 0.9.1 Description: Add images and thumbnails. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-image/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-image DocumentationLanguage: en, de, sv -Published: 2024-04-01 18:49:07 +Published: 2024-04-04 14:56:26 Status: available -system/extensions/image.php: image.php, create, update +system/workers/image.php: image.php, create, update media/images/photo.jpg: photo.jpg, create, optional media/thumbnails/photo-100x40.jpg: photo-100x40.jpg, create, optional @@ -456,26 +456,26 @@ system/extensions/instagram.php: instagram.php, create, update system/extensions/instagram.js: instagram.js, create, update Extension: Italian -Version: 0.8.44 +Version: 0.9.1 Description: Italian language. Translator: Giovanni Salmeri Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/italian.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/italian -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:23:53 Status: available -system/extensions/italian.php: italian.php, create, update +system/workers/italian.php: italian.php, create, update Extension: Japanese -Version: 0.8.44 +Version: 0.9.1 Description: Japanese language. Translator: Yuhko Senuma, Tomonori Ikeda Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/japanese.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/japanese -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:23:56 Status: available -system/extensions/japanese.php: japanese.php, create, update +system/workers/japanese.php: japanese.php, create, update Extension: Karlskrona Version: 0.8.19 @@ -501,39 +501,39 @@ system/themes/karlskrona.css: karlskrona.css, create, update, careful system/themes/karlskrona.png: karlskrona.png, create Extension: Markdown -Version: 0.8.28 +Version: 0.9.1 Description: Text formatting for humans. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-markdown/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-markdown DocumentationLanguage: en, de, sv -Published: 2024-04-03 10:08:24 +Published: 2024-04-04 14:58:34 Status: available -system/extensions/markdown.php: markdown.php, create, update +system/workers/markdown.php: markdown.php, create, update Extension: Meta -Version: 0.8.17 +Version: 0.9.1 Description: Meta data for humans and machines. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-meta/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-meta DocumentationLanguage: en, de, sv -Published: 2023-05-19 00:58:40 +Published: 2024-04-04 20:54:52 Status: available -system/extensions/meta.php: meta.php, create, update +system/workers/meta.php: meta.php, create, update Extension: Norwegian -Version: 0.8.44 +Version: 0.9.1 Description: Norwegian language. Translator: Per Arne Solvik Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/norwegian.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/norwegian -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:23:59 Status: available -system/extensions/norwegian.php: norwegian.php, create, update +system/workers/norwegian.php: norwegian.php, create, update Extension: Paris Version: 0.8.14 @@ -556,26 +556,26 @@ system/themes/paris-opensans-light.woff: paris-opensans-light.woff, create, upda system/themes/paris-opensans-regular.woff: paris-opensans-regular.woff, create, update, careful Extension: Polish -Version: 0.8.44 +Version: 0.9.1 Description: Polish language. Translator: Paweł Klockiewicz, Kanbeq Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/polish.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/polish -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:24:03 Status: available -system/extensions/polish.php: polish.php, create, update +system/workers/polish.php: polish.php, create, update Extension: Portuguese -Version: 0.8.44 +Version: 0.9.1 Description: Portuguese language. Translator: Al Garcia Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/portuguese.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/portuguese -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:24:07 Status: available -system/extensions/portuguese.php: portuguese.php, create, update +system/workers/portuguese.php: portuguese.php, create, update Extension: Previousnext Version: 0.8.19 @@ -602,16 +602,16 @@ Status: available system/extensions/private.php: private.php, create, update Extension: Publish -Version: 0.8.73 +Version: 0.9.1 Description: Make and publish extensions. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-publish/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-publish DocumentationLanguage: en, de, sv -Published: 2024-03-21 22:24:10 +Published: 2024-04-04 15:11:41 Status: available -system/extensions/publish.php: publish.php, create, update +system/workers/publish.php: publish.php, create, update Extension: Readingtime Version: 0.8.22 @@ -626,15 +626,15 @@ Status: available system/extensions/readingtime.php: readingtime.php, create, update Extension: Russian -Version: 0.8.44 +Version: 0.9.1 Description: Russian language. Translator: Сергей Ворон Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/russian.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/russian -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:24:11 Status: available -system/extensions/russian.php: russian.php, create, update +system/workers/russian.php: russian.php, create, update Extension: Search Version: 0.8.30 @@ -651,16 +651,16 @@ system/layouts/search.html: search.html, create, update, careful content/search/page.md: page.md, create, optional Extension: Serve -Version: 0.8.24 +Version: 0.9.1 Description: Built-in web server. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-serve/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-serve DocumentationLanguage: en, de, sv -Published: 2023-05-25 22:35:15 +Published: 2024-04-04 15:00:12 Status: available -system/extensions/serve.php: serve.php, create, update +system/workers/serve.php: serve.php, create, update Extension: Sitemap Version: 0.8.15 @@ -692,38 +692,38 @@ system/extensions/slider.css: slider.css, create, update system/extensions/slider-splide.min.js: slider-splide.min.js, create, update Extension: Slovak -Version: 0.8.44 +Version: 0.9.1 Description: Slovak language. Translator: Ádám Tuba Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/slovak.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/slovak -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:24:13 Status: available -system/extensions/slovak.php: slovak.php, create, update +system/workers/slovak.php: slovak.php, create, update Extension: Spanish -Version: 0.8.44 +Version: 0.9.1 Description: Spanish language. Translator: Al Garcia, David Garcia Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/spanish.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/spanish -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:24:16 Status: available -system/extensions/spanish.php: spanish.php, create, update +system/workers/spanish.php: spanish.php, create, update Extension: Stockholm -Version: 0.8.14 +Version: 0.9.1 Description: Stockholm is a clean theme. Designer: Anna Svensson Tag: default, theme DownloadUrl: https://github.com/annaesvensson/yellow-stockholm/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-stockholm DocumentationLanguage: en, de, sv -Published: 2024-04-20 18:43:38 +Published: 2024-04-04 15:00:52 Status: available -system/extensions/stockholm.php: stockholm.php, create, update +system/workers/stockholm.php: stockholm.php, create, update system/themes/stockholm.css: stockholm.css, create, update, careful system/themes/stockholm.png: stockholm.png, create system/themes/stockholm-opensans-bold.woff: stockholm-opensans-bold.woff, create, update, careful @@ -731,15 +731,15 @@ system/themes/stockholm-opensans-light.woff: stockholm-opensans-light.woff, crea system/themes/stockholm-opensans-regular.woff: stockholm-opensans-regular.woff, create, update, careful Extension: Swedish -Version: 0.8.44 +Version: 0.9.1 Description: Swedish language. Translator: Anna Svensson Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/swedish.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/swedish -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:24:20 Status: available -system/extensions/swedish.php: swedish.php, create, update +system/workers/swedish.php: swedish.php, create, update Extension: Toc Version: 0.8.11 @@ -766,40 +766,41 @@ Status: available system/extensions/traffic.php: traffic.php, create, update Extension: Turkish -Version: 0.8.44 +Version: 0.9.1 Description: Turkish language. Translator: Osman Kars Tag: language DownloadUrl: https://github.com/annaesvensson/yellow-language/raw/main/downloads/turkish.zip DocumentationUrl: https://github.com/annaesvensson/yellow-language/tree/main/translations/turkish -Published: 2024-03-21 00:16:05 +Published: 2024-04-04 15:24:23 Status: available -system/extensions/turkish.php: turkish.php, create, update +system/workers/turkish.php: turkish.php, create, update Extension: Update -Version: 0.8.101 +Version: 0.9.1 Description: Keep your website up to date. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-update/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-update DocumentationLanguage: en, de, sv -Published: 2024-04-01 18:50:26 +Published: 2024-04-04 15:10:35 Status: available -system/extensions/update.php: update.php, create, update +system/workers/update.php: update.php, create, update +system/workers/updatepatch.bin: updatepatch.php, create, additional system/extensions/updatepatch.bin: updatepatch.php, create, additional Extension: Wiki -Version: 0.8.31 +Version: 0.9.1 Description: Wiki for your website. Developer: Anna Svensson Tag: feature DownloadUrl: https://github.com/annaesvensson/yellow-wiki/archive/refs/heads/main.zip DocumentationUrl: https://github.com/annaesvensson/yellow-wiki DocumentationLanguage: en, de, sv -Published: 2024-04-01 18:51:50 +Published: 2024-04-04 17:13:33 Status: available -system/extensions/wiki.php: wiki.php, create, update +system/workers/wiki.php: wiki.php, create, update system/layouts/wiki.html: wiki.html, create, update, careful system/layouts/wiki-start.html: wiki-start.html, create, update, careful content/shared/page-new-wiki.md: page-new-wiki.md, create, optional diff --git a/system/extensions/update-current.ini b/system/extensions/update-current.ini @@ -1,139 +0,0 @@ -# Datenstrom Yellow update settings for installed extensions - -Extension: Core -Version: 0.8.133 -Description: Core functionality of your website. -Developer: Anna Svensson -Tag: feature -DownloadUrl: https://github.com/annaesvensson/yellow-core/archive/refs/heads/main.zip -DocumentationUrl: https://github.com/annaesvensson/yellow-core -DocumentationLanguage: en, de, sv -Published: 2024-04-03 21:29:08 -Status: available -system/extensions/core.php: core.php, create, update -system/layouts/default.html: default.html, create, update, careful -system/layouts/error.html: error.html, create, update, careful -system/layouts/header.html: header.html, create, update, careful -system/layouts/footer.html: footer.html, create, update, careful -system/layouts/navigation.html: navigation.html, create, update, careful -system/layouts/pagination.html: pagination.html, create, update, careful - -Extension: Edit -Version: 0.8.79 -Description: Edit your website in a web browser. -Developer: Anna Svensson -Tag: feature -DownloadUrl: https://github.com/annaesvensson/yellow-edit/archive/refs/heads/main.zip -DocumentationUrl: https://github.com/annaesvensson/yellow-edit -DocumentationLanguage: en, de, sv -Published: 2024-04-03 19:54:42 -Status: available -system/extensions/edit.php: edit.php, create, update -system/extensions/edit.css: edit.css, create, update -system/extensions/edit.js: edit.js, create, update -system/extensions/edit-stack.svg: edit-stack.svg, create, update -system/extensions/edit.woff: edit.woff, delete -content/shared/page-new-default.md: page-new-default.md, create, optional - -Extension: Generate -Version: 0.8.54 -Description: Generate a static website. -Developer: Anna Svensson -Tag: feature -DownloadUrl: https://github.com/annaesvensson/yellow-generate/archive/refs/heads/main.zip -DocumentationUrl: https://github.com/annaesvensson/yellow-generate -DocumentationLanguage: en, de, sv -Published: 2024-03-29 20:45:42 -Status: available -system/extensions/generate.php: generate.php, create, update - -Extension: Image -Version: 0.8.20 -Description: Add images and thumbnails. -Developer: Anna Svensson -Tag: feature -DownloadUrl: https://github.com/annaesvensson/yellow-image/archive/refs/heads/main.zip -DocumentationUrl: https://github.com/annaesvensson/yellow-image -DocumentationLanguage: en, de, sv -Published: 2024-04-01 18:49:07 -Status: available -system/extensions/image.php: image.php, create, update -media/images/photo.jpg: photo.jpg, create, optional -media/thumbnails/photo-100x40.jpg: photo-100x40.jpg, create, optional - -Extension: Install -Version: 0.8.95 -Description: Install a brand new website. -Developer: Anna Svensson -DownloadUrl: https://github.com/annaesvensson/yellow-install/archive/refs/heads/main.zip -DocumentationUrl: https://github.com/annaesvensson/yellow-install -DocumentationLanguage: en, de, sv -Published: 2024-04-01 15:57:41 -Status: unassembled -system/extensions/install.php: install.php, create -system/extensions/install-language.bin: install-language.bin, compress @source/yellow-language/, create -system/extensions/install-wiki.bin: install-wiki.bin, compress @source/yellow-wiki/, create -system/extensions/install-blog.bin: install-blog.bin, compress @source/yellow-blog/, create -system/extensions/yellow-system.ini: yellow-system.ini, create -system/extensions/yellow-user.ini: yellow-user.ini, create -system/extensions/yellow-language.ini: yellow-language.ini, create -content/1-home/page.md: 1-home-page.md, create -content/9-about/page.md: 9-about-page.md, create -content/shared/page-error-404.md: page-error-404.md, create -media/downloads/yellow.pdf: yellow.pdf, create -./yellow.php: yellow.php, create -./robots.txt: robots.txt, create - -Extension: Markdown -Version: 0.8.28 -Description: Text formatting for humans. -Developer: Anna Svensson -Tag: feature -DownloadUrl: https://github.com/annaesvensson/yellow-markdown/archive/refs/heads/main.zip -DocumentationUrl: https://github.com/annaesvensson/yellow-markdown -DocumentationLanguage: en, de, sv -Published: 2024-04-03 10:08:24 -Status: available -system/extensions/markdown.php: markdown.php, create, update - -Extension: Serve -Version: 0.8.24 -Description: Built-in web server. -Developer: Anna Svensson -Tag: feature -DownloadUrl: https://github.com/annaesvensson/yellow-serve/archive/refs/heads/main.zip -DocumentationUrl: https://github.com/annaesvensson/yellow-serve -DocumentationLanguage: en, de, sv -Published: 2023-05-25 22:35:15 -Status: available -system/extensions/serve.php: serve.php, create, update - -Extension: Stockholm -Version: 0.8.14 -Description: Stockholm is a clean theme. -Designer: Anna Svensson -Tag: default, theme -DownloadUrl: https://github.com/annaesvensson/yellow-stockholm/archive/refs/heads/main.zip -DocumentationUrl: https://github.com/annaesvensson/yellow-stockholm -DocumentationLanguage: en, de, sv -Published: 2024-04-20 18:43:38 -Status: available -system/extensions/stockholm.php: stockholm.php, create, update -system/themes/stockholm.css: stockholm.css, create, update, careful -system/themes/stockholm.png: stockholm.png, create -system/themes/stockholm-opensans-bold.woff: stockholm-opensans-bold.woff, create, update, careful -system/themes/stockholm-opensans-light.woff: stockholm-opensans-light.woff, create, update, careful -system/themes/stockholm-opensans-regular.woff: stockholm-opensans-regular.woff, create, update, careful - -Extension: Update -Version: 0.8.101 -Description: Keep your website up to date. -Developer: Anna Svensson -Tag: feature -DownloadUrl: https://github.com/annaesvensson/yellow-update/archive/refs/heads/main.zip -DocumentationUrl: https://github.com/annaesvensson/yellow-update -DocumentationLanguage: en, de, sv -Published: 2024-04-01 18:50:26 -Status: available -system/extensions/update.php: update.php, create, update -system/extensions/updatepatch.bin: updatepatch.php, create, additional diff --git a/system/extensions/update.php b/system/extensions/update.php @@ -1,941 +0,0 @@ -<?php -// Update extension, https://github.com/annaesvensson/yellow-update - -class YellowUpdate { - const VERSION = "0.8.101"; - const PRIORITY = "2"; - public $yellow; // access to API - public $extensions; // number of extensions - - // Handle initialisation - public function onLoad($yellow) { - $this->yellow = $yellow; - $this->yellow->system->setDefault("updateCurrentRelease", "none"); - $this->yellow->system->setDefault("updateAvailableUrl", "auto"); - $this->yellow->system->setDefault("updateAvailableFile", "update-available.ini"); - $this->yellow->system->setDefault("updateCurrentFile", "update-current.ini"); - $this->yellow->system->setDefault("updateExtensionFile", "extension.ini"); - $this->yellow->system->setDefault("updateEventPending", "none"); - $this->yellow->system->setDefault("updateEventDaily", "0"); - $this->yellow->system->setDefault("updateTrashTimeout", "7776660"); - } - - // Handle update - public function onUpdate($action) { - if ($action=="clean" || $action=="update") { //TODO: remove later, for backwards compatibility - $fileNameOld = $this->yellow->system->get("coreExtensionDirectory")."update-latest.ini"; - if (is_file($fileNameOld) && !$this->yellow->toolbox->deleteFile($fileNameOld)) { - $this->yellow->toolbox->log("error", "Can't delete file '$fileNameOld'!"); - } - } - if ($action=="clean" || $action=="daily") { - $statusCode = 200; - $path = $this->yellow->system->get("coreExtensionDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.download$/", false, false) as $entry) { - if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; - } - if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!"); - $statusCode = 200; - $path = $this->yellow->system->get("coreTrashDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) { - $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout"); - if ($expire<=time() && !$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; - } - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, true) as $entry) { - $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout"); - if ($expire<=time() && !$this->yellow->toolbox->deleteDirectory($entry)) $statusCode = 500; - } - if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!"); - } - } - - // Handle request - public function onRequest($scheme, $address, $base, $location, $fileName) { - return $this->processRequestPending($scheme, $address, $base, $location, $fileName); - } - - // Handle command - public function onCommand($command, $text) { - $statusCode = $this->processCommandPending(); - if ($statusCode==0) { - switch ($command) { - case "about": $statusCode = $this->processCommandAbout($command, $text); break; - case "install": $statusCode = $this->processCommandInstall($command, $text); break; - case "uninstall": $statusCode = $this->processCommandUninstall($command, $text); break; - case "update": $statusCode = $this->processCommandUpdate($command, $text); break; - default: $statusCode = 0; break; - } - } - return $statusCode; - } - - // Handle command help - public function onCommandHelp() { - return array("about [extension]", "install [extension]", "uninstall [extension]", "update [extension]"); - } - - // Handle page content element - public function onParseContentElement($page, $name, $text, $attributes, $type) { - $output = null; - if ($name=="yellow" && $type=="inline") { - if ($text=="about") { - list($dummy, $settingsCurrent) = $this->getExtensionSettings(true); - $output = "Datenstrom Yellow ".YellowCore::RELEASE."<br />\n"; - foreach ($settingsCurrent as $key=>$value) { - $output .= ucfirst($key)." ".$value->get("version")."<br />\n"; - } - } - if ($text=="release") $output = "Datenstrom Yellow ".YellowCore::RELEASE; - if ($text=="log") { - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreWebsiteFile"); - $fileHandle = @fopen($fileName, "rb"); - if ($fileHandle) { - clearstatcache(true, $fileName); - if (flock($fileHandle, LOCK_SH)) { - $dataBufferSize = 1024; - fseek($fileHandle, max(0, filesize($fileName) - $dataBufferSize)); - $dataBuffer = fread($fileHandle, $dataBufferSize); - if (strlenb($dataBuffer)==$dataBufferSize) { - $dataBuffer = ($pos = strposu($dataBuffer, "\n")) ? substru($dataBuffer, $pos+1) : $dataBuffer; - } - flock($fileHandle, LOCK_UN); - } - fclose($fileHandle); - } - $output = str_replace("\n", "<br />\n", htmlspecialchars($dataBuffer)); - } - } - return $output; - } - - // Process command to show current version - public function processCommandAbout($command, $text) { - $statusCode = 200; - $extensions = $this->getExtensionsFromText($text); - if (!is_array_empty($extensions)) { - list($statusCode, $settings) = $this->getExtensionAboutInformation($extensions); - if ($statusCode==200) { - foreach ($settings as $key=>$value) { - echo ucfirst($key)." ".$value->get("version")." - ".$this->getExtensionDescription($key, $value)."\n"; - if ($value->isExisting("documentationUrl")) echo "Read more at ".$value->get("documentationUrl")."\n"; - } - } - if ($statusCode>=400) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n"; - } else { - echo "Datenstrom Yellow ".YellowCore::RELEASE."\n"; - list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true); - foreach ($settingsCurrent as $key=>$value) { - echo ucfirst($key)." ".$value->get("version")."\n"; - } - } - return $statusCode; - } - - // Process command to install extensions - public function processCommandInstall($command, $text) { - $extensions = $this->getExtensionsFromText($text); - if (!is_array_empty($extensions)) { - $this->extensions = 0; - list($statusCode, $settings) = $this->getExtensionInstallInformation($extensions); - if ($statusCode==200) $statusCode = $this->downloadExtensions($settings); - if ($statusCode==200) $statusCode = $this->updateExtensions("install"); - if ($statusCode>=400) echo "ERROR installing files: ".$this->yellow->page->errorMessage."\n"; - echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; - echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed\n"; - } else { - list($statusCode, $settingsAvailable) = $this->getExtensionSettings(false); - foreach ($settingsAvailable as $key=>$value) { - echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n"; - } - if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n"; - } - return $statusCode; - } - - // Process command to uninstall extensions - public function processCommandUninstall($command, $text) { - $extensions = $this->getExtensionsFromText($text); - if (!is_array_empty($extensions)) { - $this->extensions = 0; - list($statusCode, $settings) = $this->getExtensionUninstallInformation($extensions, "core, update"); - if ($statusCode==200) $statusCode = $this->removeExtensions($settings); - if ($statusCode>=400) echo "ERROR uninstalling files: ".$this->yellow->page->errorMessage."\n"; - echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; - echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." uninstalled\n"; - } else { - list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true); - foreach ($settingsCurrent as $key=>$value) { - echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n"; - } - if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n"; - } - return $statusCode; - } - - // Process command to update website - public function processCommandUpdate($command, $text) { - $extensions = $this->getExtensionsFromText($text); - if (!is_array_empty($extensions)) { - list($statusCode, $settings) = $this->getExtensionUpdateInformation($extensions); - if ($statusCode!=200 || !is_array_empty($settings)) { - $this->extensions = 0; - if ($statusCode==200) $statusCode = $this->downloadExtensions($settings); - if ($statusCode==200) $statusCode = $this->updateExtensions("update"); - if ($statusCode>=400) echo "ERROR updating files: ".$this->yellow->page->errorMessage."\n"; - echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; - echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." updated\n"; - } else { - echo "Your website is up to date\n"; - } - } else { - list($statusCode, $settings) = $this->getExtensionUpdateInformation(array("all")); - if (!is_array_empty($settings)) { - foreach ($settings as $key=>$value) { - echo ucfirst($key)." ".$value->get("version")."\n"; - } - echo "Yellow $command: Updates are available. Please type 'php yellow.php update all'.\n"; - } elseif ($statusCode!=200) { - echo "ERROR updating files: ".$this->yellow->page->errorMessage."\n"; - } else { - echo "Your website is up to date\n"; - } - } - return $statusCode; - } - - // Process command for pending events - public function processCommandPending() { - $statusCode = 0; - $this->extensions = 0; - $this->updatePatchPending(); - $this->updateEventPending(); - $statusCode = $this->updateExtensionPending(); - if ($statusCode==303) { - echo "Detected ZIP file".($this->extensions!=1 ? "s" : ""); - echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed. Please run command again.\n"; - } - return $statusCode; - } - - // Process request for pending events - public function processRequestPending($scheme, $address, $base, $location, $fileName) { - $statusCode = 0; - if ($this->yellow->lookup->isContentFile($fileName)) { - $this->updatePatchPending(); - $this->updateEventPending(); - $statusCode = $this->updateExtensionPending(); - if ($statusCode==303) { - $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); - $statusCode = $this->yellow->sendStatus(303, $location); - } - } - return $statusCode; - } - - // Download extensions - public function downloadExtensions($settings) { - $statusCode = 200; - $path = $this->yellow->system->get("coreExtensionDirectory"); - foreach ($settings as $key=>$value) { - $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; - list($statusCode, $fileData) = $this->getExtensionFile($value->get("downloadUrl")); - if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileName.".download", $fileData)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - if ($statusCode!=200) break; - } - if ($statusCode==200) { - foreach ($settings as $key=>$value) { - $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; - if (!$this->yellow->toolbox->renameFile($fileName.".download", $fileName)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - } - } - return $statusCode; - } - - // Update extensions - public function updateExtensions($action) { - $statusCode = 200; - if (function_exists("opcache_reset")) opcache_reset(); - $path = $this->yellow->system->get("coreExtensionDirectory"); - foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { - $statusCode = max($statusCode, $this->updateExtensionArchive($entry, $action)); - 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, $action) { - $statusCode = 200; - $zip = new ZipArchive(); - if ($zip->open($path)===true) { - if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updateExtensionArchive file:$path<br/>\n"; - $pathBase = ""; - if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1]; - $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateExtensionFile")); - $settings = $this->yellow->toolbox->getTextSettings($fileData, ""); - list($extension, $version, $newModified, $oldModified) = $this->getExtensionInformation($settings); - if (!is_string_empty($extension) && !is_string_empty($version)) { - $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action, $settings)); - $paths = $this->getExtensionDirectories($zip, $pathBase); - foreach ($this->getExtensionFileNames($settings) as $fileName) { - list($entry, $flags) = $this->yellow->toolbox->getTextList($settings[$fileName], ",", 2); - if (!$this->yellow->lookup->isContentFile($fileName)) { - $fileNameSource = $pathBase.$entry; - $fileData = $zip->getFromName($fileNameSource); - $lastModified = $this->yellow->toolbox->getFileModified($fileName); - $statusCode = max($statusCode, $this->updateExtensionFile($fileName, $fileData, - $newModified, $oldModified, $lastModified, $flags, $extension)); - } else { - foreach ($this->getExtensionContentRootPages() as $page) { - list($fileNameSource, $fileNameDestination) = $this->getExtensionContentFileNames( - $fileName, $pathBase, $entry, $flags, $paths, $page); - $fileData = $zip->getFromName($fileNameSource); - $lastModified = $this->yellow->toolbox->getFileModified($fileNameDestination); - $statusCode = max($statusCode, $this->updateExtensionFile($fileNameDestination, $fileData, - $newModified, $oldModified, $lastModified, $flags, $extension)); - } - } - } - $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action)); - $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); - ++$this->extensions; - } else { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't detect file '$path'!"); - } - $zip->close(); - } else { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't open file '$path'!"); - } - return $statusCode; - } - - // Update extension from file - public function updateExtensionFile($fileName, $fileData, $newModified, $oldModified, $lastModified, $flags, $extension) { - $statusCode = 200; - $fileName = $this->yellow->lookup->normalisePath($fileName); - if ($this->yellow->lookup->isValidFile($fileName)) { - $create = $update = $delete = false; - if (preg_match("/create/i", $flags) && !is_file($fileName) && !is_string_empty($fileData)) $create = true; - if (preg_match("/update/i", $flags) && is_file($fileName) && !is_string_empty($fileData)) $update = true; - if (preg_match("/delete/i", $flags) && is_file($fileName)) $delete = true; - if (preg_match("/optional/i", $flags) && $this->yellow->extension->isExisting($extension)) $create = $update = $delete = false; - if (preg_match("/careful/i", $flags) && is_file($fileName) && $lastModified!=$oldModified) $update = false; - if ($create) { - if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || - !$this->yellow->toolbox->modifyFile($fileName, $newModified)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - } - if ($update) { - if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory")) || - !$this->yellow->toolbox->createFile($fileName, $fileData) || - !$this->yellow->toolbox->modifyFile($fileName, $newModified)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - } - if ($delete) { - if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); - } - } - if ($this->yellow->system->get("coreDebugMode")>=2) { - $debug = "action:".($create ? "create" : "").($update ? "update" : "").($delete ? "delete" : ""); - if (!$create && !$update && !$delete) $debug = "action:none"; - echo "YellowUpdate::updateExtensionFile file:$fileName $debug<br/>\n"; - } - } - return $statusCode; - } - - // Update pending patches - public function updatePatchPending() { - $fileName = $this->yellow->system->get("coreExtensionDirectory")."updatepatch.bin"; - if (is_file($fileName)) { - if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updatePatchPending file:$fileName<br/>\n"; - if (!$this->yellow->extension->isExisting("updatepatch")) { - require_once($fileName); - $this->yellow->extension->register("updatepatch", "YellowUpdatePatch"); - } - if ($this->yellow->extension->isExisting("updatepatch")) { - $value = $this->yellow->extension->data["updatepatch"]; - if (method_exists($value["object"], "onLoad")) $value["object"]->onLoad($this->yellow); - if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("patch"); - } - unset($this->yellow->extension->data["updatepatch"]); - if (function_exists("opcache_reset")) opcache_reset(); - if (!$this->yellow->toolbox->deleteFile($fileName)) { - $this->yellow->toolbox->log("error", "Can't delete file '$fileName'!"); - } - } - } - - // Update pending events - public function updateEventPending() { - if ($this->yellow->system->get("updateCurrentRelease")!="none") { - if ($this->yellow->system->get("updateCurrentRelease")!=YellowCore::RELEASE) { - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - if (!$this->yellow->system->save($fileName, array("updateCurrentRelease" => YellowCore::RELEASE))) { - $this->yellow->toolbox->log("error", "Can't write file '$fileName'!"); - } else { - list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation(); - $product = "Datenstrom Yellow ".YellowCore::RELEASE; - $this->yellow->toolbox->log("info", "Update $product, PHP ".PHP_VERSION.", $name $version, $os"); - } - } - if ($this->yellow->system->get("updateEventPending")!="none") { - foreach (explode(",", $this->yellow->system->get("updateEventPending")) as $token) { - list($extension, $action) = $this->yellow->toolbox->getTextList($token, "/", 2); - if ($this->yellow->extension->isExisting($extension) && $action!="uninstall") { - $value = $this->yellow->extension->data[$extension]; - if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action); - } - } - $this->updateSystemSettings("all", $action); - $this->updateLanguageSettings("all", $action); - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - if (!$this->yellow->system->save($fileName, array("updateEventPending" => "none"))) { - $this->yellow->toolbox->log("error", "Can't write file '$fileName'!"); - } - } - if ($this->yellow->system->get("updateEventDaily")<=time()) { - foreach ($this->yellow->extension->data as $key=>$value) { - if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("daily"); - } - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - if (!$this->yellow->system->save($fileName, array("updateEventDaily" => $this->getTimestampDaily()))) { - $this->yellow->toolbox->log("error", "Can't write file '$fileName'!"); - } - } - } - } - - // Update pending extensions - public function updateExtensionPending() { - $statusCode = 0; - $path = $this->yellow->system->get("coreExtensionDirectory"); - if (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))) { - $statusCode = $this->updateExtensions("install"); - if ($statusCode==200) $statusCode = 303; - if ($statusCode>=400) { - $this->yellow->toolbox->log("error", $this->yellow->page->errorMessage); - $this->yellow->page->statusCode = 0; - $this->yellow->page->errorMessage = ""; - $statusCode = 303; - } - } - return $statusCode; - } - - // Update extension settings - public function updateExtensionSettings($extension, $action, $settings) { - $statusCode = 200; - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile"); - $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName); - if ($action=="install" || $action=="update") { - $settingsCurrent = $this->yellow->toolbox->getTextSettings($fileData, "extension"); - $settingsCurrent[$extension] = new YellowArray(); - foreach ($settings as $key=>$value) $settingsCurrent[$extension][$key] = $value; - $settingsCurrent->uksort("strnatcasecmp"); - $fileDataNew = ""; - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\#/", $line)) $fileDataNew = $line; - break; - } - foreach ($settingsCurrent as $extension=>$block) { - if (!is_string_empty($fileDataNew)) $fileDataNew .= "\n"; - foreach ($block as $key=>$value) { - $fileDataNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; - } - } - } elseif ($action=="uninstall") { - $fileDataNew = $this->yellow->toolbox->unsetTextSettings($fileData, "extension", $extension); - } - if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - return $statusCode; - } - - // Update system settings - public function updateSystemSettings($extension, $action) { - $statusCode = 200; - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName); - if ($action=="install" || $action=="update") { - $fileDataStart = $fileDataSettings = ""; - $settings = new YellowArray(); - $settings->exchangeArray($this->yellow->system->settingsDefaults->getArrayCopy()); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\#/", $line)) { - if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n"; - continue; - } - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { - $settings[$matches[1]] = $matches[2]; - } - } - } - foreach ($settings as $key=>$value) { - $fileDataSettings .= ucfirst($key).(is_string_empty($value) ? ":\n" : ": $value\n"); - } - $fileDataNew = $fileDataStart.$fileDataSettings; - } elseif ($action=="uninstall") { - if (!is_string_empty($extension)) { - $fileDataNew = ""; - $regex = "/^".ucfirst($extension)."[A-Z]+/"; - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue; - } - $fileDataNew .= $line; - } - } - } - if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - return $statusCode; - } - - // Update language settings - public function updateLanguageSettings($extension, $action) { - $statusCode = 200; - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile"); - $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName); - if ($action=="install" || $action=="update") { - $fileDataStart = $fileDataSettings = $language = ""; - $settings = new YellowArray(); - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\#/", $line)) { - if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n"; - continue; - } - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { - if (lcfirst($matches[1])=="language") { - if (!is_array_empty($settings)) { - if (!is_string_empty($fileDataSettings)) $fileDataSettings .= "\n"; - foreach ($settings as $key=>$value) { - $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; - } - } - $language = $matches[2]; - $settings = new YellowArray(); - $settings["language"] = $language; - $settings["languageLocale"] = "n/a"; - $settings["languageDescription"] = "n/a"; - $settings["languageTranslator"] = "Unknown"; - foreach ($this->yellow->language->settingsDefaults as $key=>$value) { - $require = preg_match("/^([a-z]*)[A-Z]+/", $key, $tokens) ? $tokens[1] : "core"; - if ($require=="language") $require = "core"; - if ($this->yellow->extension->isExisting($require)) { - if ($this->yellow->language->isText($key, $language)) { - $settings[$key] = $this->yellow->language->getText($key, $language); - } else { - $settings[$key] = $this->yellow->language->getText($key, "en"); - } - } - } - } - if (!is_string_empty($language)) { - $settings[$matches[1]] = $matches[2]; - } - } - } - } - if (!is_array_empty($settings)) { - if (!is_string_empty($fileDataSettings)) $fileDataSettings .= "\n"; - foreach ($settings as $key=>$value) { - $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; - } - } - $fileDataNew = $fileDataStart.$fileDataSettings; - } elseif ($action=="uninstall") { - if (!is_string_empty($extension) && ucfirst($extension)!="Language") { - $fileDataNew = ""; - $regex = "/^".ucfirst($extension)."[A-Z]+/"; - foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { - if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { - if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue; - } - $fileDataNew .= $line; - } - } - } - if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - return $statusCode; - } - - // Update extension notification - public function updateExtensionNotification($extension, $action) { - $statusCode = 200; - if ($this->yellow->extension->isExisting($extension) && $action=="uninstall") { - $value = $this->yellow->extension->data[$extension]; - if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action); - } - $updateEventPending = $this->yellow->system->get("updateEventPending"); - if ($updateEventPending=="none") $updateEventPending = ""; - if (!is_string_empty($updateEventPending)) $updateEventPending .= ","; - $updateEventPending .= "$extension/$action"; - $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); - if (!$this->yellow->system->save($fileName, array("updateEventPending" => $updateEventPending))) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); - } - return $statusCode; - } - - // Remove extensions - public function removeExtensions($settings) { - $statusCode = 200; - if (function_exists("opcache_reset")) opcache_reset(); - foreach ($settings as $extension=>$block) { - $statusCode = max($statusCode, $this->removeExtensionArchive($extension, "uninstall", $block)); - } - return $statusCode; - } - - // Remove extension archive - public function removeExtensionArchive($extension, $action, $settings) { - $statusCode = 200; - $fileNames = $this->getExtensionFileNames($settings, true); - if (!is_array_empty($fileNames)) { - $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action)); - foreach ($fileNames as $fileName) { - $statusCode = max($statusCode, $this->removeExtensionFile($fileName)); - } - if ($statusCode==200) { - $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action, $settings)); - $statusCode = max($statusCode, $this->updateSystemSettings($extension, $action)); - $statusCode = max($statusCode, $this->updateLanguageSettings($extension, $action)); - } - $version = $settings->get("version"); - $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); - ++$this->extensions; - } else { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Please delete extension '$extension' manually!"); - } - return $statusCode; - } - - // Remove extension file - public function removeExtensionFile($fileName) { - $statusCode = 200; - $fileName = $this->yellow->lookup->normalisePath($fileName); - if ($this->yellow->lookup->isValidFile($fileName) && is_file($fileName)) { - if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); - } - if ($this->yellow->system->get("coreDebugMode")>=2) { - echo "YellowUpdate::removeExtensionFile file:$fileName action:delete<br/>\n"; - } - } - return $statusCode; - } - - // Return extensions from text, space separated - public function getExtensionsFromText($text) { - return array_unique(array_filter($this->yellow->toolbox->getTextArguments($text), "strlen")); - } - - // Return extension about information - public function getExtensionAboutInformation($extensions) { - $settings = array(); - list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true); - $settingsCurrent["Datenstrom Yellow"] = new YellowArray(); - $settingsCurrent["Datenstrom Yellow"]["version"] = YellowCore::RELEASE; - $settingsCurrent["Datenstrom Yellow"]["description"] = "Datenstrom Yellow is for people who make small websites."; - $settingsCurrent["Datenstrom Yellow"]["documentationUrl"] = "https://datenstrom.se/yellow/"; - foreach ($extensions as $extension) { - $found = false; - if (strtoloweru($extension)=="yellow") $extension = "Datenstrom Yellow"; - foreach ($settingsCurrent as $key=>$value) { - if (strtoloweru($key)==strtoloweru($extension)) { - $settings[$key] = $settingsCurrent[$key]; - $found = true; - break; - } - } - if (!$found) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); - } - } - return array($statusCode, $settings); - } - - // Return extension install information - public function getExtensionInstallInformation($extensions) { - $settings = array(); - list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(true); - list($statusCodeAvailable, $settingsAvailable) = $this->getExtensionSettings(false); - $statusCode = max($statusCodeCurrent, $statusCodeAvailable); - foreach ($extensions as $extension) { - $found = false; - foreach ($settingsAvailable as $key=>$value) { - if (strtoloweru($key)==strtoloweru($extension)) { - if (!$settingsCurrent->isExisting($key)) $settings[$key] = $settingsAvailable[$key]; - $found = true; - break; - } - } - if (!$found) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); - } - } - return array($statusCode, $settings); - } - - // Return extension about information - public function getExtensionUninstallInformation($extensions, $extensionsProtected = "") { - $settings = array(); - list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true); - foreach ($extensions as $extension) { - $found = false; - foreach ($settingsCurrent as $key=>$value) { - if (strtoloweru($key)==strtoloweru($extension)) { - $settings[$key] = $settingsCurrent[$key]; - $found = true; - break; - } - } - if (!$found) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); - } - } - $protected = preg_split("/\s*,\s*/", $extensionsProtected); - foreach ($settings as $key=>$value) { - if (in_array($key, $protected)) unset($settings[$key]); - } - return array($statusCode, $settings); - } - - // Return extension update information - public function getExtensionUpdateInformation($extensions) { - $settings = array(); - list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(true); - list($statusCodeAvailable, $settingsAvailable) = $this->getExtensionSettings(false); - $statusCode = max($statusCodeCurrent, $statusCodeAvailable); - if (in_array("all", $extensions)) { - foreach ($settingsCurrent as $key=>$value) { - if ($settingsAvailable->isExisting($key)) { - $versionCurrent = $settingsCurrent[$key]->get("version"); - $versionAvailable = $settingsAvailable[$key]->get("version"); - if (strnatcasecmp($versionCurrent, $versionAvailable)<0) { - $settings[$key] = $settingsAvailable[$key]; - } - } - } - } else { - foreach ($extensions as $extension) { - $found = false; - foreach ($settingsCurrent as $key=>$value) { - if (strtoloweru($key)==strtoloweru($extension) && $settingsAvailable->isExisting($key)) { - $versionCurrent = $settingsCurrent[$key]->get("version"); - $versionAvailable = $settingsAvailable[$key]->get("version"); - if (strnatcasecmp($versionCurrent, $versionAvailable)<0) { - $settings[$key] = $settingsAvailable[$key]; - } - $found = true; - break; - } - } - if (!$found) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); - } - } - } - return array($statusCode, $settings); - } - - // Return extension settings - public function getExtensionSettings($current) { - $statusCode = 200; - $settings = array(); - if ($current) { - $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateCurrentFile"); - $fileData = $this->yellow->toolbox->readFile($fileNameCurrent); - $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); - foreach ($settings->getArrayCopy() as $key=>$value) { - if (!$this->yellow->extension->isExisting($key)) unset($settings[$key]); - } - foreach ($this->yellow->extension->data as $key=>$value) { - if (!$settings->isExisting($key)) $settings[$key] = new YellowArray(); - $settings[$key]["extension"] = ucfirst($key); - $settings[$key]["version"] = $value["version"]; - } - } else { - $fileNameAvailable = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateAvailableFile"); - $expire = $this->yellow->toolbox->getFileModified($fileNameAvailable) + 60*10; - if ($expire<=time()) { - $url = $this->yellow->system->get("updateAvailableUrl"); - if ($url=="auto") $url = "https://raw.githubusercontent.com/datenstrom/yellow/main/system/extensions/update-available.ini"; - list($statusCode, $fileData) = $this->getExtensionFile($url); - if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileNameAvailable, $fileData)) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't write file '$fileNameAvailable'!"); - } - } - $fileData = $this->yellow->toolbox->readFile($fileNameAvailable); - $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); - } - $settings->uksort("strnatcasecmp"); - return array($statusCode, $settings); - } - - // Return extension information - public function getExtensionInformation($settings) { - $extension = lcfirst($settings->get("extension")); - $version = $settings->get("version"); - $newModified = strtotime($settings->get("published")); - $oldModified = 0; - $invalid = false; - foreach ($settings as $key=>$value) { - if (strposu($key, "/")) { - $fileName = $this->yellow->lookup->normalisePath($key); - if (!$this->yellow->lookup->isValidFile($fileName)) $invalid = true; - if ($oldModified==0) $oldModified = $this->yellow->toolbox->getFileModified($fileName); - } - } - if ($invalid) $extension = $version = ""; - return array($extension, $version, $newModified, $oldModified); - } - - // Return extension directories - public function getExtensionDirectories($zip, $pathBase) { - $paths = array(); - for ($index=0; $index<$zip->numFiles; ++$index) { - $entry = substru($zip->getNameIndex($index), strlenu($pathBase)); - if (preg_match("#^(.*\/).*?$#", $entry, $matches)) { - array_push($paths, $matches[1]); - } - } - return array_unique($paths); - } - - // Return extension file names - public function getExtensionFileNames($settings, $reverse = false) { - $fileNames = array(); - foreach ($settings as $key=>$value) { - if (strposu($key, "/")) array_push($fileNames, $key); - } - if ($reverse) $fileNames = array_reverse($fileNames); - return $fileNames; - } - - // Return extension root pages for content files - public function getExtensionContentRootPages() { - return $this->yellow->content->scanLocation(""); - } - - // Return extension files names for content files - public function getExtensionContentFileNames($fileName, $pathBase, $entry, $flags, $paths, $page) { - if (preg_match("/multi-language/i", $flags)) { - $pathMultiLanguage = ""; - $languagesWanted = array($page->get("language"), "en"); - foreach ($languagesWanted as $language) { - foreach ($paths as $path) { - if ($this->yellow->lookup->normaliseToken(rtrim($path, "/"))==$language) { - $pathMultiLanguage = $path; - break; - } - } - if (!is_string_empty($pathMultiLanguage)) break; - } - $fileNameSource = $pathBase.$pathMultiLanguage.$entry; - } else { - $fileNameSource = $pathBase.$entry; - } - if ($this->yellow->system->get("coreMultiLanguageMode")) { - $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); - $fileNameDestination = $page->fileName.substru($fileName, $contentDirectoryLength); - } else { - $fileNameDestination = $fileName; - } - return array($fileNameSource, $fileNameDestination); - } - - // Return extension description including responsible developer/designer/translator - public function getExtensionDescription($key, $value) { - $description = $responsible = ""; - if ($value->isExisting("description")) $description = $value->get("description"); - if ($value->isExisting("developer")) $responsible = "Developed by ".$value["developer"]."."; - if ($value->isExisting("designer")) $responsible = "Designed by ".$value["designer"]."."; - if ($value->isExisting("translator")) $responsible = "Translated by ".$value["translator"]."."; - if (is_string_empty($description)) $description = "No description available."; - return "$description $responsible"; - } - - // Return extension file - public function getExtensionFile($url) { - $curlHandle = curl_init(); - curl_setopt($curlHandle, CURLOPT_URL, $this->getExtensionDownloadUrl($url)); - curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowUpdate/".YellowUpdate::VERSION."; SoftwareUpdater)"); - curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); - $fileData = curl_exec($curlHandle); - $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); - $redirectUrl = ($statusCode>=300 && $statusCode<=399) ? curl_getinfo($curlHandle, CURLINFO_REDIRECT_URL) : ""; - curl_close($curlHandle); - if ($statusCode==0) { - $statusCode = 450; - $this->yellow->page->error($statusCode, "Can't connect to the update server!"); - } - if ($statusCode!=450 && $statusCode!=200) { - $statusCode = 500; - $this->yellow->page->error($statusCode, "Can't download file '$url'!"); - } - if ($this->yellow->system->get("coreDebugMode")>=2 && !is_string_empty($redirectUrl)) { - echo "YellowUpdate::getExtensionFile redirected to url:$redirectUrl<br/>\n"; - } - if ($this->yellow->system->get("coreDebugMode")>=2) { - echo "YellowUpdate::getExtensionFile status:$statusCode url:$url<br/>\n"; - } - return array($statusCode, $fileData); - } - - // Return extension download URL, redirect to known URL if necessary - public function getExtensionDownloadUrl($url) { - if (preg_match("#^https://github.com/(.+)/archive/refs/heads/main.zip$#", $url, $matches)) { - $url = "https://codeload.github.com/".$matches[1]."/zip/refs/heads/main"; - } - if (preg_match("#^https://github.com/(.+)/raw/main/(.+)$#", $url, $matches)) { - $url = "https://raw.githubusercontent.com/".$matches[1]."/main/".$matches[2]; - } - return $url; - } - - // Return time of next daily update - public function getTimestampDaily() { - $timeOffset = 0; - foreach (str_split($this->yellow->system->get("sitename")) as $char) { - $timeOffset = ($timeOffset+ord($char)) % 60; - } - return mktime(0, 0, 0) + 60*60*24 + $timeOffset; - } -} diff --git a/system/extensions/yellow-extension.ini b/system/extensions/yellow-extension.ini @@ -0,0 +1,142 @@ +# Datenstrom Yellow extension settings + +Extension: Core +Version: 0.9.1 +Description: Core functionality of your website. +Developer: Anna Svensson +Tag: feature +DownloadUrl: https://github.com/annaesvensson/yellow-core/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-core +DocumentationLanguage: en, de, sv +Published: 2024-04-04 14:38:12 +Status: available +system/workers/core.php: core.php, create, update +system/layouts/default.html: default.html, create, update, careful +system/layouts/error.html: error.html, create, update, careful +system/layouts/header.html: header.html, create, update, careful +system/layouts/footer.html: footer.html, create, update, careful +system/layouts/navigation.html: navigation.html, create, update, careful +system/layouts/pagination.html: pagination.html, create, update, careful + +Extension: Edit +Version: 0.9.1 +Description: Edit your website in a web browser. +Developer: Anna Svensson +Tag: feature +DownloadUrl: https://github.com/annaesvensson/yellow-edit/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-edit +DocumentationLanguage: en, de, sv +Published: 2024-04-04 14:52:31 +Status: available +system/workers/edit.php: edit.php, create, update +system/workers/edit.css: edit.css, create, update +system/workers/edit.js: edit.js, create, update +system/workers/edit-stack.svg: edit-stack.svg, create, update +system/workers/edit.woff: edit.woff, delete +content/shared/page-new-default.md: page-new-default.md, create, optional + +Extension: Generate +Version: 0.9.1 +Description: Generate a static website. +Developer: Anna Svensson +Tag: feature +DownloadUrl: https://github.com/annaesvensson/yellow-generate/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-generate +DocumentationLanguage: en, de, sv +Published: 2024-04-04 14:55:02 +Status: available +system/workers/generate.php: generate.php, create, update + +Extension: Image +Version: 0.9.1 +Description: Add images and thumbnails. +Developer: Anna Svensson +Tag: feature +DownloadUrl: https://github.com/annaesvensson/yellow-image/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-image +DocumentationLanguage: en, de, sv +Published: 2024-04-04 14:56:26 +Status: available +system/workers/image.php: image.php, create, update +media/images/photo.jpg: photo.jpg, create, optional +media/thumbnails/photo-100x40.jpg: photo-100x40.jpg, create, optional + +Extension: Install +Version: 0.9.1 +Description: Install a brand new website. +Developer: Anna Svensson +DownloadUrl: https://github.com/annaesvensson/yellow-install/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-install +DocumentationLanguage: en, de, sv +Published: 2024-04-04 14:49:36 +Status: unassembled +system/workers/install.php: install.php, create +system/workers/install-language.bin: install-language.bin, compress @source/yellow-language/, create +system/workers/install-wiki.bin: install-wiki.bin, compress @source/yellow-wiki/, create +system/workers/install-blog.bin: install-blog.bin, compress @source/yellow-blog/, create +system/extensions/yellow-system.ini: yellow-system.ini, create +system/extensions/yellow-user.ini: yellow-user.ini, create +system/extensions/yellow-language.ini: yellow-language.ini, create +content/1-home/page.md: 1-home-page.md, create +content/9-about/page.md: 9-about-page.md, create +content/shared/page-error-404.md: page-error-404.md, create +media/downloads/yellow-english.pdf: yellow-english.pdf, create +media/downloads/yellow-deutsch.pdf: yellow-deutsch.pdf, create +media/downloads/yellow-svenska.pdf: yellow-svenska.pdf, create +./yellow.php: yellow.php, create +./robots.txt: robots.txt, create + +Extension: Markdown +Version: 0.9.1 +Description: Text formatting for humans. +Developer: Anna Svensson +Tag: feature +DownloadUrl: https://github.com/annaesvensson/yellow-markdown/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-markdown +DocumentationLanguage: en, de, sv +Published: 2024-04-04 14:58:34 +Status: available +system/workers/markdown.php: markdown.php, create, update + +Extension: Serve +Version: 0.9.1 +Description: Built-in web server. +Developer: Anna Svensson +Tag: feature +DownloadUrl: https://github.com/annaesvensson/yellow-serve/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-serve +DocumentationLanguage: en, de, sv +Published: 2024-04-04 15:00:12 +Status: available +system/workers/serve.php: serve.php, create, update + +Extension: Stockholm +Version: 0.9.1 +Description: Stockholm is a clean theme. +Designer: Anna Svensson +Tag: default, theme +DownloadUrl: https://github.com/annaesvensson/yellow-stockholm/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-stockholm +DocumentationLanguage: en, de, sv +Published: 2024-04-04 15:00:52 +Status: available +system/workers/stockholm.php: stockholm.php, create, update +system/themes/stockholm.css: stockholm.css, create, update, careful +system/themes/stockholm.png: stockholm.png, create +system/themes/stockholm-opensans-bold.woff: stockholm-opensans-bold.woff, create, update, careful +system/themes/stockholm-opensans-light.woff: stockholm-opensans-light.woff, create, update, careful +system/themes/stockholm-opensans-regular.woff: stockholm-opensans-regular.woff, create, update, careful + +Extension: Update +Version: 0.9.1 +Description: Keep your website up to date. +Developer: Anna Svensson +Tag: feature +DownloadUrl: https://github.com/annaesvensson/yellow-update/archive/refs/heads/main.zip +DocumentationUrl: https://github.com/annaesvensson/yellow-update +DocumentationLanguage: en, de, sv +Published: 2024-04-04 15:10:35 +Status: available +system/workers/update.php: update.php, create, update +system/workers/updatepatch.bin: updatepatch.php, create, additional +system/extensions/updatepatch.bin: updatepatch.php, create, additional diff --git a/system/workers/core.php b/system/workers/core.php @@ -0,0 +1,3961 @@ +<?php +// Core extension, https://github.com/annaesvensson/yellow-core + +class YellowCore { + const VERSION = "0.9.1"; + const RELEASE = "0.9"; + public $content; // content files + public $media; // media files + public $system; // system settings + public $language; // language settings + public $user; // user settings + public $extension; // extensions + public $lookup; // lookup and normalisation methods + public $toolbox; // toolbox with helper methods + public $page; // current page + + public function __construct() { + $this->content = new YellowContent($this); + $this->media = new YellowMedia($this); + $this->system = new YellowSystem($this); + $this->language = new YellowLanguage($this); + $this->user = new YellowUser($this); + $this->extension = new YellowExtension($this); + $this->lookup = new YellowLookup($this); + $this->toolbox = new YellowToolbox($this); + $this->page = new YellowPage($this); + $this->checkRequirements(); + $this->system->setDefault("sitename", "Localhost"); + $this->system->setDefault("author", "Datenstrom"); + $this->system->setDefault("email", "webmaster"); + $this->system->setDefault("language", "en"); + $this->system->setDefault("layout", "default"); + $this->system->setDefault("theme", "default"); + $this->system->setDefault("parser", "markdown"); + $this->system->setDefault("status", "public"); + $this->system->setDefault("coreServerUrl", "auto"); + $this->system->setDefault("coreTimezone", "UTC"); + $this->system->setDefault("coreContentExtension", ".md"); + $this->system->setDefault("coreContentDefaultFile", "page.md"); + $this->system->setDefault("coreContentErrorFile", "page-error-(.*).md"); + $this->system->setDefault("coreLanguageFile", "yellow-language.ini"); + $this->system->setDefault("coreUserFile", "yellow-user.ini"); + $this->system->setDefault("coreExtensionFile", "yellow-extension.ini"); + $this->system->setDefault("coreWebsiteFile", "yellow-website.log"); + $this->system->setDefault("coreMediaLocation", "/media/"); + $this->system->setDefault("coreDownloadLocation", "/media/downloads/"); + $this->system->setDefault("coreImageLocation", "/media/images/"); + $this->system->setDefault("coreThumbnailLocation", "/media/thumbnails/"); + $this->system->setDefault("coreExtensionLocation", "/media/extensions/"); + $this->system->setDefault("coreMultiLanguageMode", "0"); + $this->system->setDefault("coreDebugMode", "0"); + } + + public function __destruct() { + $this->shutdown(); + } + + // Check requirements + public function checkRequirements() { + if (!version_compare(PHP_VERSION, "7.0", ">=")) $this->exitFatalError("Datenstrom Yellow requires PHP 7.0 or higher!"); + if (!extension_loaded("curl")) $this->exitFatalError("Datenstrom Yellow requires PHP curl extension!"); + if (!extension_loaded("gd")) $this->exitFatalError("Datenstrom Yellow requires PHP gd extension!"); + if (!extension_loaded("mbstring")) $this->exitFatalError("Datenstrom Yellow requires PHP mbstring extension!"); + if (!extension_loaded("zip")) $this->exitFatalError("Datenstrom Yellow requires PHP zip extension!"); + mb_internal_encoding("UTF-8"); + } + + // Handle initialisation + public function load() { + $this->system->load("system/extensions/yellow-system.ini"); + $this->system->set("coreSystemFile", "yellow-system.ini"); + $this->system->set("coreContentDirectory", "content/"); + $this->system->set("coreMediaDirectory", $this->lookup->findMediaDirectory("coreMediaLocation")); + $this->system->set("coreSystemDirectory", "system/"); + $this->system->set("coreCacheDirectory", "system/cache/"); + $this->system->set("coreExtensionDirectory", "system/extensions/"); + $this->system->set("coreLayoutDirectory", "system/layouts/"); + $this->system->set("coreThemeDirectory", "system/themes/"); + $this->system->set("coreTrashDirectory", "system/trash/"); + $this->system->set("coreWorkerDirectory", "system/workers/"); + list($pathInstall, $pathRoot, $pathHome) = $this->lookup->findFileSystemInformation(); + $this->system->set("coreServerInstallDirectory", $pathInstall); + $this->system->set("coreServerRootDirectory", $pathRoot); + $this->system->set("coreServerHomeDirectory", $pathHome); + $this->system->set("coreThemeLocation", "/media/extensions/"); // TODO: remove later, for backwards compatibility + register_shutdown_function(array($this, "processFatalError")); + if ($this->system->get("coreDebugMode")>=1) { + ini_set("display_errors", 1); + error_reporting(E_ALL); + } + date_default_timezone_set($this->system->get("coreTimezone")); + $this->extension->load($this->system->get("coreWorkerDirectory")); + $this->language->load($this->system->get("coreExtensionDirectory").$this->system->get("coreLanguageFile")); + $this->user->load($this->system->get("coreExtensionDirectory").$this->system->get("coreUserFile")); + $this->startup(); + } + + // Handle request from web browser + public function request() { + $statusCode = 0; + $this->toolbox->timerStart($time); + ob_start(); + list($scheme, $address, $base, $location, $fileName) = $this->lookup->getRequestInformation(); + $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, true); + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onRequest")) { + $this->lookup->requestHandler = $key; + $statusCode = $value["object"]->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->isError()) $statusCode = $this->processRequestError(); + ob_end_flush(); + $this->toolbox->timerStop($time); + if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) { + echo "YellowCore::request status:$statusCode time:$time ms<br/>\n"; + } + return $statusCode; + } + + // Process request + public function processRequest($scheme, $address, $base, $location, $fileName, $cacheable) { + $statusCode = 0; + if (is_readable($fileName)) { + if ($this->lookup->isRequestCleanUrl($location)) { + $location = $location.$this->toolbox->getLocationArgumentsCleanUrl(); + $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->sendStatus(303, $location); + } + } else { + if ($this->lookup->isRedirectLocation($location)) { + $location = $this->lookup->getRedirectLocation($location); + $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->sendStatus(301, $location); + } + } + if ($statusCode==0) { + if ($this->lookup->isContentFile($fileName)) { + $statusCode = $this->sendPage($scheme, $address, $base, $location, $fileName, $cacheable, true); + } elseif (!is_string_empty($fileName)) { + $statusCode = $this->sendFile(200, $fileName, $cacheable); + } + if (!is_readable($fileName)) $this->page->error(404); + } + if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) { + echo "YellowCore::processRequest file:$fileName<br/>\n"; + } + return $statusCode; + } + + // Process request with error + public function processRequestError() { + ob_clean(); + $statusCode = $this->sendPage($this->page->scheme, $this->page->address, $this->page->base, + $this->page->location, $this->page->fileName, false, false); + if ($this->system->get("coreDebugMode")>=1) echo "YellowCore::processRequestError file:".$this->page->fileName."<br/>\n"; + return $statusCode; + } + + // Process fatal runtime error + public function processFatalError() { + $error = error_get_last(); + if (!is_null($error) && isset($error["type"]) && ($error["type"]==E_ERROR || $error["type"]==E_PARSE)) { + $fileNameAbsolute = isset($error["file"]) ? $error["file"] : ""; + $fileName = substru($fileNameAbsolute, strlenu($this->system->get("coreServerInstallDirectory"))); + $this->toolbox->log("error", "Can't parse file '$fileName'!"); + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500)); + $troubleshooting = PHP_SAPI!="cli" ? + "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl(); + echo "<br/>\nDatenstrom Yellow stopped with fatal error. Activate the debug mode for more information. $troubleshooting\n"; + } + } + + // Show error message and terminate immediately + public function exitFatalError($errorMessage = "") { + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500)); + $troubleshooting = PHP_SAPI!="cli" ? + "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl(); + echo "$errorMessage $troubleshooting\n"; + exit(1); + } + + // Send page response + public function sendPage($scheme, $address, $base, $location, $fileName, $cacheable, $showSource) { + $rawData = $showSource ? $this->toolbox->readFile($fileName) : $this->page->getRawDataError(); + $statusCode = max($this->page->statusCode, 200); + $errorMessage = $this->page->errorMessage; + $this->page = new YellowPage($this); + $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable); + $this->page->parseMeta($rawData, $statusCode, $errorMessage); + $this->language->set($this->page->get("language")); + $this->page->parseContent(); + $this->page->parsePage(); + $statusCode = $this->sendData($this->page->statusCode, $this->page->headerData, $this->page->outputData); + if ($this->system->get("coreDebugMode")>=1) { + foreach ($this->page->headerData as $key=>$value) { + echo "YellowCore::sendPage $key: $value<br/>\n"; + } + $language = $this->page->get("language"); + $layout = $this->page->get("layout"); + $theme = $this->page->get("theme"); + $parser = $this->page->get("parser"); + echo "YellowCore::sendPage language:$language layout:$layout theme:$theme parser:$parser<br/>\n"; + } + return $statusCode; + } + + // Send data response + public function sendData($statusCode, $headerData, $outputData) { + $lastModifiedFormatted = isset($headerData["Last-Modified"]) ? $headerData["Last-Modified"] : ""; + if ($statusCode==200 && !isset($headerData["Cache-Control"]) && $this->toolbox->isNotModified($lastModifiedFormatted)) { + $statusCode = 304; + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); + } else { + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); + foreach ($headerData as $key=>$value) { + $this->toolbox->sendHttpHeader("$key: $value"); + } + if (!is_null($outputData)) echo $outputData; + } + return $statusCode; + } + + // Send file response + public function sendFile($statusCode, $fileName, $cacheable) { + $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted($this->toolbox->getFileModified($fileName)); + if ($statusCode==200 && $cacheable && $this->toolbox->isNotModified($lastModifiedFormatted)) { + $statusCode = 304; + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); + } else { + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); + if (!$cacheable) $this->toolbox->sendHttpHeader("Cache-Control: no-cache, no-store"); + $this->toolbox->sendHttpHeader("Content-Type: ".$this->toolbox->getMimeContentType($fileName)); + $this->toolbox->sendHttpHeader("Last-Modified: ".$lastModifiedFormatted); + echo $this->toolbox->readFile($fileName); + } + return $statusCode; + } + + // Send status response + public function sendStatus($statusCode, $location = "") { + if (!is_string_empty($location)) $this->page->status($statusCode, $location); + $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode)); + foreach ($this->page->headerData as $key=>$value) { + $this->toolbox->sendHttpHeader("$key: $value"); + } + return $statusCode; + } + + // Handle command from command line + public function command($line = "") { + $statusCode = 0; + $this->toolbox->timerStart($time); + list($command, $text) = $this->lookup->getCommandInformation($line); + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onCommand")) { + $this->lookup->commandHandler = $key; + $statusCode = $value["object"]->onCommand($command, $text); + if ($statusCode!=0) break; + } + } + if ($statusCode==0 && is_string_empty($command)) { + $lines = array(); + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onCommandHelp")) { + $this->lookup->commandHandler = $key; + $output = $value["object"]->onCommandHelp(); + $lines = array_merge($lines, is_array($output) ? $output : array($output)); + } + } + usort($lines, "strnatcasecmp"); + $this->showCommandHelp($lines); + $statusCode = 200; + } + if ($statusCode==0) { + $this->lookup->commandHandler = "core"; + $statusCode = 400; + echo "Yellow $command: Command not found\n"; + } + $this->toolbox->timerStop($time); + if ($this->system->get("coreDebugMode")>=1) { + echo "YellowCore::command status:$statusCode time:$time ms<br/>\n"; + } + return $statusCode<400 ? 0 : 1; + } + + // Show command help + public function showCommandHelp($lines) { + echo "Datenstrom Yellow is for people who make small websites. https://datenstrom.se/yellow/\n"; + $lineCounter = 0; + foreach ($lines as $line) { + echo(++$lineCounter>1 ? " " : "Syntax: ")."php yellow.php $line\n"; + } + } + + // Handle startup + public function startup() { + if (isset($this->extension->data)) { + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onStartup")) $value["object"]->onStartup(); + } + } + } + + // Handle shutdown + public function shutdown() { + if (isset($this->extension->data)) { + foreach ($this->extension->data as $key=>$value) { + if (method_exists($value["object"], "onShutdown")) $value["object"]->onShutdown(); + } + } + } + + // Include layout + public function layout($name, $arguments = null) { + $this->lookup->layoutArguments = func_get_args(); + $this->page->includeLayout($name); + } + + // Return layout arguments + public function getLayoutArguments($sizeMin = 9) { + return array_pad($this->lookup->layoutArguments, $sizeMin, null); + } +} + +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 (!isset($this->pages[$location])) { + $this->pages[$location] = array(); + $scheme = $this->yellow->page->scheme; + $address = $this->yellow->page->address; + $base = $this->yellow->page->base; + if (is_string_empty($location)) { + $rootLocations = $this->yellow->lookup->findContentRootLocations(); + foreach ($rootLocations as $rootLocation=>$rootFileName) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $rootLocation, $rootFileName, false); + $page->parseMeta(""); + array_push($this->pages[$location], $page); + } + } else { + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowContent::scanLocation location:$location<br/>\n"; + $fileNames = $this->yellow->lookup->findChildrenFromContentLocation($location); + foreach ($fileNames as $fileName) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, + $this->yellow->lookup->findContentLocationFromFile($fileName), $fileName, false); + $page->parseMeta($this->yellow->toolbox->readFile($fileName, 4096)); + if (strlenb($page->rawData)<4096) $page->statusCode = 200; + array_push($this->pages[$location], $page); + } + } + } + return $this->pages[$location]; + } + + // Return page from, null if not found + public function find($location, $absoluteLocation = false) { + $found = false; + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base)); + foreach ($this->scanLocation($this->getParentLocation($location)) as $page) { + if ($page->location==$location) { + $found = true; + break; + } + } + return $found ? $page : null; + } + + // Return page collection with all pages + public function index($showInvisible = false, $multiLanguage = false) { + $rootLocation = $multiLanguage ? "" : $this->getRootLocation($this->yellow->page->location); + return $this->getChildrenRecursive($rootLocation, $showInvisible); + } + + // 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 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)) { + $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(); + if ($this->yellow->system->get("coreMultiLanguageMode")) { + 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)) { + $pages->append($page); + } + } + return $pages; + } + + // Return child pages recursively + public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { + --$levelMax; + $pages = new YellowPageCollection($this->yellow); + foreach ($this->scanLocation($location) as $page) { + if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) { + $pages->append($page); + } + if (!$this->yellow->lookup->isFileLocation($page->location) && $levelMax!=0) { + $pages->merge($this->getChildrenRecursive($page->location, $showInvisible, $levelMax)); + } + } + return $pages; + } + + // Return shared pages + public function getShared($location) { + $pages = new YellowPageCollection($this->yellow); + $sharedLocation = $this->getHomeLocation($location)."shared/"; + return $pages->merge($this->scanLocation($sharedLocation)); + } + + // Return root location + public function getRootLocation($location) { + $rootLocation = "root/"; + if ($this->yellow->system->get("coreMultiLanguageMode")) { + foreach ($this->scanLocation("") as $page) { + $token = substru($page->location, 4); + if ($token!="/" && substru($location, 0, strlenu($token))==$token) { + $rootLocation = "root$token"; + break; + } + } + } + return $rootLocation; + } + + // Return home location + public function getHomeLocation($location) { + return substru($this->getRootLocation($location), 4); + } + + // Return parent location + public function getParentLocation($location) { + $parentLocation = ""; + $token = rtrim(substru($this->getRootLocation($location), 4), "/"); + if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { + if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; + } + if (is_string_empty($parentLocation)) $parentLocation = "root$token/"; + return $parentLocation; + } + + // Return top-level location + public function getParentTopLocation($location) { + $parentTopLocation = ""; + $token = rtrim(substru($this->getRootLocation($location), 4), "/"); + if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; + if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/"; + return $parentTopLocation; + } +} + +class YellowMedia { + public $yellow; // access to API + public $files; // scanned files + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->files = array(); + } + + // Scan file system on demand + public function scanLocation($location) { + if (!isset($this->files[$location])) { + $this->files[$location] = array(); + $scheme = $this->yellow->page->scheme; + $address = $this->yellow->page->address; + $base = $this->yellow->system->get("coreServerBase"); + if (is_string_empty($location)) { + $fileNames = array($this->yellow->system->get("coreMediaDirectory")); + } else { + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowMedia::scanLocation location:$location<br/>\n"; + $fileNames = $this->yellow->lookup->findChildrenFromMediaLocation($location); + } + foreach ($fileNames as $fileName) { + $file = new YellowPage($this->yellow); + $file->setRequestInformation($scheme, $address, $base, + $this->yellow->lookup->findMediaLocationFromFile($fileName), $fileName, false); + $file->parseMeta(null); + array_push($this->files[$location], $file); + } + } + return $this->files[$location]; + } + + // Return page with media file information, null if not found + public function find($location, $absoluteLocation = false) { + $found = false; + if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->system->get("coreServerBase"))); + foreach ($this->scanLocation($this->getParentLocation($location)) as $file) { + if ($file->location==$location) { + $found = true; + break; + } + } + return $found ? $file : null; + } + + // Return page collection with all media files + public function index($showInvisible = false, $multiPass = false) { + return $this->getChildrenRecursive("", $showInvisible); + } + + // 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)) { + $files->append($file); + } + } + return $files; + } + + // Return child files recursively + public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) { + --$levelMax; + $files = new YellowPageCollection($this->yellow); + foreach ($this->scanLocation($location) as $file) { + if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) { + $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("coreMediaLocation"); + } + + // Return parent location + public function getParentLocation($location) { + $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); + if (preg_match("#^($token.*\/).+?$#", $location, $matches)) { + if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1]; + } + if (is_string_empty($parentLocation)) $parentLocation = ""; + return $parentLocation; + } + + // Return top-level location + public function getParentTopLocation($location) { + $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/"); + if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1]; + if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/"; + return $parentTopLocation; + } +} + +class YellowSystem { + public $yellow; // access to API + public $modified; // system modification date + public $settings; // system settings + public $settingsDefaults; // system settings defaults + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->settings = new YellowArray(); + $this->settingsDefaults = new YellowArray(); + } + + // Load system settings from file + public function load($fileName) { + $this->modified = $this->yellow->toolbox->getFileModified($fileName); + $fileData = $this->yellow->toolbox->readFile($fileName); + $this->settings = $this->yellow->toolbox->getTextSettings($fileData, ""); + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowSystem::load file:$fileName<br/>\n"; + if ($this->yellow->system->get("coreDebugMode")>=3) { + foreach ($this->settings as $key=>$value) { + echo "YellowSystem::load ".ucfirst($key).":$value<br/>\n"; + } + } + } + + // Save system settings to file + public function save($fileName, $settings) { + $this->modified = time(); + $settingsNew = new YellowArray(); + foreach ($settings as $key=>$value) { + if (!is_string_empty($key) && !is_string_empty($value)) { + $this->set($key, $value); + $settingsNew[$key] = $value; + } + } + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileData = $this->yellow->toolbox->setTextSettings($fileData, "", "", $settingsNew); + return $this->yellow->toolbox->createFile($fileName, $fileData); + } + + // Set default system setting + public function setDefault($key, $value) { + $this->settingsDefaults[$key] = $value; + } + + // Set default system settings + public function setDefaults($lines) { + foreach ($lines as $line) { + if (preg_match("/^\#/", $line)) continue; + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $this->settingsDefaults[$matches[1]] = $matches[2]; + } + } + } + } + + // Set system setting + public function set($key, $value) { + $this->settings[$key] = $value; + } + + // Return system setting + public function get($key) { + if (isset($this->settings[$key])) { + $value = $this->settings[$key]; + } else { + $value = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : ""; + } + return $value; + } + + // Return system setting, HTML encoded + public function getHtml($key) { + return htmlspecialchars($this->get($key)); + } + + // Return different value for system setting + public function getDifferent($key) { + $array = array_diff($this->getAvailable($key), array($this->get($key))); + return reset($array); + } + + // Return available values for system setting + public function getAvailable($key) { + $values = array(); + $valueDefault = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : ""; + if ($key=="email") { + foreach ($this->yellow->user->settings as $userKey=>$userValue) { + array_push($values, $userKey); + } + } elseif ($key=="language") { + foreach ($this->yellow->language->settings as $languageKey=>$languageValue) { + array_push($values, $languageKey); + } + } elseif ($key=="layout") { + $path = $this->yellow->system->get("coreLayoutDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.html$/", true, false, false) as $entry) { + array_push($values, lcfirst(substru($entry, 0, -5))); + } + } elseif ($key=="theme") { + $path = $this->yellow->system->get("coreThemeDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false, false) as $entry) { + array_push($values, lcfirst(substru($entry, 0, -4))); + } + } + return !is_array_empty($values) ? $values : array($valueDefault); + } + public function getValues($key) { return $this->getAvailable($key); } //TODO: remove later, for backwards compatibility + + // Return system settings + public function getSettings($filterStart = "", $filterEnd = "") { + $settings = array(); + if (is_string_empty($filterStart) && is_string_empty($filterEnd)) { + $settings = array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()); + } else { + foreach (array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()) as $key=>$value) { + if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; + if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; + } + } + return $settings; + } + + // Return 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 isset($this->settings[$key]); + } +} + +class YellowLanguage { + public $yellow; // access to API + public $modified; // language modification date + public $settings; // language settings + public $settingsDefaults; // language settings defaults + public $language; // current language + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->settings = new YellowArray(); + $this->settingsDefaults = new YellowArray(); + $this->language = ""; + } + + // Load language settings from file + public function load($fileName) { + $this->modified = $this->yellow->toolbox->getFileModified($fileName); + $fileData = $this->yellow->toolbox->readFile($fileName); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "language"); + foreach ($settings as $language=>$block) { + if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); + foreach ($block as $key=>$value) { + $this->settings[$language][$key] = $value; + } + } + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowLanguage::load file:$fileName<br/>\n"; + foreach ($this->settings->getArrayCopy() as $key=>$value) { + if (!isset($this->settings[$key]["languageDescription"])) { + unset($this->settings[$key]); + } + } + $callback = function ($a, $b) { + return strnatcmp($a["languageDescription"], $b["languageDescription"]); + }; + $this->settings->uasort($callback); + } + + // Set current language + public function set($language) { + $this->language = $language; + } + + // Set default language setting + public function setDefault($key, $value, $language) { + if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); + $this->settings[$language][$key] = $value; + $this->settingsDefaults[$key] = true; + } + + // Set default language settings + public function setDefaults($lines) { + $language = ""; + foreach ($lines as $line) { + if (preg_match("/^\#/", $line)) continue; + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])=="language" && !is_string_empty($matches[2])) { + $language = $matches[2]; + if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); + } + if (!is_string_empty($language) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $this->settings[$language][$matches[1]] = $matches[2]; + $this->settingsDefaults[$matches[1]] = true; + } + } + } + } + + // Set language setting + public function setText($key, $value, $language) { + if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray(); + $this->settings[$language][$key] = $value; + } + + // Return language setting + public function getText($key, $language = "") { + if (is_string_empty($language)) $language = $this->language; + return $this->isText($key, $language) ? $this->settings[$language][$key] : "[$key]"; + } + + // Return language setting, HTML encoded + public function getTextHtml($key, $language = "") { + return htmlspecialchars($this->getText($key, $language)); + } + + // Return text as language specific date, convert to one of the standard formats + public function getDateStandard($text, $language = "") { + if (preg_match("/^\d+$/", $text)) { + $output = $text; + } elseif (preg_match("/^\d+\-\d+$/", $text)) { + $format = $this->getText("coreDateFormatShort", $language); + $output = $this->getDateFormatted(strtotime($text), $format, $language); + } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) { + $format = $this->getText("coreDateFormatMedium", $language); + $output = $this->getDateFormatted(strtotime($text), $format, $language); + } else { + $format = $this->getText("coreDateFormatLong", $language); + $output = $this->getDateFormatted(strtotime($text), $format, $language); + } + return $output; + } + + // Return Unix time as date, relative to today + public function getDateRelative($timestamp, $format, $daysLimit, $language = "") { + $timeDifference = mktime(0, 0, 0) - strtotime(date("Y-m-d", $timestamp)); + $days = abs(intval($timeDifference/86400)); + $key = $timeDifference>=0 ? "coreDatePast" : "coreDateFuture"; + $tokens = preg_split("/\s*,\s*/", $this->getText($key, $language)); + if (count($tokens)>=8) { + if ($days<=$daysLimit || $daysLimit==0) { + if ($days==0) { + $output = $tokens[0]; + } elseif ($days==1) { + $output = $tokens[1]; + } elseif ($days>=2 && $days<=29) { + $output = preg_replace("/@x/i", $days, $tokens[2]); + } elseif ($days>=30 && $days<=59) { + $output = $tokens[3]; + } elseif ($days>=60 && $days<=364) { + $output = preg_replace("/@x/i", intval($days/30), $tokens[4]); + } elseif ($days>=365 && $days<=729) { + $output = $tokens[5]; + } else { + $output = preg_replace("/@x/i", intval($days/365.25), $tokens[6]); + } + } else { + $output = preg_replace("/@x/i", $this->getDateFormatted($timestamp, $format, $language), $tokens[7]); + } + } else { + $output = "[$key]"; + } + return $output; + } + + // Return Unix time as date + public function getDateFormatted($timestamp, $format, $language = "") { + $dateMonthsNominative = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsNominative", $language)); + $dateMonthsGenitive = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsGenitive", $language)); + $dateWeekdays = preg_split("/\s*,\s*/", $this->getText("coreDateWeekdays", $language)); + $monthNominative = $dateMonthsNominative[date("n", $timestamp) - 1]; + $monthGenitive = $dateMonthsGenitive[date("n", $timestamp) - 1]; + $weekday = $dateWeekdays[date("N", $timestamp) - 1]; + $timeZone = $this->yellow->system->get("coreTimezone"); + $timeZoneHelper = new DateTime("now", new DateTimeZone($timeZone)); + $timeZoneOffset = $timeZoneHelper->getOffset(); + $timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600)); + $format = preg_replace("/(?<!\\\)F/", addcslashes($monthNominative, "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)V/", addcslashes($monthGenitive, "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)M/", addcslashes(substru($monthNominative, 0, 3), "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)D/", addcslashes(substru($weekday, 0, 3), "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)l/", addcslashes($weekday, "A..Za..z"), $format); + $format = preg_replace("/(?<!\\\)T/", addcslashes($timeZoneAbbreviation, "A..Za..z"), $format); + return date($format, $timestamp); + } + + // Return language settings + public function getSettings($filterStart = "", $filterEnd = "", $language = "") { + $settings = array(); + if (is_string_empty($language)) $language = $this->language; + if (isset($this->settings[$language])) { + if (is_string_empty($filterStart) && is_string_empty($filterEnd)) { + $settings = $this->settings[$language]->getArrayCopy(); + } else { + foreach ($this->settings[$language] as $key=>$value) { + if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value; + if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value; + } + } + } + return $settings; + } + + // Return language settings modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; + } + + // Check if language setting exists + public function isText($key, $language = "") { + if (is_string_empty($language)) $language = $this->language; + return isset($this->settings[$language]) && isset($this->settings[$language][$key]); + } + + // Check if language exists + public function isExisting($language) { + return isset($this->settings[$language]); + } +} + +class YellowUser { + public $yellow; // access to API + public $modified; // user modification date + public $settings; // user settings + public $email; // current email + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->settings = new YellowArray(); + $this->email = ""; + } + + // Load user settings from file + public function load($fileName) { + $this->modified = $this->yellow->toolbox->getFileModified($fileName); + $fileData = $this->yellow->toolbox->readFile($fileName); + $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "email"); + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUser::load file:$fileName<br/>\n"; + } + + // Save user settings to file + public function save($fileName, $email, $settings) { + $this->modified = time(); + $settingsNew = new YellowArray(); + $settingsNew["email"] = $email; + foreach ($settings as $key=>$value) { + if (!is_string_empty($key) && !is_string_empty($value)) { + $this->setUser($key, $value, $email); + $settingsNew[$key] = $value; + } + } + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileData = $this->yellow->toolbox->setTextSettings($fileData, "email", $email, $settingsNew); + return $this->yellow->toolbox->createFile($fileName, $fileData); + } + + // Remove user settings from file + public function remove($fileName, $email) { + $this->modified = time(); + if (isset($this->settings[$email])) unset($this->settings[$email]); + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileData = $this->yellow->toolbox->unsetTextSettings($fileData, "email", $email); + return $this->yellow->toolbox->createFile($fileName, $fileData); + } + + // Set current email + public function set($email) { + $this->email = $email; + } + + // Set user setting + public function setUser($key, $value, $email) { + if (!isset($this->settings[$email])) $this->settings[$email] = new YellowArray(); + $this->settings[$email][$key] = $value; + } + + // Return user setting + public function getUser($key, $email = "") { + if (is_string_empty($email)) $email = $this->email; + return isset($this->settings[$email]) && isset($this->settings[$email][$key]) ? $this->settings[$email][$key] : ""; + } + + // Return user setting, HTML encoded + public function getUserHtml($key, $email = "") { + return htmlspecialchars($this->getUser($key, $email)); + } + + // Return user settings + public function getSettings($email = "") { + $settings = array(); + if (is_string_empty($email)) $email = $this->email; + if (isset($this->settings[$email])) $settings = $this->settings[$email]->getArrayCopy(); + return $settings; + } + + // Return user settings modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified; + } + + // Check if user setting exists + public function isUser($key, $email = "") { + if (is_string_empty($email)) $email = $this->email; + return isset($this->settings[$email]) && isset($this->settings[$email][$key]); + } + + // Check if user exists + public function isExisting($email) { + return isset($this->settings[$email]); + } +} + +class YellowExtension { + public $yellow; // access to API + public $modified; // extension modification date + public $data; // extension data + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->modified = 0; + $this->data = array(); + } + + // Load extensions + public function load($path) { + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) { + $this->modified = max($this->modified, $this->yellow->toolbox->getFileModified($entry)); + require_once($entry); + $name = $this->yellow->lookup->normaliseName(basename($entry), true, true); + $this->register(lcfirst($name), "Yellow".ucfirst($name)); + if ($this->yellow->system->get("coreDebugMode")>=3) echo "YellowExtension::load file:$entry<br/>\n"; + } + $callback = function ($a, $b) { + return $a["priority"] - $b["priority"]; + }; + uasort($this->data, $callback); + foreach ($this->data as $key=>$value) { + if (method_exists($this->data[$key]["object"], "onLoad")) $this->data[$key]["object"]->onLoad($this->yellow); + } + } + + // Register extension + public function register($key, $class) { + if (!$this->isExisting($key) && class_exists($class)) { + $this->data[$key] = array(); + $this->data[$key]["object"] = $class=="YellowCore" ? new stdClass : new $class; + $this->data[$key]["class"] = $class; + $this->data[$key]["version"] = defined("$class::VERSION") ? $class::VERSION : 0; + $this->data[$key]["priority"] = defined("$class::PRIORITY") ? $class::PRIORITY : count($this->data) + 10; + } + } + + // Return extension + public function get($key) { + return $this->data[$key]["object"]; + } + + // 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($key) { + return isset($this->data[$key]); + } +} + +class YellowLookup { + public $yellow; // access to API + public $requestHandler; // request handler name + public $commandHandler; // command handler name + public $layoutArguments; // layout arguments + + public function __construct($yellow) { + $this->yellow = $yellow; + } + + // Return file system information + public function findFileSystemInformation() { + $pathInstall = substru(__DIR__, 0, 1-strlenu($this->yellow->system->get("coreWorkerDirectory"))); + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreMultiLanguageMode") ? "default/" : ""; + $pathHome = "home/"; + if (!is_string_empty($pathRoot)) { + $firstRoot = ""; + $token = $root = rtrim($pathRoot, "/"); + foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { + if (is_string_empty($firstRoot)) $firstRoot = $token = $entry; + if ($this->normaliseToken($entry)==$root) { + $token = $entry; + break; + } + } + $pathRoot = $this->normaliseToken($token)."/"; + $pathBase .= "$firstRoot/"; + } + if (!is_string_empty($pathHome)) { + $firstHome = ""; + $token = $home = rtrim($pathHome, "/"); + foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { + if (is_string_empty($firstHome)) $firstHome = $token = $entry; + if ($this->normaliseToken($entry)==$home) { + $token = $entry; + break; + } + } + $pathHome = $this->normaliseToken($token)."/"; + } + return array($pathInstall, $pathRoot, $pathHome); + } + + // Return content language + public function findContentLanguage($fileName, $languageDefault) { + $language = $languageDefault; + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); + if (!is_string_empty($pathRoot)) { + $fileName = substru($fileName, strlenu($pathBase)); + if (preg_match("/^(.+?)\//", $fileName, $matches)) { + $name = $this->normaliseToken($matches[1]); + if (strlenu($name)==2) $language = $name; + } + } + return $language; + } + + // Return content root locations + public function findContentRootLocations() { + $rootLocations = array(); + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); + if (!is_string_empty($pathRoot)) { + foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) { + $token = $this->normaliseToken($entry)."/"; + if ($token==$pathRoot) $token = ""; + $rootLocations["root/$token"] = "$pathBase$entry/"; + } + } else { + $rootLocations["root/"] = "$pathBase"; + } + if ($this->yellow->system->get("coreDebugMode")>=3) { + foreach ($rootLocations as $key=>$key) { + echo "YellowLookup::findContentRootLocations $key -> $value<br/>\n"; + } + } + return $rootLocations; + } + + // Return content location from file path + public function findContentLocationFromFile($fileName) { + $invalid = false; + $location = "/"; + $pathBase = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); + $pathHome = $this->yellow->system->get("coreServerHomeDirectory"); + $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); + $fileExtension = $this->yellow->system->get("coreContentExtension"); + if (substru($fileName, 0, strlenu($pathBase))==$pathBase && mb_check_encoding($fileName, "UTF-8")) { + $fileName = substru($fileName, strlenu($pathBase)); + $tokens = explode("/", $fileName); + if (!is_string_empty($pathRoot)) { + $token = $this->normaliseToken($tokens[0])."/"; + if ($token!=$pathRoot) $location .= $token; + array_shift($tokens); + } + for ($i=0; $i<count($tokens)-1; ++$i) { + $token = $this->normaliseToken($tokens[$i])."/"; + if ($i || $token!=$pathHome) $location .= $token; + } + $token = $this->normaliseToken($tokens[$i], $fileExtension); + if ($token!=$fileDefault) { + $location .= $this->normaliseToken($tokens[$i], $fileExtension, true); + } + $extension = ($pos = strrposu($fileName, ".")) ? substru($fileName, $pos) : ""; + if ($extension!=$fileExtension) $invalid = true; + } else { + $invalid = true; + } + if ($this->yellow->system->get("coreDebugMode")>=2) { + $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName"; + echo "YellowLookup::findContentLocationFromFile $debug<br/>\n"; + } + return $invalid ? "" : $location; + } + + // Return file path from content location + public function findFileFromContentLocation($location, $directory = false) { + $found = $invalid = false; + $path = $this->yellow->system->get("coreContentDirectory"); + $pathRoot = $this->yellow->system->get("coreServerRootDirectory"); + $pathHome = $this->yellow->system->get("coreServerHomeDirectory"); + $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); + $fileExtension = $this->yellow->system->get("coreContentExtension"); + $tokens = explode("/", $location); + if ($this->isRootLocation($location)) { + if (!is_string_empty($pathRoot)) { + $token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, "/"); + $path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid); + } + } else { + if (!is_string_empty($pathRoot)) { + if (count($tokens)>2) { + if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathRoot, "/"))) $invalid = true; + $path .= $this->findFileDirectory($path, $tokens[1], "", true, false, $found, $invalid); + if ($found) array_shift($tokens); + } + if (!$found) { + $path .= $this->findFileDirectory($path, rtrim($pathRoot, "/"), "", true, true, $found, $invalid); + } + } + if (count($tokens)>2) { + if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathHome, "/"))) $invalid = true; + for ($i=1; $i<count($tokens)-1; ++$i) { + $path .= $this->findFileDirectory($path, $tokens[$i], "", true, true, $found, $invalid); + } + } else { + $i = 1; + $tokens[0] = rtrim($pathHome, "/"); + $path .= $this->findFileDirectory($path, $tokens[0], "", true, true, $found, $invalid); + } + if (!$directory) { + if (!is_string_empty($tokens[$i])) { + $token = $tokens[$i].$fileExtension; + if ($token==$fileDefault) $invalid = true; + $path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid); + } else { + $path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false); + } + if ($this->yellow->system->get("coreDebugMode")>=2) { + $debug = "$location -> ".($invalid ? "INVALID" : $path); + echo "YellowLookup::findFileFromContentLocation $debug<br/>\n"; + } + } + } + return $invalid ? "" : $path; + } + + // Return children from content location + public function findChildrenFromContentLocation($location) { + $fileNames = array(); + if (!$this->isFileLocation($location)) { + $path = $this->findFileFromContentLocation($location, true); + $fileDefault = $this->yellow->system->get("coreContentDefaultFile"); + $fileExtension = $this->yellow->system->get("coreContentExtension"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) { + $token = $this->findFileDefault($path.$entry, $fileDefault, $fileExtension, false); + array_push($fileNames, $path.$entry."/".$token); + } + if (!$this->isRootLocation($location)) { + $regex = "/^.*\\".$fileExtension."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { + if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue; + array_push($fileNames, $path.$entry); + } + } + } + return $fileNames; + } + + // Return media location from file path + public function findMediaLocationFromFile($fileName) { + $location = ""; + $themeDirectoryLength = strlenu($this->yellow->system->get("coreThemeDirectory")); + $workerDirectoryLength = strlenu($this->yellow->system->get("coreWorkerDirectory")); + $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); + if (substru($fileName, 0, $themeDirectoryLength)==$this->yellow->system->get("coreThemeDirectory")) { + if ($this->isSafeFile($fileName)) { + $location = $this->yellow->system->get("coreExtensionLocation").substru($fileName, $themeDirectoryLength); + } + } elseif (substru($fileName, 0, $workerDirectoryLength)==$this->yellow->system->get("coreWorkerDirectory")) { + if ($this->isSafeFile($fileName)) { + $location = $this->yellow->system->get("coreExtensionLocation").substru($fileName, $workerDirectoryLength); + } + } elseif (substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory")) { + $location = "/".$fileName; + } + return $location; + } + + // Return file path from media location + public function findFileFromMediaLocation($location) { + $fileName = ""; + $extensionLocationLength = strlenu($this->yellow->system->get("coreExtensionLocation")); + $mediaLocationLength = strlenu($this->yellow->system->get("coreMediaLocation")); + if (substru($location, 0, $extensionLocationLength)==$this->yellow->system->get("coreExtensionLocation")) { + if ($this->isSafeFile($location)) { + $fileNameOne = $this->yellow->system->get("coreThemeDirectory").substru($location, $extensionLocationLength); + $fileNameTwo = $this->yellow->system->get("coreWorkerDirectory").substru($location, $extensionLocationLength); + $fileName = is_file($fileNameOne) ? $fileNameOne : $fileNameTwo; + } + } elseif (substru($location, 0, $mediaLocationLength)==$this->yellow->system->get("coreMediaLocation")) { + $fileName = substru($location, 1); + } + return $fileName; + } + + // Return children from media location + public function findChildrenFromMediaLocation($location) { + $fileNames = array(); + if (!$this->isFileLocation($location)) { + $path = $this->findFileFromMediaLocation($location); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, true) as $entry) { + array_push($fileNames, $entry."/"); + } + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, false, true) as $entry) { + array_push($fileNames, $entry); + } + } + return $fileNames; + } + + // Return media directory from a system setting + public function findMediaDirectory($key) { + return substru($key, -8, 8)=="Location" ? $this->findFileFromMediaLocation($this->yellow->system->get($key)) : ""; + } + + // Return file or directory that matches token + public function findFileDirectory($path, $token, $fileExtension, $directory, $default, &$found, &$invalid) { + if ($this->normaliseToken($token, $fileExtension)!=$token) $invalid = true; + if (!$invalid) { + $regex = "/^[\d\-\_\.]*".str_replace("-", ".", $token)."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, $directory, false) as $entry) { + if ($this->normaliseToken($entry, $fileExtension)==$token) { + $token = $entry; + $found = true; + break; + } + } + } + if ($directory) $token .= "/"; + return ($default || $found) ? $token : ""; + } + + // Return default file in directory + public function findFileDefault($path, $fileDefault, $fileExtension, $includePath = true) { + $token = $fileDefault; + if (!is_file($path."/".$fileDefault)) { + $regex = "/^[\d\-\_\.]*($fileDefault)$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) { + if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) { + $token = $entry; + break; + } + } + } + return $includePath ? "$path/$token" : $token; + } + + // Normalise file/directory token + public function normaliseToken($text, $fileExtension = "", $removeExtension = false) { + if (!is_string_empty($fileExtension)) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; + if (preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_empty($matches[1])) $text = $matches[1]; + return preg_replace("/[^\pL\d\-\_]/u", "-", $text).($removeExtension ? "" : $fileExtension); + } + + // Normalise name + public function normaliseName($text, $removePrefix = false, $removeExtension = false, $filterStrict = false) { + if ($removeExtension) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; + if ($removePrefix && preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_empty($matches[1])) $text = $matches[1]; + if ($filterStrict) $text = strtoloweru($text); + return preg_replace("/[^\pL\d\-\_]/u", "-", $text); + } + + // Normalise prefix + public function normalisePrefix($text) { + $prefix = ""; + if (preg_match("/^([\d\-\_\.]*)(.*)$/", $text, $matches)) $prefix = $matches[1]; + if (!is_string_empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-"; + return $prefix; + } + + // Normalise elements and attributes in HTML/SVG data + public function normaliseData($text, $type = "html", $filterStrict = true) { + $output = ""; + $elementsHtml = array( + "a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"); + $elementsSvg = array( + "svg", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "feblend", "fecolormatrix", "fecomponenttransfer", "fecomposite", "feconvolvematrix", "fediffuselighting", "fedisplacementmap", "fedistantlight", "feflood", "fefunca", "fefuncb", "fefuncg", "fefuncr", "fegaussianblur", "femerge", "femergenode", "femorphology", "feoffset", "fepointlight", "fespecularlighting", "fespotlight", "fetile", "feturbulence", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "use", "view", "vkern"); + $attributesHtml = array( + "accept", "action", "align", "allow", "allowfullscreen", "alt", "autocomplete", "autoplay", "background", "bgcolor", "border", "cellpadding", "cellspacing", "charset", "checked", "cite", "class", "clear", "color", "cols", "colspan", "content", "contenteditable", "controls", "coords", "crossorigin", "datetime", "default", "dir", "disabled", "download", "enctype", "face", "for", "frameborder", "headers", "height", "hidden", "high", "href", "hreflang", "id", "integrity", "ismap", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "multiple", "muted", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "poster", "prefix", "preload", "property", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "sandbox", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "target", "title", "type", "usemap", "valign", "value", "width", "xmlns"); + $attributesSvg = array( + "accent-height", "accumulate", "additivive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "datenstrom", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "tabindex", "targetx", "targety", "transform", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xlink:href", "xml:id", "xml:space", "xmlns", "y", "y1", "y2", "z", "zoomandpan"); + $attributesAllowEmptyString = array("alt", "download", "sandbox", "value"); + $elementsSafe = $elementsHtml; + $attributesSafe = $attributesHtml; + if ($type=="svg") { + $elementsSafe = array_merge($elementsHtml, $elementsSvg); + $attributesSafe = array_merge($attributesHtml, $attributesSvg); + } + $offsetBytes = 0; + while (true) { + $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementStart = $elementFound ? $matches[1][0] : ""; + $elementName = $elementFound ? $matches[2][0]: ""; + $elementMiddle = $elementFound ? $matches[3][0]: ""; + $elementEnd = $elementFound ? $matches[4][0]: ""; + $output .= $elementBefore; + if (substrb($elementName, 0, 1)=="!") { + $output .= "<$elementName$elementMiddle>"; + } elseif (in_array(strtolower($elementName), $elementsSafe)) { + $elementAttributes = $this->getTextAttributes($elementMiddle, $attributesAllowEmptyString); + foreach ($elementAttributes as $key=>$value) { + if (!in_array(strtolower($key), $attributesSafe) && !preg_match("/^(aria|data)-/i", $key)) { + unset($elementAttributes[$key]); + } + } + if ($filterStrict) { + $href = isset($elementAttributes["href"]) ? $elementAttributes["href"] : ""; + if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) { + $elementAttributes["href"] = "error-xss-filter"; + } + $href = isset($elementAttributes["xlink:href"]) ? $elementAttributes["xlink:href"] : ""; + if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) { + $elementAttributes["xlink:href"] = "error-xss-filter"; + } + } + $output .= "<$elementStart$elementName"; + foreach ($elementAttributes as $key=>$value) $output .= " $key=\"$value\""; + if (!is_string_empty($elementEnd)) $output .= " "; + $output .= "$elementEnd>"; + } + if (!$elementFound) break; + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + } + return $output; + } + + // Normalise name and email for a single address + public function normaliseAddress($input, $type = "mail", $filterStrict = true) { + $output = ""; + if ($type=="mail") { + if (preg_match("/^(.*?)(\s*)<(.*?)>$/", $input, $matches)) { + $name = $matches[1]; + $email = $matches[3]; + } else { + $name = ""; + $email = $input; + } + $name = preg_replace("/[^\pL\d\-\. ]/u", "", $name); + $name = preg_replace("/\s+/s", " ", $name); + if ($filterStrict && !preg_match("/^[\w\+\-\.\@]+$/", $email)) { + $email = "error-mail-address"; + } + $output = is_string_empty($name) ? "<$email>" : "$name <$email>"; + } + return $output; + } + + // Normalise fields in MIME headers + public function normaliseHeaders($input, $type = "mime", $filterStrict = true) { + $output = ""; + if ($type=="mime") { + $keysMixedEncoding = array("To", "From", "Reply-To", "Cc", "Bcc"); + foreach ($input as $key=>$value) { + $key = ucwords(preg_replace("/[^a-zA-Z\-]/u", "-", $key), "-"); + if (in_array($key, $keysMixedEncoding)) { + $text = "$key: "; + foreach (preg_split("/\s*,\s*/", $value) as $email) { + if (!preg_match("/^(.*?)(\s*)<(.*?)>$/", $email, $matches)) { + $matches[1] = $matches[2] = ""; + $matches[3] = $email; + } + if (!is_string_empty($matches[1]) && !preg_match("/^[\pL\d\-\. ]+$/u", $matches[1])) { + $matches[1] = $matches[2] = ""; + $matches[3] = "error-mail-address"; + } + if ($filterStrict && !preg_match("/^[\w\+\-\.\@]+$/", $matches[3])) { + $matches[3] = "error-mail-address"; + } + if (substru($text, -2, 2)!=": ") $text .= ",\r\n "; + $text = $this->getMimeHeader($text, $matches[1]); + $text = $this->getMimeHeader($text, "$matches[2]<$matches[3]>", false); + } + $text .= "\r\n"; + } else { + $text = $this->getMimeHeader("$key: ", $value)."\r\n"; + } + $output .= $text; + } + } + return $output; + } + + // Normalise relative path tokens + public function normalisePath($text) { + $textFiltered = ""; + $textLength = strlenb($text); + for ($pos=0; $pos<$textLength; ++$pos) { + if ($text[$pos]=="." && ($pos==0 || $text[$pos-1]=="/")) { + while ($text[$pos]==".") ++$pos; + if ($text[$pos]=="/") ++$pos; + --$pos; + continue; + } + $textFiltered .= $text[$pos]; + } + return $textFiltered; + } + + // 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; + } + + // 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; + } + + // Normalise location, make absolute location + public function normaliseLocation($location, $pageLocation, $filterStrict = true) { + if (!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) { + $pageBase = $this->yellow->page->base; + $mediaBase = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreMediaLocation"); + if (!preg_match("/^\#/", $location)) { + if (!preg_match("/^\//", $location)) { + $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location; + } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) { + $location = $pageBase.$location; + } + } else { + $location = $pageBase.$pageLocation.$location; + } + $location = str_replace("/./", "/", $location); + $location = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $location); + } else { + if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter"; + } + return $location; + } + + // Normalise location arguments + public function normaliseArguments($text, $appendSlash = true, $filterStrict = true) { + if ($appendSlash) $text .= "/"; + if ($filterStrict) $text = str_replace(" ", "-", strtoloweru($text)); + $text = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $text); + return str_replace(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text)); + } + + // Normalise URL, make absolute URL + public function normaliseUrl($scheme, $address, $base, $location, $filterStrict = true) { + if (!preg_match("/^\w+:/", $location)) { + $url = "$scheme://$address$base$location"; + } else { + if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter"; + $url = $location; + } + return $url; + } + + // Return URL information + public function getUrlInformation($url) { + $scheme = $address = $base = ""; + if (preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, "/"), $matches)) { + $scheme = $matches[1]; + $address = $matches[2]; + $base = $matches[3]; + } + return array($scheme, $address, $base); + } + + // Return request information + public function getRequestInformation($scheme = "", $address = "", $base = "") { + if (is_string_empty($scheme) && is_string_empty($address) && is_string_empty($base)) { + $url = $this->yellow->system->get("coreServerUrl"); + if ($url=="auto" || $this->isCommandLine()) $url = $this->yellow->toolbox->detectServerUrl(); + list($scheme, $address, $base) = $this->getUrlInformation($url); + $this->yellow->system->set("coreServerScheme", $scheme); + $this->yellow->system->set("coreServerAddress", $address); + $this->yellow->system->set("coreServerBase", $base); + if ($this->yellow->system->get("coreDebugMode")>=3) { + echo "YellowLookup::getRequestInformation $scheme://$address$base<br/>\n"; + } + } + $location = substru($this->yellow->toolbox->detectServerLocation(), strlenu($base)); + $fileName = ""; + if (is_string_empty($fileName)) $fileName = $this->findFileFromMediaLocation($location); + if (is_string_empty($fileName)) $fileName = $this->findFileFromContentLocation($location); + return array($scheme, $address, $base, $location, $fileName); + } + + // Return command information + public function getCommandInformation($line = "") { + if (is_string_empty($line)) { + $line = $this->yellow->toolbox->getTextString(array_slice($this->yellow->toolbox->getServer("argv"), 1)); + if ($this->yellow->system->get("coreDebugMode")>=3) { + echo "YellowLookup::getCommandInformation $line<br/>\n"; + } + } + return $this->yellow->toolbox->getTextList($line, " ", 2); + } + + // Return request handler + public function getRequestHandler() { + return $this->requestHandler; + } + + // Return command handler + public function getCommandHandler() { + return $this->commandHandler; + } + + // Return attributes from text + public function getTextAttributes($text, $attributesAllowEmptyString) { + $tokens = array(); + $posStart = $posQuote = 0; + $textLength = strlenb($text); + for ($pos=0; $pos<$textLength; ++$pos) { + if ($text[$pos]==" " && !$posQuote) { + if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + $posStart = $pos+1; + } + if ($text[$pos]=="=" && !$posQuote) { + if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + array_push($tokens, "="); + $posStart = $pos+1; + } + if ($text[$pos]=="\"") { + if ($posQuote) { + if ($pos>$posQuote) array_push($tokens, substrb($text, $posQuote+1, $pos-$posQuote-1)); + $posQuote = 0; + $posStart = $pos+1; + } else { + if ($pos==$posStart) $posQuote = $pos; + } + } + } + if ($pos>$posStart && !$posQuote) { + array_push($tokens, substrb($text, $posStart, $pos-$posStart)); + } + $attributes = array(); + for ($i=0; $i<count($tokens); ++$i) { + if ($i+2<count($tokens) && $tokens[$i+1]=="=") { + $key = $tokens[$i]; + $value = $tokens[$i+2]; + $i += 2; + } else { + $key = $value = $tokens[$i]; + } + if (!is_string_empty($key) && (!is_string_empty($value) || in_array(strtolower($key), $attributesAllowEmptyString))) { + $attributes[$key] = $value; + } + } + return $attributes; + } + + // Return HTML attributes from generic Markdown attributes + public function getHtmlAttributes($text) { + $htmlAttributes = ""; + $htmlAttributesData = array(); + foreach (explode(" ", $text) as $token) { + if (substru($token, 0, 1)==".") { + if (!isset($htmlAttributesData["class"])) { + $htmlAttributesData["class"] = substru($token, 1); + } else { + $htmlAttributesData["class"] .= " ".substru($token, 1); + } + } + if (substru($token, 0, 1)=="#") $htmlAttributesData["id"] = substru($token, 1); + if (preg_match("/^([\w]+)=(.+)/", $token, $matches)) $htmlAttributesData[$matches[1]] = $matches[2]; + } + foreach ($htmlAttributesData as $key=>$value) { + $htmlAttributes .= " $key=\"".htmlspecialchars($value)."\""; + } + return $htmlAttributes; + } + + // Return MIME header field, encode and fold if necessary + public function getMimeHeader($text, $field, $allowEncode = true) { + if ($allowEncode) { + $encode = preg_match("/[\x7F-\xFF]/", $field); + $fieldPos = 0; + while (true) { + $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0); + $bytesAvailable = max(0, 78-$textPos); + $fragment = substrb($field, $fieldPos); + if ($encode && !is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?="; + if ($bytesAvailable<strlenb($fragment)) { + $bytesHandled = $bytesAvailable; + if (!$encode) { + for ($pos=$bytesHandled;$pos>0;--$pos) { + if ($field[$fieldPos+$pos]==" ") { + $fragment = substrb($field, $fieldPos, $pos); + $bytesHandled = $pos+1; + break; + } + } + if ($pos==0) $encode = true; + } + if ($encode) { + while (true) { + $fragment = substrb($field, $fieldPos, $bytesHandled); + if (!is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?="; + if ($bytesAvailable>=strlenb($fragment) || $bytesHandled==0) break; + --$bytesHandled; + } + } + $text .= $fragment."\r\n "; + $fieldPos += $bytesHandled; + } else { + $text .= $fragment; + break; + } + } + } else { + $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0); + $bytesAvailable = max(0, 78-$textPos); + if ($bytesAvailable<strlenb($field)) { + $text .= "\r\n ".ltrim($field); + } else { + $text .= $field; + } + } + return $text; + } + + // Return directory location + public function getDirectoryLocation($location) { + return ($pos = strrposu($location, "/")) ? substru($location, 0, $pos+1) : "/"; + } + + // Return redirect location + public function getRedirectLocation($location) { + if ($this->isFileLocation($location)) { + $location = "$location/"; + } else { + $languageDefault = $this->yellow->system->get("language"); + $language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->content->getLanguages(), $languageDefault); + $location = "/$language/"; + } + return $location; + } + + // Check if clean URL is requested + public function isRequestCleanUrl($location) { + return isset($_REQUEST["clean-url"]) && substru($location, -1, 1)=="/"; + } + + // Check if location is specifying root + public function isRootLocation($location) { + return substru($location, 0, 1)!="/"; + } + + // 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->findFileFromContentLocation("$location/", true)); + } elseif ($location=="/") { + $redirect = $this->yellow->system->get("coreMultiLanguageMode"); + } + 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 (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true; + } + return $nested; + } + + // Check if location is within shared directory + public function isSharedLocation($location) { + $sharedLocation = $this->yellow->content->getHomeLocation($location)."shared/"; + return substru($location, 0, strlenu($sharedLocation))==$sharedLocation; + } + + // Check if location is within current HTTP request + public function isActiveLocation($location, $currentLocation) { + if ($this->isFileLocation($location)) { + $active = $currentLocation==$location; + } else { + if ($location==$this->yellow->content->getHomeLocation($location)) { + $active = $this->getDirectoryLocation($currentLocation)==$location; + } else { + $active = substru($currentLocation, 0, strlenu($location))==$location; + } + } + return $active; + } + + // Check if URL is a well-known URL scheme + public function isSafeUrl($url) { + return preg_match("/^(http|https|ftp|mailto|tel):/", $url); + } + + // Check if file is a well-known file type + public function isSafeFile($fileName) { + return preg_match("/\.(css|gif|ico|js|jpg|map|png|scss|svg|woff|woff2)$/", $fileName); + } + + // Check if file is valid + public function isValidFile($fileName) { + $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); + $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); + $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); + return strposu($fileName, "/")===false || + substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory") || + substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory") || + substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); + } + + // Check if content file + public function isContentFile($fileName) { + $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); + return substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory"); + } + + // Check if media file + public function isMediaFile($fileName) { + $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory")); + return substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory"); + } + + // Check if system file + public function isSystemFile($fileName) { + $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory")); + return substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory"); + } + + // Check if running at command line + public function isCommandLine() { + return isset($this->commandHandler); + } +} + +class YellowToolbox { + public $yellow; // access to API + + public function __construct($yellow) { + $this->yellow = $yellow; + } + + // Return browser cookie from from current HTTP request + public function getCookie($key) { + return isset($_COOKIE[$key]) ? $_COOKIE[$key] : ""; + } + + // Return server argument from current HTTP request + public function getServer($key) { + return isset($_SERVER[$key]) ? $_SERVER[$key] : ""; + } + + // Return location arguments from current HTTP request + public function getLocationArguments() { + return $this->getServer("LOCATION_ARGUMENTS"); + } + + // Return location arguments from current HTTP request, modify existing arguments + public function getLocationArgumentsNew($key, $value) { + $locationArguments = ""; + $found = false; + $separator = $this->getLocationArgumentsSeparator(); + foreach (explode("/", $this->getServer("LOCATION_ARGUMENTS")) as $token) { + if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { + if ($matches[1]==$key) { + $matches[2] = $value; + $found = true; + } + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + if (!is_string_empty($locationArguments)) $locationArguments .= "/"; + $locationArguments .= "$matches[1]:$matches[2]"; + } + } + } + if (!$found && !is_string_empty($key) && !is_string_empty($value)) { + if (!is_string_empty($locationArguments)) $locationArguments .= "/"; + $locationArguments .= "$key:$value"; + } + if (!is_string_empty($locationArguments)) { + $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false, false); + if (!$this->isLocationArgumentsPagination($locationArguments)) $locationArguments .= "/"; + } + return $locationArguments; + } + + // Return location arguments from current HTTP request, convert form parameters + public function getLocationArgumentsCleanUrl() { + $locationArguments = ""; + foreach (array_merge($_GET, $_POST) as $key=>$value) { + if (!is_string_empty($key) && !is_string_empty($value)) { + if (!is_string_empty($locationArguments)) $locationArguments .= "/"; + $key = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $key); + $value = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $value); + $locationArguments .= "$key:$value"; + } + } + if (!is_string_empty($locationArguments)) { + $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false, false); + if (!$this->isLocationArgumentsPagination($locationArguments)) $locationArguments .= "/"; + } + return $locationArguments; + } + + // Return location arguments separator + public function getLocationArgumentsSeparator() { + return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ":" : "="; + } + + // Return 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 420: $text = "Not public"; break; + case 430: $text = "Login failed"; break; + case 434: $text = "Can create"; break; + case 435: $text = "Can restore"; break; + case 450: $text = "Update error"; break; + case 500: $text = "Server error"; break; + case 503: $text = "Service unavailable"; break; + default: $text = "Error $statusCode"; + } + $serverProtocol = $this->getServer("SERVER_PROTOCOL"); + if (!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1"; + return $shortFormat ? $text : "$serverProtocol $statusCode $text"; + } + + // Return MIME content type + public function getMimeContentType($fileName) { + $contentType = ""; + $contentTypes = array( + "css" => "text/css", + "gif" => "image/gif", + "html" => "text/html; charset=utf-8", + "ico" => "image/x-icon", + "js" => "application/javascript", + "json" => "application/json", + "jpg" => "image/jpeg", + "md" => "text/markdown", + "png" => "image/png", + "scss" => "text/x-scss", + "svg" => "image/svg+xml", + "txt" => "text/plain", + "woff" => "application/font-woff", + "woff2" => "application/font-woff2", + "xml" => "text/xml; charset=utf-8"); + $fileType = $this->getFileType($fileName); + if (is_string_empty($fileType)) { + $contentType = $contentTypes["html"]; + } elseif (array_key_exists($fileType, $contentTypes)) { + $contentType = $contentTypes[$fileType]; + } + return $contentType; + } + + // Send HTTP header + public function sendHttpHeader($text) { + if (!headers_sent()) header($text); + } + + // Return files and directories + public function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true) { + return $this->getDirectoryEntriesRecursive($path, $regex, $sort, $directories, $includePath, 1); + } + + // Return files and directories recursively + public function getDirectoryEntriesRecursive($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true, $levelMax = 0) { + --$levelMax; + $entries = array(); + $directoryHandle = @opendir($path); + if ($directoryHandle) { + $path = rtrim($path, "/"); + $directoryEntries = array(); + while (($entry = readdir($directoryHandle))!==false) { + if (substru($entry, 0, 1)==".") continue; + $entry = $this->yellow->lookup->normaliseUnicode($entry); + if (preg_match($regex, $entry)) { + if ($directories) { + if (is_dir("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); + } else { + if (is_file("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry); + } + } + if (is_dir("$path/$entry") && $levelMax!=0) array_push($directoryEntries, "$path/$entry"); + } + if ($sort) { + natcasesort($entries); + natcasesort($directoryEntries); + } + closedir($directoryHandle); + foreach ($directoryEntries as $directoryEntry) { + $entries = array_merge($entries, $this->getDirectoryEntriesRecursive($directoryEntry, $regex, $sort, $directories, $includePath, $levelMax)); + } + } + return $entries; + } + + // Return directory information, modification date and file count + public function getDirectoryInformation($path) { + return $this->getDirectoryInformationRecursive($path, 1); + } + + // Return directory information recursively, modification date and file count + public function getDirectoryInformationRecursive($path, $levelMax = 0) { + --$levelMax; + $modified = $fileCount = 0; + $directoryHandle = @opendir($path); + if ($directoryHandle) { + $path = rtrim($path, "/"); + $directoryEntries = array(); + while (($entry = readdir($directoryHandle))!==false) { + if (substru($entry, 0, 1)==".") continue; + $modified = max($modified, $this->getFileModified("$path/$entry")); + if (is_file("$path/$entry")) ++$fileCount; + if (is_dir("$path/$entry") && $levelMax!=0) array_push($directoryEntries, "$path/$entry"); + } + closedir($directoryHandle); + foreach ($directoryEntries as $directoryEntry) { + list($modifiedBelow, $fileCountBelow) = $this->getDirectoryInformationRecursive($directoryEntry, $levelMax); + $modified = max($modified, $modifiedBelow); + $fileCount += $fileCountBelow; + } + } + return array($modified, $fileCount); + } + + // Read file, empty string if not found + public function readFile($fileName, $sizeMax = 0) { + $fileData = ""; + $fileHandle = @fopen($fileName, "rb"); + if ($fileHandle) { + clearstatcache(true, $fileName); + if (flock($fileHandle, LOCK_SH)) { + $fileSize = $sizeMax ? $sizeMax : filesize($fileName); + if ($fileSize) $fileData = fread($fileHandle, $fileSize); + flock($fileHandle, LOCK_UN); + } + fclose($fileHandle); + } + return $fileData; + } + + // Create file + public function createFile($fileName, $fileData, $mkdir = false) { + $ok = false; + if ($mkdir) { + $path = dirname($fileName); + if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + } + $fileHandle = @fopen($fileName, "cb"); + if ($fileHandle) { + clearstatcache(true, $fileName); + if (flock($fileHandle, LOCK_EX)) { + ftruncate($fileHandle, 0); + fwrite($fileHandle, $fileData); + flock($fileHandle, LOCK_UN); + } + fclose($fileHandle); + $ok = true; + } + return $ok; + } + + // Append file + public function appendFile($fileName, $fileData, $mkdir = false) { + $ok = false; + if ($mkdir) { + $path = dirname($fileName); + if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + } + $fileHandle = @fopen($fileName, "ab"); + if ($fileHandle) { + clearstatcache(true, $fileName); + if (flock($fileHandle, LOCK_EX)) { + fwrite($fileHandle, $fileData); + flock($fileHandle, LOCK_UN); + } + fclose($fileHandle); + $ok = true; + } + return $ok; + } + + // Copy file + public function copyFile($fileNameSource, $fileNameDestination, $mkdir = false) { + clearstatcache(); + if ($mkdir) { + $path = dirname($fileNameDestination); + if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + } + return @copy($fileNameSource, $fileNameDestination); + } + + // Rename file + public function renameFile($fileNameSource, $fileNameDestination, $mkdir = false) { + clearstatcache(); + if ($mkdir) { + $path = dirname($fileNameDestination); + if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true); + } + return @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 (is_string_empty($pathTrash)) { + $ok = @unlink($fileName); + } else { + if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); + $fileNameDestination = $pathTrash; + $fileNameDestination .= pathinfo($fileName, PATHINFO_FILENAME); + $fileNameDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s")); + $fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION); + $ok = @rename($fileName, $fileNameDestination); + } + return $ok; + } + + // Delete directory + public function deleteDirectory($path, $pathTrash = "") { + clearstatcache(); + if (is_string_empty($pathTrash)) { + $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); + $files = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST); + foreach ($files as $file) { + if ($file->getType()=="dir") { + @rmdir($file->getPathname()); + } else { + @unlink($file->getPathname()); + } + } + $ok = @rmdir($path); + } else { + if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true); + $pathDestination = $pathTrash; + $pathDestination .= basename($path); + $pathDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s")); + $ok = @rename($path, $pathDestination); + } + return $ok; + } + + // Set file/directory modification date, Unix time + public function modifyFile($fileName, $modified) { + clearstatcache(true, $fileName); + return @touch($fileName, $modified); + } + + // Return file/directory modification date, Unix time + public function getFileModified($fileName) { + return (is_file($fileName) || is_dir($fileName)) ? filemtime($fileName) : 0; + } + + // Return file/directory deletion date, Unix time + public function getFileDeleted($fileName) { + $deleted = 0; + $text = basename($fileName); + $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text; + if (preg_match("#^(.+)-(\d\d\d\d-\d\d-\d\d)-(\d\d)-(\d\d)-(\d\d)$#", $text, $matches)) { + $deleted = strtotime("$matches[2] $matches[3]:$matches[4]:$matches[5]"); + } + return $deleted; + } + + // Return file type + public function getFileType($fileName) { + return strtoloweru(($pos = strrposu($fileName, ".")) ? substru($fileName, $pos+1) : ""); + } + + // Return file group + public function getFileGroup($fileName, $path) { + $group = "none"; + if (preg_match("#^$path(.+?)\/#", $fileName, $matches)) $group = strtoloweru($matches[1]); + return $group; + } + + // Return number of bytes + public function getNumberBytes($text) { + $bytes = intval($text); + switch (strtoupperu(substru($text, -1))) { + case "G": $bytes *= 1024*1024*1024; break; + case "M": $bytes *= 1024*1024; break; + case "K": $bytes *= 1024; break; + } + return $bytes; + } + + // Return lines from text, including newline + public function getTextLines($text) { + $lines = preg_split("/\n/", $text); + foreach ($lines as &$line) { + $line = $line."\n"; + } + if (is_string_empty($text) || substru($text, -1, 1)=="\n") array_pop($lines); + return $lines; + } + + // Return settings from text + function getTextSettings($text, $blockStart) { + $settings = new YellowArray(); + if (is_string_empty($blockStart)) { + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\#/", $line)) continue; + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $settings[$matches[1]] = $matches[2]; + } + } + } + } else { + $blockKey = ""; + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\#/", $line)) continue; + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { + $blockKey = $matches[2]; + $settings[$blockKey] = new YellowArray(); + } + if (!is_string_empty($blockKey) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $settings[$blockKey][$matches[1]] = $matches[2]; + } + } + } + } + return $settings; + } + + // Set settings in text + function setTextSettings($text, $blockStart, $blockKey, $settings) { + $textNew = ""; + if (is_string_empty($blockStart)) { + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && isset($settings[$matches[1]])) { + $textNew .= "$matches[1]: ".$settings[$matches[1]]."\n"; + unset($settings[$matches[1]]); + continue; + } + } + $textNew .= $line; + } + foreach ($settings as $key=>$value) { + $textNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + } else { + $scan = false; + $textStart = $textMiddle = $textEnd = ""; + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { + $scan = lcfirst($matches[2])==lcfirst($blockKey); + } + } + if (!$scan && is_string_empty($textMiddle)) { + $textStart .= $line; + } elseif ($scan) { + $textMiddle .= $line; + } else { + $textEnd .= $line; + } + } + $textSettings = ""; + foreach ($this->getTextLines($textMiddle) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && isset($settings[$matches[1]])) { + $textSettings .= "$matches[1]: ".$settings[$matches[1]]."\n"; + unset($settings[$matches[1]]); + continue; + } + $textSettings .= $line; + } + } + foreach ($settings as $key=>$value) { + $textSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + if (!is_string_empty($textMiddle)) { + $textMiddle = $textSettings; + if (!is_string_empty($textEnd)) $textMiddle .= "\n"; + } else { + if (!is_string_empty($textStart)) $textEnd .= "\n"; + $textEnd .= $textSettings; + } + $textNew = $textStart.$textMiddle.$textEnd; + } + return $textNew; + } + + // Remove settings from text + function unsetTextSettings($text, $blockStart, $blockKey) { + $textNew = ""; + if (!is_string_empty($blockStart)) { + $scan = false; + $textStart = $textMiddle = $textEnd = ""; + foreach ($this->getTextLines($text) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) { + $scan = lcfirst($matches[2])==lcfirst($blockKey); + } + } + if (!$scan && is_string_empty($textMiddle)) { + $textStart .= $line; + } elseif ($scan) { + $textMiddle .= $line; + } else { + $textEnd .= $line; + } + } + $textNew = rtrim($textStart.$textEnd)."\n"; + } + return $textNew; + } + + // Return array of specific size from text + public function getTextList($text, $separator, $size) { + $tokens = explode($separator, $text, $size); + return array_pad($tokens, $size, ""); + } + + // Return array of variable size from text, space separated + public function getTextArguments($text, $optional = "-", $sizeMin = 9) { + $text = preg_replace("/\s+/s", " ", trim($text)); + $tokens = str_getcsv($text, " ", "\""); + foreach ($tokens as $key=>$value) { + if (is_null($value) || $value==$optional) $tokens[$key] = ""; + } + return array_pad($tokens, $sizeMin, ""); + } + + // Return text from array, space separated + public function getTextString($tokens, $optional = "-") { + $text = ""; + foreach ($tokens as $token) { + if (preg_match("/\s/", $token)) $token = "\"$token\""; + if (is_string_empty($token)) $token = $optional; + if (!is_string_empty($text)) $text .= " "; + $text .= $token; + } + return $text; + } + + // Return number of words in text + public function getTextWords($text) { + $text = preg_replace("/([\p{Han}\p{Hiragana}\p{Katakana}]{3})/u", "$1 ", $text); + $text = preg_replace("/(\pL|\p{N})/u", "x", $text); + return str_word_count($text); + } + + // Return text truncated at word boundary + public function getTextTruncated($text, $lengthMax) { + if (strlenu($text)>$lengthMax-1) { + $text = substru($text, 0, $lengthMax); + $pos = strrposu($text, " "); + $text = substru($text, 0, $pos ? $pos : $lengthMax-1)."…"; + } + return $text; + } + + // Create text description, with or without HTML + public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") { + $output = ""; + $elementsBlock = array("blockquote", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "li", "ol", "p", "pre", "ul"); + $elementsVoid = array("area", "br", "col", "embed", "hr", "img", "input", "param", "source", "wbr"); + if ($lengthMax==0) $lengthMax = strlenu($text); + if ($removeHtml) { + $hiddenLevel = 0; + $offsetBytes = 0; + while (true) { + $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; + $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; + $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; + $elementAttributes = isset($matches[3][0]) ? $matches[3][0] : ""; + $elementEnd = isset($matches[4][0]) ? $matches[4][0] : ""; + if (!is_string_empty($elementBefore) && !$hiddenLevel) { + $rawText = preg_replace("/\s+/s", " ", html_entity_decode($elementBefore, ENT_QUOTES, "UTF-8")); + if (is_string_empty($elementStart) && in_array(strtolower($elementName), $elementsBlock)) $rawText = rtrim($rawText)." "; + if (substru($rawText, 0, 1)==" " && (is_string_empty($output) || substru($output, -1)==" ")) $rawText = ltrim($rawText); + $output .= $this->getTextTruncated($rawText, $lengthMax); + $lengthMax -= strlenu($rawText); + } + if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) { + $output .= $endMarkerText; + $lengthMax = 0; + } + if ($lengthMax<=0 || !$elementFound) break; + if ($hiddenLevel>0 || + preg_match("/aria-hidden=\"true\"/i", $elementAttributes) || + preg_match("/role=\"doc-noteref\"/i", $elementAttributes)) { + if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { + if (is_string_empty($elementStart)) { + ++$hiddenLevel; + } else { + --$hiddenLevel; + } + } + } + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + } + $output = preg_replace("/\s+\…$/s", "…", $output); + } else { + $elementsOpen = array(); + $offsetBytes = 0; + while (true) { + $elementFound = preg_match("/&.*?\;|<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes); + $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes); + $elementRawData = isset($matches[0][0]) ? $matches[0][0] : ""; + $elementStart = isset($matches[1][0]) ? $matches[1][0] : ""; + $elementName = isset($matches[2][0]) ? $matches[2][0] : ""; + $elementEnd = isset($matches[4][0]) ? $matches[4][0] : ""; + if (!is_string_empty($elementBefore)) { + $output .= $this->getTextTruncated($elementBefore, $lengthMax); + $lengthMax -= strlenu($elementBefore); + } + if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) { + $output .= $endMarkerText; + $lengthMax = 0; + } + if ($lengthMax<=0 || !$elementFound) break; + if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) { + if (is_string_empty($elementStart)) { + array_push($elementsOpen, $elementName); + } else { + array_pop($elementsOpen); + } + } + $output .= $elementRawData; + if ($elementRawData[0]=="&") --$lengthMax; + $offsetBytes = $matches[0][1] + strlenb($matches[0][0]); + } + $output = preg_replace("/\s+\…$/s", "…", $output); + for ($i=count($elementsOpen)-1; $i>=0; --$i) { + $output .= "</".$elementsOpen[$i].">"; + } + } + return trim($output); + } + + // Create title from text + public function createTextTitle($text) { + if (preg_match("/^.*\/([\pL\d\-\_]+)/u", $text, $matches)) $text = str_replace("-", " ", 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 (is_string_empty($dataBuffer) && function_exists("random_bytes")) { + $dataBuffer = @random_bytes($dataBufferSize); + } + if (is_string_empty($dataBuffer) && function_exists("openssl_random_pseudo_bytes")) { + $dataBuffer = @openssl_random_pseudo_bytes($dataBufferSize); + } + if (strlenb($dataBuffer)==$dataBufferSize) { + if ($bcryptFormat) { + $salt = substrb(base64_encode($dataBuffer), 0, $length); + $base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + $bcrypt64Chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + $salt = strtr($salt, $base64Chars, $bcrypt64Chars); + } else { + $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 (is_string_empty($salt) || strlenb($hash)!=60) $hash = ""; + break; + case "sha256": $prefix = "$5y$"; + $salt = $this->createSalt(32); + $hash = "$prefix$salt".hash("sha256", $salt.$text); + if (is_string_empty($salt) || strlenb($hash)!=100) $hash = ""; + break; + } + return $hash; + } + + // 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 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) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$key && !is_string_empty($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)) { + $found = false; + $key = lcfirst($key); + $rawDataMiddle = ""; + foreach ($this->getTextLines($parts[2]) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$key) { + $rawDataMiddle .= "$matches[1]: $value\n"; + $found = true; + continue; + } + } + $rawDataMiddle .= $line; + } + if (!$found) $rawDataMiddle .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3]; + } else { + $rawDataNew = $rawData; + } + return $rawDataNew; + } + + // Remove meta data in raw data + public function unsetMetaData($rawData, $key) { + if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) { + $key = lcfirst($key); + $rawDataMiddle = ""; + foreach ($this->getTextLines($parts[2]) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])==$key) continue; + } + $rawDataMiddle .= $line; + } + $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3]; + } else { + $rawDataNew = $rawData; + } + return $rawDataNew; + } + + // Return troubleshooting URL + public function getTroubleshootingUrl() { + return "https://datenstrom.se/yellow/help/troubleshooting"; + } + + // Detect server URL + public function detectServerUrl() { + $scheme = "http"; + if ($this->getServer("REQUEST_SCHEME")=="https" || $this->getServer("HTTPS")=="on") $scheme = "https"; + if ($this->getServer("HTTP_X_FORWARDED_PROTO")=="https") $scheme = "https"; + $address = $this->getServer("SERVER_NAME"); + $port = $this->getServer("SERVER_PORT"); + if ($port!=80 && $port!=443) $address .= ":$port"; + $base = ""; + if (preg_match("/^(.*)\/.*\.php$/", $this->getServer("SCRIPT_NAME"), $matches)) $base = $matches[1]; + return "$scheme://$address$base/"; + } + + // Detect server location + public function detectServerLocation() { + if (isset($_SERVER["REQUEST_URI"])) { + $location = $_SERVER["REQUEST_URI"]; + $location = rawurldecode(($pos = strposu($location, "?")) ? substru($location, 0, $pos) : $location); + $location = $this->yellow->lookup->normalisePath($location); + if (substru($location, 0, 1)!="/") $location = "/".$location; + $separator = $this->getLocationArgumentsSeparator(); + if (preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches)) { + $_SERVER["LOCATION"] = $location = $matches[1]; + $_SERVER["LOCATION_ARGUMENTS"] = $matches[2]; + foreach (explode("/", $matches[2]) as $token) { + if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $matches[1] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[1]); + $matches[2] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[2]); + $_REQUEST[$matches[1]] = $matches[2]; + } + } + } + } else { + $_SERVER["LOCATION"] = $location; + $_SERVER["LOCATION_ARGUMENTS"] = ""; + } + } + return $this->getServer("LOCATION"); + } + + // Detect server sitename + public function detectServerSitename() { + $sitename = "Localhost"; + if (preg_match("#^(www\.)?([\w\-]+)#", $this->getServer("SERVER_NAME"), $matches)) { + $sitename = ucfirst($matches[2]); + } + return $sitename; + } + + // Detect server timezone + public function detectServerTimezone() { + $timezone = ini_get("date.timezone"); + if (is_string_empty($timezone)) { + if (PHP_OS=="Darwin") { + if (preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1]; + } else { + if (preg_match("/^(\S+)\/(\S+)/", $this->readFile("/etc/timezone"), $matches)) $timezone = $matches[1]; + } + } + if (!in_array($timezone, timezone_identifiers_list())) $timezone = "UTC"; + return $timezone; + } + + // Detect server name, version and operating system + public function detectServerInformation() { + $name = "Unknown"; + $version = "x.x.x"; + $os = PHP_OS; + if (preg_match("/^(\S+)\/(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) { + $name = $matches[1]; + $version = $matches[2]; + } elseif (preg_match("/^(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) { + $name = $matches[1]; + } + if (PHP_SAPI=="cli" || PHP_SAPI=="cli-server") { + $name = "Built-in"; + $version = PHP_VERSION; + } + if (PHP_OS=="Darwin") { + $os = "Mac"; + } elseif (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") { + $os = "Windows"; + } + return array($name, $version, $os); + } + + // Detect browser language + public function detectBrowserLanguage($languages, $languageDefault) { + $languageFound = $languageDefault; + foreach (preg_split("/\s*,\s*/", $this->getServer("HTTP_ACCEPT_LANGUAGE")) as $text) { + list($language, $dummy) = $this->getTextList($text, ";", 2); + if (!is_string_empty($language) && in_array($language, $languages)) { + $languageFound = $language; + break; + } + } + return $languageFound; + } + + // Detect terminal width and height + public function detectTerminalInformation() { + $width = $height = 0; + if (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") { + exec("powershell \$Host.UI.RawUI.WindowSize.Width", $outputLines, $returnStatus); + if ($returnStatus==0 && !is_array_empty($outputLines)) { + $width = intval(end($outputLines)); + } + exec("powershell \$Host.UI.RawUI.WindowSize.Height", $outputLines, $returnStatus); + if ($returnStatus==0 && !is_array_empty($outputLines)) { + $height = intval(end($outputLines)); + } + } else { + exec("stty size", $outputLines, $returnStatus); + if ($returnStatus==0 && preg_match("/^(\d+)\s+(\d+)/", implode("\n", $outputLines), $matches)) { + $width = intval($matches[2]); + $height = intval($matches[1]); + } + } + return array($width, $height); + } + + // Detect image width, height, orientation and type for GIF/JPG/PNG/SVG + public function detectImageInformation($fileName, $fileType = "") { + $width = $height = $orientation = 0; + $type = ""; + $fileHandle = @fopen($fileName, "rb"); + if ($fileHandle) { + if (is_string_empty($fileType)) $fileType = $this->getFileType($fileName); + if ($fileType=="gif") { + $dataSignature = fread($fileHandle, 6); + $dataHeader = fread($fileHandle, 7); + if (!feof($fileHandle) && ($dataSignature=="GIF87a" || $dataSignature=="GIF89a")) { + $width = (ord($dataHeader[1])<<8) + ord($dataHeader[0]); + $height = (ord($dataHeader[3])<<8) + ord($dataHeader[2]); + $type = $fileType; + } + } elseif ($fileType=="jpg") { + $dataBufferSizeMax = filesize($fileName); + $dataBufferSize = min($dataBufferSizeMax, 4096); + if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); + $dataSignature = substrb($dataBuffer, 0, 2); + if (!feof($fileHandle) && $dataSignature=="\xff\xd8") { + for ($pos=2; $pos+8<$dataBufferSize; $pos+=$length) { + if ($dataBuffer[$pos]!="\xff") break; + $dataMarker = $dataBuffer[$pos+1]; + if ($dataMarker=="\xe1") { + $orientation = $this->getImageOrientationFromBuffer($dataBuffer, $pos+4, $dataBufferSize); + } + if (($dataMarker>="\xc0" && $dataMarker<="\xc3") || + ($dataMarker>="\xc5" && $dataMarker<="\xc7") || + ($dataMarker>="\xc9" && $dataMarker<="\xcb") || + ($dataMarker>="\xcd" && $dataMarker<="\xcf")) { + $width = (ord($dataBuffer[$pos+7])<<8) + ord($dataBuffer[$pos+8]); + $height = (ord($dataBuffer[$pos+5])<<8) + ord($dataBuffer[$pos+6]); + $type = $fileType; + break; + } + $length = (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]) + 2; + while ($pos+$length+8>=$dataBufferSize) { + if ($dataBufferSize==$dataBufferSizeMax) break; + $dataBufferDiff = min($dataBufferSizeMax, $dataBufferSize*2) - $dataBufferSize; + $dataBufferSize += $dataBufferDiff; + $dataBufferChunk = fread($fileHandle, $dataBufferDiff); + if (feof($fileHandle) || $dataBufferChunk===false) { + $dataBufferSize = 0; + break; + } + $dataBuffer .= $dataBufferChunk; + } + } + } + } elseif ($fileType=="png") { + $dataSignature = fread($fileHandle, 8); + $dataHeader = fread($fileHandle, 16); + if (!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n") { + $width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]); + $height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]); + $type = $fileType; + } + } elseif ($fileType=="svg") { + $dataBufferSizeMax = filesize($fileName); + $dataBufferSize = min($dataBufferSizeMax, 4096); + if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize); + if (!feof($fileHandle) && preg_match("/<svg(\s.*?)>/s", $dataBuffer, $matches)) { + if (preg_match("/\swidth=\"(\d+)\"/s", $matches[1], $tokens)) $width = $tokens[1]; + if (preg_match("/\sheight=\"(\d+)\"/s", $matches[1], $tokens)) $height = $tokens[1]; + $type = $fileType; + } + } + fclose($fileHandle); + } + return array($width, $height, $orientation, $type); + } + + // Return image orientation from Exif + public function getImageOrientationFromBuffer($dataBuffer, $pos, $size) { + $orientation = 0; + $dataSignature = substrb($dataBuffer, $pos, 6); + if ($dataSignature=="\x45\x78\x69\x66\x00\x00" && $pos+14<=$size) { + $startPos = $pos+6; + $bigEndian = $dataBuffer[$startPos]=="M"; + $ifdOffset = $this->getLongFromBuffer($dataBuffer, $startPos+4, $bigEndian); + $ifdStartPos = $startPos+$ifdOffset; + $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0; + $pos = $ifdStartPos+2; + while ($ifdCount && $pos+12<=$size) { + $ifdTag = $this->getShortFromBuffer($dataBuffer, $pos, $bigEndian); + $ifdFormat = $this->getShortFromBuffer($dataBuffer, $pos+2, $bigEndian); + if ($ifdTag==0x8769 && $ifdFormat==4) { + $ifdOffset = $this->getLongFromBuffer($dataBuffer, $pos+8, $bigEndian); + $ifdStartPos = $startPos+$ifdOffset; + $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0; + $pos = $ifdStartPos+2; + continue; + } + if ($ifdTag==0x0112 && $ifdFormat==3) { + $orientation = $this->getShortFromBuffer($dataBuffer, $pos+8, $bigEndian); + break; + } + --$ifdCount; + $pos += 12; + } + } + return $orientation; + } + + // Return unsigned short value from buffer + public function getShortFromBuffer($dataBuffer, $pos, $bigEndian) { + if ($bigEndian) { + $value = (ord($dataBuffer[$pos])<<8) + ord($dataBuffer[$pos+1]); + } else { + $value = (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]); + } + return $value; + } + + // Return unsigned long value from buffer + public function getLongFromBuffer($dataBuffer, $pos, $bigEndian) { + if ($bigEndian) { + $value = (ord($dataBuffer[$pos])<<24) + (ord($dataBuffer[$pos+1])<<16) + + (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]); + } else { + $value = (ord($dataBuffer[$pos+3])<<24) + (ord($dataBuffer[$pos+2])<<16) + + (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]); + } + return $value; + } + + // Send email message + public function mail($action, $headers, $message) { + $statusCode = 0; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onMail")) { + $statusCode = $value["object"]->onMail($action, $headers, $message); + if ($statusCode!=0) break; + } + } + if ($statusCode==0) { + $text = $this->yellow->lookup->normaliseHeaders($headers, "mime"); + $to = $subject = $remaining = $key = ""; + foreach (preg_split("/\r\n/", $text) as $line) { + if (preg_match("/^(.*?):\s*(.*?)$/", $line, $matches) && !is_string_empty($matches[1])) { + $key = $matches[1]; + $fragment = $matches[2]; + } else { + $fragment = $line; + } + if ($key=="To") { $to .= $fragment; continue; } + if ($key=="Subject") { $subject .= $fragment; continue; } + $remaining .= $line."\r\n"; + } + $statusCode = mail($to, $subject, $message, $remaining) ? 200 : 500; + } + return $statusCode==200; + } + + // Write information to log file + public function log($action, $message) { + $statusCode = 0; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onLog")) { + $statusCode = $value["object"]->onLog($action, $message); + if ($statusCode!=0) break; + } + } + if ($statusCode==0) { + $line = date("Y-m-d H:i:s")." ".trim($action)." ".trim($message)."\n"; + $this->appendFile($this->yellow->system->get("coreServerInstallDirectory"). + $this->yellow->system->get("coreExtensionDirectory"). + $this->yellow->system->get("coreWebsiteFile"), $line); + } + } + + // Start timer + public function timerStart(&$time) { + $time = microtime(true); + } + + // Stop timer and calculate elapsed time in milliseconds + public function timerStop(&$time) { + $time = intval((microtime(true)-$time) * 1000); + } + + // Check if there are location arguments in current HTTP request + public function isLocationArguments($location = "") { + if (is_string_empty($location)) $location = $this->getServer("LOCATION").$this->getServer("LOCATION_ARGUMENTS"); + $separator = $this->getLocationArgumentsSeparator(); + return preg_match("/[^\/]+$separator.*$/", $location); + } + + // Check if there are pagination arguments in current HTTP request + public function isLocationArgumentsPagination($location) { + $separator = $this->getLocationArgumentsSeparator(); + return preg_match("/^(.*\/)?page$separator.*$/", $location); + } + + // Check if unmodified since last HTTP request + public function isNotModified($lastModifiedFormatted) { + return $this->getServer("HTTP_IF_MODIFIED_SINCE")==$lastModifiedFormatted; + } + + // TODO: remove later, for backwards compatibility + public function normaliseArguments($text, $appendSlash = true, $filterStrict = true) { return $this->yellow->lookup->normaliseArguments($text, $appendSlash, $filterStrict); } + public function normalisePath($text) { return $this->yellow->lookup->normalisePath($text); } +} + +class YellowPage { + public $yellow; // access to API + public $scheme; // server scheme + public $address; // server address + public $base; // base location + public $location; // page location + public $fileName; // content file name + public $rawData; // raw data of page + public $metaDataOffsetBytes; // meta data offset + public $metaData; // meta data + public $pageCollections; // additional pages + public $sharedPages; // shared pages + public $headerData; // response header + public $outputData; // response output + public $parser; // content parser + public $parserData; // content data of page + public $statusCode; // status code + public $errorMessage; // error message + public $lastModified; // last modification date + public $available; // page is available? (boolean) + public $visible; // page is visible location? (boolean) + public $active; // page is active location? (boolean) + public $cacheable; // page is cacheable? (boolean) + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->scheme = ""; + $this->address = ""; + $this->base = ""; + $this->location = ""; + $this->fileName = ""; + $this->metaData = new YellowArray(); + $this->pageCollections = array(); + $this->sharedPages = array(); + $this->headerData = array(); + } + + // Set request information + public function setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable) { + $this->scheme = $scheme; + $this->address = $address; + $this->base = $base; + $this->location = $location; + $this->fileName = $fileName; + $this->cacheable = $cacheable; + } + + // Parse page meta + public function parseMeta($rawData, $statusCode = 0, $errorMessage = "") { + $this->rawData = $rawData; + $this->parser = null; + $this->parserData = ""; + $this->statusCode = $statusCode; + $this->errorMessage = $errorMessage; + $this->lastModified = 0; + $this->available = true; + $this->visible = true; + $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location); + $this->parseMetaData(); + } + + // Parse page meta update + public function parseMetaUpdate() { + if ($this->statusCode==0) { + $this->rawData = $this->yellow->toolbox->readFile($this->fileName); + $this->statusCode = 200; + $this->parseMetaData(); + } + } + + // Parse page meta data + public function parseMetaData() { + $this->metaData = new YellowArray(); + $this->metaDataOffsetBytes = 0; + if (!is_null($this->rawData)) { + $this->set("title", $this->yellow->toolbox->createTextTitle($this->location)); + $this->set("language", $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language"))); + $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); + $this->parseMetaDataRaw(array("sitename", "author", "layout", "theme", "parser", "status")); + $this->parseMetaDataShared(); + $titleHeader = ($this->location==$this->yellow->content->getHomeLocation($this->location)) ? + $this->get("sitename") : $this->get("title")." - ".$this->get("sitename"); + if (!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title")); + if (!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title")); + if (!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader); + if ($this->yellow->lookup->isRootLocation($this->location) || !is_readable($this->fileName)) $this->available = false; + if ($this->get("status")=="shared") $this->available = false; + if ($this->get("status")=="unlisted") $this->visible = false; + } else { + $this->set("size", filesize($this->fileName)); + $this->set("type", $this->yellow->toolbox->getFileType($this->fileName)); + $this->set("group", $this->yellow->toolbox->getFileGroup($this->fileName, $this->yellow->system->get("coreMediaDirectory"))); + $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName))); + if (!$this->yellow->lookup->isFileLocation($this->location)) $this->available = false; + } + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParseMetaData")) $value["object"]->onParseMetaData($this); + } + } + + // Parse page meta data from raw data + public function parseMetaDataRaw($defaultKeys) { + foreach ($defaultKeys as $key) { + $value = $this->yellow->system->get($key); + if (!is_string_empty($key) && !is_string_empty($value)) $this->set($key, $value); + } + if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+/s", $this->rawData, $parts)) { + $this->metaDataOffsetBytes = strlenb($parts[0]); + foreach (preg_split("/[\r\n]+/", $parts[2]) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) $this->set($matches[1], $matches[2]); + } + } + } elseif (preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parts)) { + $this->metaDataOffsetBytes = strlenb($parts[0]); + $this->set("title", $parts[2]); + } + } + + // Parse page meta data for shared pages + public function parseMetaDataShared() { + $this->sharedPages["main"] = $this; + if (!$this->yellow->lookup->isSharedLocation($this->location) && $this->statusCode!=0) { + foreach ($this->yellow->content->getShared($this->location) as $page) { + $this->sharedPages[basename($page->location)] = $page; + $page->sharedPages["main"] = $this; + } + } + if ($this->yellow->lookup->isSharedLocation($this->location)) { + $this->set("status", "shared"); + } + } + + // Parse page content on demand + public function parseContent() { + if (!is_null($this->rawData) && !is_object($this->parser)) { + if ($this->yellow->extension->isExisting($this->get("parser"))) { + $value = $this->yellow->extension->data[$this->get("parser")]; + if (method_exists($value["object"], "onParseContentRaw")) { + $this->parser = $value["object"]; + $this->parserData = $this->getContentRaw(); + $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData); + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParseContentHtml")) { + $output = $value["object"]->onParseContentHtml($this, $this->parserData); + if (!is_null($output)) $this->parserData = $output; + } + } + } + } else { + $this->parserData = $this->getContentRaw(); + $this->parserData = preg_replace("/\[yellow error\]/i", $this->errorMessage, $this->parserData); + } + if (!$this->isExisting("description")) { + $description = $this->yellow->toolbox->createTextDescription($this->parserData, 150); + $this->set("description", !is_string_empty($description) ? $description : $this->get("title")); + } + if ($this->yellow->system->get("coreDebugMode")>=3) { + echo "YellowPage::parseContent location:".$this->location."<br/>\n"; + } + } + } + + // Parse page content element, experimental + public function parseContentElement($name, $text, $attrributes, $type) { + $output = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParseContentElement")) { + $output = $value["object"]->onParseContentElement($this, $name, $text, $attrributes, $type); + if (!is_null($output)) break; + } + if (method_exists($value["object"], "onParseContentShortcut")) { //TODO: remove later, for backwards compatibility + $output = $value["object"]->onParseContentShortcut($this, $name, $text, $type); + if (!is_null($output)) break; + } + } + if (is_null($output)) { + if ($name=="yellow" && $type=="inline" && $text=="error") { + $output = $this->errorMessage; + } + } + if ($this->yellow->system->get("coreDebugMode")>=3 && !is_string_empty($name)) { + echo "YellowPage::parseContentElement name:$name type:$type<br/>\n"; + } + return $output; + } + + // TODO: remove later, for backwards compatibility + public function parseContentShortcut($name, $text, $type) { return $this->parseContentElement($name, $text, "", $type); } + + // Parse page + public function parsePage() { + $this->parsePageLayout($this->get("layout")); + if (!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, no-store"); + if (!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8"); + if (!$this->isHeader("Content-Modified")) $this->setHeader("Content-Modified", $this->getModified(true)); + if (!$this->isHeader("Last-Modified")) $this->setHeader("Last-Modified", $this->getLastModified(true)); + $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; + if (!is_file($fileNameTheme)) { + $this->error(500, "Theme '".$this->get("theme")."' does not exist!"); + } + if (!$this->yellow->language->isExisting($this->get("language"))) { + $this->error(500, "Language '".$this->get("language")."' 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->lookup->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200) { + $location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->location); + $location = $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, "", $location); + $this->status(301, $location); + } + if ($this->yellow->lookup->getRequestHandler()=="core" && !$this->isAvailable() && $this->statusCode==200) { + $this->error(404); + } + if ($this->isExisting("pageClean")) $this->outputData = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParsePageOutput")) { + $output = $value["object"]->onParsePageOutput($this, $this->outputData); + if (!is_null($output)) $this->outputData = $output; + } + } + } + + // Parse page layout + public function parsePageLayout($name) { + $this->outputData = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParsePageLayout")) { + $value["object"]->onParsePageLayout($this, $name); + } + } + if (is_null($this->outputData)) { + ob_start(); + $this->includeLayout($name); + $this->outputData = ob_get_contents(); + ob_end_clean(); + } + } + + // Include page layout + public function includeLayout($name) { + $fileNameLayoutNormal = $this->yellow->system->get("coreLayoutDirectory").$this->yellow->lookup->normaliseName($name).".html"; + $fileNameLayoutTheme = $this->yellow->system->get("coreLayoutDirectory"). + $this->yellow->lookup->normaliseName($this->get("theme"))."-".$this->yellow->lookup->normaliseName($name).".html"; + if (is_file($fileNameLayoutTheme)) { + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowPage::includeLayout file:$fileNameLayoutTheme<br/>\n"; + } + $this->setLastModified(filemtime($fileNameLayoutTheme)); + require($fileNameLayoutTheme); + } elseif (is_file($fileNameLayoutNormal)) { + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowPage::includeLayout file:$fileNameLayoutNormal<br/>\n"; + } + $this->setLastModified(filemtime($fileNameLayoutNormal)); + require($fileNameLayoutNormal); + } else { + $this->error(500, "Layout '$name' does not exist!"); + echo "Layout error<br/>\n"; + } + } + + // Set page setting + public function set($key, $value) { + $this->metaData[$key] = $value; + } + + // Return page setting + public function get($key) { + return $this->isExisting($key) ? $this->metaData[$key] : ""; + } + + // Return page setting, HTML encoded + public function getHtml($key) { + return htmlspecialchars($this->get($key)); + } + + // Return page setting as language specific date + public function getDate($key, $format = "") { + if (!is_string_empty($format)) { + $format = $this->yellow->language->getText($format); + } else { + $format = $this->yellow->language->getText("coreDateFormatMedium"); + } + return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format); + } + + // Return page setting as language specific date, HTML encoded + public function getDateHtml($key, $format = "") { + return htmlspecialchars($this->getDate($key, $format)); + } + + // Return page setting as language specific date, relative to today + public function getDateRelative($key, $format = "", $daysLimit = 30) { + if (!is_string_empty($format)) { + $format = $this->yellow->language->getText($format); + } else { + $format = $this->yellow->language->getText("coreDateFormatMedium"); + } + return $this->yellow->language->getDateRelative(strtotime($this->get($key)), $format, $daysLimit); + } + + // Return page setting as language specific date, relative to today, HTML encoded + public function getDateRelativeHtml($key, $format = "", $daysLimit = 30) { + return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit)); + } + + // Return page setting as date + public function getDateFormatted($key, $format) { + return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format); + } + + // Return page setting as date, HTML encoded + public function getDateFormattedHtml($key, $format) { + return htmlspecialchars($this->getDateFormatted($key, $format)); + } + + // Return page content data, raw format + public function getContentRaw() { + $this->parseMetaUpdate(); + return substrb($this->rawData, $this->metaDataOffsetBytes); + } + + // Return page content data, HTML encoded or raw format + public function getContentHtml() { + $this->parseContent(); + return $this->parserData; + } + + // Return page extra data, HTML encoded + public function getExtraHtml($name) { + $output = ""; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onParsePageExtra")) { + $outputExtension = $value["object"]->onParsePageExtra($this, $name); + if (!is_null($outputExtension)) $output .= $outputExtension; + } + } + if ($name=="header") { + $extensionLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreExtensionLocation"); + $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css"; + if (is_file($fileNameTheme)) { + $fileLocation = $extensionLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".css"; + $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"$fileLocation\" />\n"; + } + $fileNameScript = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".js"; + if (is_file($fileNameScript)) { + $fileLocation = $extensionLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".js"; + $output .= "<script type=\"text/javascript\" src=\"$fileLocation\"></script>\n"; + } + $fileNameFavicon = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".png"; + if (is_file($fileNameFavicon)) { + $fileLocation = $extensionLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".png"; + $output .= "<link rel=\"icon\" type=\"image/png\" href=\"$fileLocation\" />\n"; + } + } + return $output; + } + + // Return parent page, null if none + public function getParent() { + $parentLocation = $this->yellow->content->getParentLocation($this->location); + return $this->yellow->content->find($parentLocation); + } + + // Return top-level parent page, null if none + public function getParentTop($homeFallback = false) { + $parentTopLocation = $this->yellow->content->getParentTopLocation($this->location); + if (!$this->yellow->content->find($parentTopLocation) && $homeFallback) { + $parentTopLocation = $this->yellow->content->getHomeLocation($this->location); + } + return $this->yellow->content->find($parentTopLocation); + } + + // Return page collection with pages on the same level + public function getSiblings($showInvisible = false) { + $parentLocation = $this->yellow->content->getParentLocation($this->location); + return $this->yellow->content->getChildren($parentLocation, $showInvisible); + } + + // Return page collection with child pages + public function getChildren($showInvisible = false) { + return $this->yellow->content->getChildren($this->location, $showInvisible); + } + + // Return page collection with child pages recursively + public function getChildrenRecursive($showInvisible = false, $levelMax = 0) { + return $this->yellow->content->getChildrenRecursive($this->location, $showInvisible, $levelMax); + } + + // Set page collection with additional pages + public function setPages($key, $pages) { + $this->pageCollections[$key] = $pages; + } + + // Return page collection with additional pages + public function getPages($key) { + return isset($this->pageCollections[$key]) ? $this->pageCollections[$key] : new YellowPageCollection($this->yellow); + } + + // Set shared page + public function setPage($key, $page) { + $this->sharedPages[$key] = $page; + } + + // Return shared page + public function getPage($key) { + return isset($this->sharedPages[$key]) ? $this->sharedPages[$key] : new YellowPage($this->yellow); + } + + // Return page URL + public function getUrl() { + return $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, $this->base, $this->location); + } + + // Return page base + public function getBase($multiLanguage = false) { + return $multiLanguage ? rtrim($this->base.$this->yellow->content->getHomeLocation($this->location), "/") : $this->base; + } + + // Return page location + public function getLocation($absoluteLocation = false) { + return $absoluteLocation ? $this->base.$this->location : $this->location; + } + + // Set page request argument + public function setRequest($key, $value) { + $_REQUEST[$key] = $value; + } + + // Return page request argument + public function getRequest($key) { + return isset($_REQUEST[$key]) ? $_REQUEST[$key] : ""; + } + + // Return page request argument, HTML encoded + public function getRequestHtml($key) { + return htmlspecialchars($this->getRequest($key)); + } + + // Set page response header + public function setHeader($key, $value) { + $this->headerData[$key] = $value; + } + + // Return page response header + public function getHeader($key) { + return $this->isHeader($key) ? $this->headerData[$key] : ""; + } + + // Set page response output + public function setOutput($output) { + $this->outputData = $output; + } + + // Return page modification date, Unix time or HTTP format + public function getModified($httpFormat = false) { + $modified = strtotime($this->get("modified")); + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified; + } + + // Set last modification date, Unix time + public function setLastModified($modified) { + $this->lastModified = max($this->lastModified, $modified); + } + + // Return last modification date, Unix time or HTTP format + public function getLastModified($httpFormat = false) { + $lastModified = max($this->lastModified, $this->getModified(), $this->yellow->system->getModified(), + $this->yellow->language->getModified(), $this->yellow->extension->getModified()); + foreach ($this->pageCollections as $pages) $lastModified = max($lastModified, $pages->getModified()); + foreach ($this->sharedPages as $page) $lastModified = max($lastModified, $page->getModified()); + return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($lastModified) : $lastModified; + } + + // Return raw data for error page + public function getRawDataError() { + $statusCode = $this->statusCode; + $sharedLocation = $this->yellow->content->getHomeLocation($this->location)."shared/"; + $fileNameError = $this->yellow->lookup->findFileFromContentLocation($sharedLocation, true).$this->yellow->system->get("coreContentErrorFile"); + $fileNameError = str_replace("(.*)", $statusCode, $fileNameError); + $languageError = $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language")); + if (is_file($fileNameError)) { + $rawData = $this->yellow->toolbox->readFile($fileNameError); + } elseif ($this->yellow->language->isText("coreError{$statusCode}Title", $languageError)) { + $rawData = "---\nTitle: ".$this->yellow->language->getText("coreError{$statusCode}Title", $languageError)."\n"; + $rawData .= "Layout: error\n---\n".$this->yellow->language->getText("coreError{$statusCode}Text", $languageError); + } else { + $rawData = "---\nTitle:".$this->yellow->toolbox->getHttpStatusFormatted($statusCode, true)."\n"; + $rawData .= "Layout:error\n---\n".$this->errorMessage; + } + return $rawData; + } + + // Return page status code, number or HTTP format + public function getStatusCode($httpFormat = false) { + $statusCode = $this->statusCode; + if ($httpFormat) { + $statusCode = $this->yellow->toolbox->getHttpStatusFormatted($statusCode); + if (!is_string_empty($this->errorMessage)) $statusCode .= ": ".$this->errorMessage; + } + return $statusCode; + } + + // Respond with status code, no page content + public function status($statusCode, $location = "") { + if ($statusCode>0 && !$this->isExisting("pageClean")) { + $this->statusCode = $statusCode; + $this->lastModified = 0; + $this->headerData = array(); + if (!is_string_empty($location)) { + $this->setHeader("Location", $location); + $this->setHeader("Cache-Control", "no-cache, no-store"); + } + $this->set("pageClean", (string)$statusCode); + } + } + + // Respond with error page + public function error($statusCode, $errorMessage = "") { + if ($statusCode>=400 && is_string_empty($this->errorMessage)) { + $this->statusCode = $statusCode; + $this->errorMessage = is_string_empty($errorMessage) ? "Page error!" : $errorMessage; + } + } + + // Check if page is available + public function isAvailable() { + return $this->available; + } + + // Check if page is visible + public function isVisible() { + return $this->visible; + } + + // Check if page is within current HTTP request + public function isActive() { + return $this->active; + } + + // Check if page is cacheable + public function isCacheable() { + return $this->cacheable; + } + + // Check if page with error + public function isError() { + return $this->statusCode>=400; + } + + // Check if page setting exists + public function isExisting($key) { + return isset($this->metaData[$key]); + } + + // Check if request argument exists + public function isRequest($key) { + return isset($_REQUEST[$key]); + } + + // Check if response header exists + public function isHeader($key) { + return isset($this->headerData[$key]); + } + + // Check if shared page exists + public function isPage($key) { + return isset($this->sharedPages[$key]); + } + + // TODO: remove later, for backwards compatibility + public function getContent($rawFormat = false) { return $rawFormat ? $this->getContentRaw() : $this->getContentHtml(); } + public function getExtra($name) { return $this->getExtraHtml($name); } +} + +class YellowPageCollection extends ArrayObject { + public $yellow; // access to API + public $filterValue; // current page filter value + public $paginationNumber; // current page number in pagination + public $paginationCount; // highest page number in pagination + + public function __construct($yellow) { + parent::__construct(array()); + $this->yellow = $yellow; + } + + // Append page to end of page collection + #[\ReturnTypeWillChange] + public function append($page) { + parent::append($page); + } + + // Prepend page to start of page collection + #[\ReturnTypeWillChange] + public function prepend($page) { + $array = $this->getArrayCopy(); + array_unshift($array, $page); + $this->exchangeArray($array); + } + + // Remove page from page collection + public function remove($page): YellowPageCollection { + $array = array(); + $location = $page->location; + foreach ($this->getArrayCopy() as $page) { + if ($page->location!=$location) array_push($array, $page); + } + $this->exchangeArray($array); + return $this; + } + + // Filter page collection by page setting + public function filter($key, $value, $exactMatch = true): YellowPageCollection { + $array = array(); + $value = str_replace(" ", "-", strtoloweru($value)); + $valueLength = strlenu($value); + $this->filterValue = ""; + foreach ($this->getArrayCopy() as $page) { + if ($page->isExisting($key)) { + foreach (preg_split("/\s*,\s*/", $page->get($key)) as $pageValue) { + $pageValueLength = $exactMatch ? strlenu($pageValue) : $valueLength; + if ($value==substru(str_replace(" ", "-", strtoloweru($pageValue)), 0, $pageValueLength)) { + if (is_string_empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength); + array_push($array, $page); + break; + } + } + } + } + $this->exchangeArray($array); + return $this; + } + + // Filter page collection by location or file + public function match($regex = "/.*/", $filterByLocation = true): YellowPageCollection { + $array = array(); + $this->filterValue = $regex; + foreach ($this->getArrayCopy() as $page) { + if (preg_match($regex, $filterByLocation ? $page->location : $page->fileName)) array_push($array, $page); + } + $this->exchangeArray($array); + return $this; + } + + // Sort page collection by settings similarity + public function similar($page): YellowPageCollection { + $location = $page->location; + $keywords = strtoloweru($page->get("title").",".$page->get("tag").",".$page->get("author")); + $tokens = array_unique(array_filter(preg_split("/[,\s\(\)\+\-]/", $keywords), "strlen")); + if (!is_array_empty($tokens)) { + $array = array(); + foreach ($this->getArrayCopy() as $page) { + $sortScore = 0; + foreach ($tokens as $token) { + if (stristr($page->get("title"), $token)) $sortScore += 50; + if (stristr($page->get("tag"), $token)) $sortScore += 5; + if (stristr($page->get("author"), $token)) $sortScore += 2; + } + if ($page->location!=$location) { + $page->set("sortScore", $sortScore); + array_push($array, $page); + } + } + $this->exchangeArray($array); + $this->sort("modified", false)->sort("sortScore", false); + } + return $this; + } + + // Sort page collection by page setting + public function sort($key, $ascendingOrder = true): YellowPageCollection { + $array = $this->getArrayCopy(); + $sortIndex = 0; + foreach ($array as $page) { + $page->set("sortIndex", ++$sortIndex); + } + $callback = function ($a, $b) use ($key, $ascendingOrder) { + $result = $ascendingOrder ? + strnatcasecmp($a->get($key), $b->get($key)) : + strnatcasecmp($b->get($key), $a->get($key)); + return $result==0 ? $a->get("sortIndex") - $b->get("sortIndex") : $result; + }; + usort($array, $callback); + $this->exchangeArray($array); + return $this; + } + + // Group page collection by page setting, return array with multiple collections + public function group($key, $ascendingOrder = true, $format = ""): array { + $array = array(); + $groupByInitial = $format=="initial"; + $groupByDate = !is_string_empty($format) && $format!="count" && $format!="initial"; + foreach ($this->getIterator() as $page) { + if ($page->isExisting($key)) { + foreach (preg_split("/\s*,\s*/", $page->get($key)) as $group) { + if ($groupByInitial) { + $group = strtoupperu(substru($group, 0, 1)); + } elseif ($groupByDate) { + $group = $this->yellow->language->getDateFormatted(strtotime($group), $format); + } + if (!is_string_empty($group)) { + if (!isset($array[$group])) { + $groupSearch = strtoloweru($group); + foreach (array_keys($array) as $groupFound) { + if (strtoloweru($groupFound)==$groupSearch) { + $group = $groupFound; + break; + } + } + if (!isset($array[$group])) $array[$group] = new YellowPageCollection($this->yellow); + } + $array[$group]->append($page); + } + } + } + } + $callbackString = function ($a, $b) use ($ascendingOrder) { + return $ascendingOrder ? strnatcasecmp($a, $b) : strnatcasecmp($b, $a); + }; + $callbackCollection = function ($a, $b) use ($ascendingOrder) { + return $ascendingOrder ? count($a)-count($b) : count($b)-count($a); + }; + if ($format!="count") { + uksort($array, $callbackString); + } else { + uasort($array, $callbackCollection); + } + return $array; + } + + // Calculate union, merge page collection + public function merge($input): YellowPageCollection { + $this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input)); + return $this; + } + + // Calculate intersection, remove pages that are not present in another page collection + public function intersect($input): YellowPageCollection { + $callback = function ($a, $b) { + return strcmp($a->location, $b->location); + }; + $this->exchangeArray(array_uintersect($this->getArrayCopy(), (array)$input, $callback)); + return $this; + } + + // Calculate difference, remove pages that are present in another page collection + public function diff($input): YellowPageCollection { + $callback = function ($a, $b) { + return strcmp($a->location, $b->location); + }; + $this->exchangeArray(array_udiff($this->getArrayCopy(), (array)$input, $callback)); + return $this; + } + + // Limit the number of pages in page collection + public function limit($pagesMax): YellowPageCollection { + $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax)); + return $this; + } + + // Reverse page collection + public function reverse(): YellowPageCollection { + $this->exchangeArray(array_reverse($this->getArrayCopy())); + return $this; + } + + // Randomize page collection + public function shuffle(): YellowPageCollection { + $array = $this->getArrayCopy(); + shuffle($array); + $this->exchangeArray($array); + return $this; + } + + // Paginate page collection + public function paginate($limit): YellowPageCollection { + if (!$this->isPagination() && $limit!=0) { + $this->paginationNumber = 1; + $this->paginationCount = ceil($this->count() / $limit); + if ($this->yellow->page->isRequest("page")) { + $this->paginationNumber = intval($this->yellow->page->getRequest("page")); + } + if ($this->paginationNumber<0 || $this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0; + if ($this->paginationNumber) { + $this->exchangeArray(array_slice($this->getArrayCopy(), ($this->paginationNumber - 1) * $limit, $limit)); + } else { + $this->yellow->page->error(404); + } + } + return $this; + } + + // 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) { + $location = $locationArguments = ""; + if ($pageNumber>=1 && $pageNumber<=$this->paginationCount) { + $location = $this->yellow->page->getLocation($absoluteLocation); + $locationArguments = $this->yellow->toolbox->getLocationArgumentsNew("page", $pageNumber>1 ? "$pageNumber" : ""); + } + return $location.$locationArguments; + } + + // 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; + } + + // Check if page collection is empty + public function isEmpty() { + return empty($this->getArrayCopy()); + } +} + +class YellowArray extends ArrayObject { + public function __construct($array = []) { + parent::__construct($array); + } + + // Set array element + public function set($key, $value) { + $this->offsetSet($key, $value); + } + + // Return array element + public function get($key) { + return $this->offsetExists($key) ? $this->offsetGet($key) : ""; + } + + // Check if array element exists + public function isExisting($key) { + return $this->offsetExists($key); + } + + // Return array element + #[\ReturnTypeWillChange] + public function offsetGet($key) { + if (is_string($key)) $key = lcfirst($key); + return parent::offsetGet($key); + } + + // Set array element + #[\ReturnTypeWillChange] + public function offsetSet($key, $value) { + if (is_string($key)) $key = lcfirst($key); + parent::offsetSet($key, $value); + } + + // Remove array element + #[\ReturnTypeWillChange] + public function offsetUnset($key) { + if (is_string($key)) $key = lcfirst($key); + parent::offsetUnset($key); + } + + // Check if array element exists + #[\ReturnTypeWillChange] + public function offsetExists($key) { + if (is_string($key)) $key = lcfirst($key); + return parent::offsetExists($key); + } + + // Check if array is empty + public function isEmpty() { + return empty($this->getArrayCopy()); + } +} + +// Make string lowercase, UTF-8 compatible +function strtoloweru() { + return call_user_func_array("mb_strtolower", func_get_args()); +} + +// Make string uppercase, UTF-8 compatible +function strtoupperu() { + return call_user_func_array("mb_strtoupper", func_get_args()); +} + +// Return string length, UTF-8 characters +function strlenu() { + return call_user_func_array("mb_strlen", func_get_args()); +} + +// Return string length, bytes +function strlenb() { + return call_user_func_array("strlen", func_get_args()); +} + +// Return string position of first match, UTF-8 characters +function strposu() { + return call_user_func_array("mb_strpos", func_get_args()); +} + +// Return string position of first match, bytes +function strposb() { + return call_user_func_array("strpos", func_get_args()); +} + +// Return string position of last match, UTF-8 characters +function strrposu() { + return call_user_func_array("mb_strrpos", func_get_args()); +} + +// Return string position of last match, bytes +function strrposb() { + return call_user_func_array("strrpos", func_get_args()); +} + +// Return part of a string, UTF-8 characters +function substru() { + return call_user_func_array("mb_substr", func_get_args()); +} + +// Return part of a string, bytes +function substrb() { + return call_user_func_array("substr", func_get_args()); +} + +// Check if string is empty +function is_string_empty($string) { + return is_null($string) || $string===""; +} +function strempty($string) { return is_null($string) || $string===""; } //TODO: remove later, for backwards compatibility + +// Check if array is empty +function is_array_empty($array) { + return is_null($array) || (is_array($array) ? empty($array) : empty($array->getArrayCopy())); +} diff --git a/system/extensions/edit-stack.svg b/system/workers/edit-stack.svg diff --git a/system/extensions/edit.css b/system/workers/edit.css diff --git a/system/extensions/edit.js b/system/workers/edit.js diff --git a/system/workers/edit.php b/system/workers/edit.php @@ -0,0 +1,2018 @@ +<?php +// Edit extension, https://github.com/annaesvensson/yellow-edit + +class YellowEdit { + const VERSION = "0.9.1"; + public $yellow; // access to API + public $response; // web response + public $merge; // text merge + public $editable; // page can be edited? (boolean) + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->response = new YellowEditResponse($yellow); + $this->merge = new YellowEditMerge($yellow); + $this->yellow->system->setDefault("editSiteEmail", "noreply"); + $this->yellow->system->setDefault("editLocation", "/edit/"); + $this->yellow->system->setDefault("editUploadNewLocation", "/media/@group/@filename"); + $this->yellow->system->setDefault("editUploadExtensions", ".gif, .jpg, .mp3, .ogg, .pdf, .png, .svg, .zip"); + $this->yellow->system->setDefault("editKeyboardShortcuts", "ctrl+b bold, ctrl+i italic, ctrl+k strikethrough, ctrl+e code, ctrl+s save, ctrl+alt+p preview"); + $this->yellow->system->setDefault("editToolbarButtons", "auto"); + $this->yellow->system->setDefault("editEndOfLine", "auto"); + $this->yellow->system->setDefault("editNewFile", "page-new-(.*).md"); + $this->yellow->system->setDefault("editUserPasswordMinLength", "8"); + $this->yellow->system->setDefault("editUserHashAlgorithm", "bcrypt"); + $this->yellow->system->setDefault("editUserHashCost", "10"); + $this->yellow->system->setDefault("editUserAccess", "create, edit, delete, restore, upload"); + $this->yellow->system->setDefault("editUserHome", "/"); + $this->yellow->system->setDefault("editLoginRestriction", "0"); + $this->yellow->system->setDefault("editLoginSessionTimeout", "2592000"); + $this->yellow->system->setDefault("editBruteForceProtection", "25"); + } + + // Handle update + public function onUpdate($action) { + if ($action=="clean" || $action=="daily") { + $cleanup = false; + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $fileData = $this->yellow->toolbox->readFile($fileNameUser); + $fileDataNew = ""; + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (lcfirst($matches[1])=="email" && !is_string_empty($matches[2])) { + $status = $this->yellow->user->getUser("status", $matches[2]); + $reserved = strtotime($this->yellow->user->getUser("modified", $matches[2])) + 60*60*24; + $cleanup = $status!="active" && $status!="inactive" && $reserved<=time(); + } + } + if (!$cleanup) $fileDataNew .= $line; + } + $fileDataNew = rtrim($fileDataNew)."\n"; + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileNameUser, $fileDataNew)) { + $this->yellow->toolbox->log("error", "Can't write file '$fileNameUser'!"); + } + } + } + + // Handle request + public function onRequest($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->isEditLocation($location)) { + $this->editable = true; + $scheme = $this->yellow->system->get("coreServerScheme"); + $address = $this->yellow->system->get("coreServerAddress"); + $base = rtrim($this->yellow->system->get("coreServerBase").$this->yellow->system->get("editLocation"), "/"); + list($scheme, $address, $base, $location, $fileName) = $this->yellow->lookup->getRequestInformation($scheme, $address, $base); + $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName); + } + return $statusCode; + } + + // Handle command + public function onCommand($command, $text) { + switch ($command) { + case "user": $statusCode = $this->processCommandUser($command, $text); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + return "user [option email password]"; + } + + // Handle page meta data + public function onParseMetaData($page) { + $page->set("editPageUrl", $this->yellow->lookup->normaliseUrl( + $this->yellow->system->get("coreServerScheme"), + $this->yellow->system->get("coreServerAddress"), + $this->yellow->system->get("coreServerBase"), + rtrim($this->yellow->system->get("editLocation"), "/").$page->location)); + } + + // Handle page content element + public function onParseContentElement($page, $name, $text, $attributes, $type) { + $output = null; + if ($name=="edit" && $type=="inline") { + list($target, $description) = $this->yellow->toolbox->getTextList($text, " ", 2); + if (is_string_empty($target) || $target=="-") $target = "main"; + if (is_string_empty($description)) $description = ucfirst($name); + $pageTarget = $target=="main" ? $page->getPage("main") : $page->getPage("main")->getPage($target); + $output = "<a href=\"".$pageTarget->get("editPageUrl")."\">".htmlspecialchars($description)."</a>"; + } + return $output; + } + + // Handle page layout + public function onParsePageLayout($page, $name) { + if ($this->editable) { + $this->response->processPageData($page); + } + } + + // Handle page extra data + public function onParsePageExtra($page, $name) { + $output = null; + if ($this->editable && $name=="header") { + $extensionLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreExtensionLocation"); + $output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"{$extensionLocation}edit.css\" />\n"; + $output .= "<script type=\"text/javascript\" src=\"{$extensionLocation}edit.js\"></script>\n"; + $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.user = ".json_encode($this->response->getUserData()).";\n"; + $output .= "yellow.language = ".json_encode($this->response->getLanguageData()).";\n"; + $output .= "// ]]>\n"; + $output .= "</script>\n"; + } + return $output; + } + + // Process command to update user account + public function processCommandUser($command, $text) { + list($option) = $this->yellow->toolbox->getTextArguments($text); + switch ($option) { + case "": $statusCode = $this->userShow($command, $text); break; + case "add": $statusCode = $this->userAdd($command, $text); break; + case "change": $statusCode = $this->userChange($command, $text); break; + case "remove": $statusCode = $this->userRemove($command, $text); break; + default: $statusCode = 400; echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Show user accounts + public function userShow($command, $text) { + $data = array(); + foreach ($this->yellow->user->settings as $key=>$value) { + $data[$key] = "$value[email] - User account by $value[name]."; + } + uksort($data, "strnatcasecmp"); + foreach ($data as $line) echo "$line\n"; + if (is_array_empty($data)) echo "Yellow $command: No user accounts\n"; + return 200; + } + + // Add user account + public function userAdd($command, $text) { + $status = "ok"; + list($option, $email, $password) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($email) || is_string_empty($password)) $status = $this->response->status = "incomplete"; + if ($status=="ok") $status = $this->getUserAccount("add", $email, $password); + if ($status=="ok" && $this->isUserAccountTaken($email)) $status = "taken"; + switch ($status) { + case "incomplete": echo "ERROR updating settings: Please enter email and password!\n"; break; + case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; + 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; + case "short": echo "ERROR updating settings: Please enter a longer password!\n"; break; + } + if ($status=="ok") { + $name = $this->yellow->system->get("sitename"); + $userLanguage = $this->yellow->system->get("language"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "name" => $name, + "description" => $this->yellow->language->getText("editUserDescription", $userLanguage), + "language" => $userLanguage, + "access" => $this->yellow->system->get("editUserAccess"), + "home" => $this->yellow->system->get("editUserHome"), + "hash" => $this->response->createHash($password), + "stamp" => $this->response->createStamp(), + "pending" => "none", + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time()), + "status" => "active"); + $status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; + $this->yellow->toolbox->log($status=="ok" ? "info" : "error", "Add user '".strtok($name, " ")."'"); + } + if ($status=="ok") { + $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); + $status = substru($this->yellow->user->getUser("hash", $email), 0, 10)!="error-hash" ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Hash algorithm '$algorithm' not supported!\n"; + } + $statusCode = $status=="ok" ? 200 : 500; + echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."added\n"; + return $statusCode; + } + + // Change user account + public function userChange($command, $text) { + $status = "ok"; + list($option, $email, $password) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($email)) $status = $this->response->status = "invalid"; + if ($status=="ok") $status = $this->getUserAccount("change", $email, $password); + if ($status=="ok" && !$this->yellow->user->isExisting($email)) $status = "unknown"; + switch ($status) { + case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; + case "unknown": echo "ERROR updating settings: Can't find email '$email'!\n"; break; + case "weak": echo "ERROR updating settings: Please enter a different password!\n"; break; + case "short": echo "ERROR updating settings: Please enter a longer password!\n"; break; + } + if ($status=="ok") { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "hash" => is_string_empty($password) ? $this->yellow->user->getUser("hash", $email) : $this->response->createHash($password), + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time())); + $status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; + } + $statusCode = $status=="ok" ? 200 : 500; + echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."changed\n"; + return $statusCode; + } + + // Remove user account + public function userRemove($command, $text) { + $status = "ok"; + list($option, $email) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($email)) $status = $this->response->status = "invalid"; + if ($status=="ok") $status = $this->getUserAccount("remove", $email, ""); + if ($status=="ok" && !$this->yellow->user->isExisting($email)) $status = "unknown"; + switch ($status) { + case "invalid": echo "ERROR updating settings: Please enter a valid email!\n"; break; + case "unknown": echo "ERROR updating settings: Can't find email '$email'!\n"; break; + } + if ($status=="ok") { + $name = $this->yellow->user->getUser("name", $email); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $status = $this->yellow->user->remove($fileNameUser, $email) ? "ok" : "error"; + if ($status=="error") echo "ERROR updating settings: Can't write file '$fileNameUser'!\n"; + $this->yellow->toolbox->log($status=="ok" ? "info" : "error", "Remove user '".strtok($name, " ")."'"); + } + $statusCode = $status=="ok" ? 200 : 500; + echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."removed\n"; + return $statusCode; + } + + // Process request + public function processRequest($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->checkUserAuth($scheme, $address, $base, $location, $fileName)) { + switch ($this->yellow->page->getRequest("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 "quit": $statusCode = $this->processRequestQuit($scheme, $address, $base, $location, $fileName); break; + case "account": $statusCode = $this->processRequestAccount($scheme, $address, $base, $location, $fileName); break; + case "configure": $statusCode = $this->processRequestConfigure($scheme, $address, $base, $location, $fileName); break; + case "update": $statusCode = $this->processRequestUpdate($scheme, $address, $base, $location, $fileName); break; + case "create": $statusCode = $this->processRequestCreate($scheme, $address, $base, $location, $fileName); break; + case "edit": $statusCode = $this->processRequestEdit($scheme, $address, $base, $location, $fileName); break; + case "delete": $statusCode = $this->processRequestDelete($scheme, $address, $base, $location, $fileName); break; + case "restore": $statusCode = $this->processRequestRestore($scheme, $address, $base, $location, $fileName); break; + case "preview": $statusCode = $this->processRequestPreview($scheme, $address, $base, $location, $fileName); break; + case "upload": $statusCode = $this->processRequestUpload($scheme, $address, $base, $location, $fileName); break; + } + } elseif ($this->checkUserUnauth($scheme, $address, $base, $location, $fileName)) { + $this->yellow->lookup->requestHandler = "core"; + switch ($this->yellow->page->getRequest("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->getRedirectLocation($location); + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(301, $location); + } else { + $statusCode = 404; + if ($this->response->isUserAccess("create", $location)) $statusCode = 434; + if ($this->response->isUserAccess("restore", $location) && $this->response->isDeletedLocation($location)) { + $statusCode = 435; + } + $this->yellow->page->error($statusCode); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request for user login + public function processRequestLogin($scheme, $address, $base, $location, $fileName) { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time())); + if ($this->yellow->user->save($fileNameUser, $this->response->userEmail, $settings)) { + $home = $this->yellow->user->getUser("home", $this->response->userEmail); + if (substru($location, 0, strlenu($home))==$home) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } 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("coreServerScheme"), + $this->yellow->system->get("coreServerAddress"), + $this->yellow->system->get("coreServerBase"), + $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", "-", $this->yellow->page->getRequest("name"))); + $email = trim($this->yellow->page->getRequest("email")); + $password = trim($this->yellow->page->getRequest("password")); + $consent = trim($this->yellow->page->getRequest("consent")); + if (is_string_empty($name) || is_string_empty($email) || is_string_empty($password) || is_string_empty($consent)) $this->response->status = "incomplete"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); + if ($this->response->status=="ok" && $this->response->isLoginRestriction()) $this->response->status = "next"; + if ($this->response->status=="ok" && $this->isUserAccountTaken($email)) $this->response->status = "next"; + if ($this->response->status=="ok") { + $userLanguage = $this->yellow->lookup->findContentLanguage($fileName, $this->yellow->system->get("language")); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "name" => $name, + "description" => $this->yellow->language->getText("editUserDescription", $userLanguage), + "language" => $userLanguage, + "access" => $this->yellow->system->get("editUserAccess"), + "home" => $this->yellow->system->get("editUserHome"), + "hash" => $this->response->createHash($password), + "stamp" => $this->response->createStamp(), + "pending" => "none", + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time()), + "status" => "unconfirmed"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); + $this->response->status = substru($this->yellow->user->getUser("hash", $email), 0, 10)!="error-hash" ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Hash algorithm '$algorithm' not supported!"); + } + if ($this->response->status=="ok") { + $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 = $this->yellow->page->getRequest("email"); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "unapproved"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $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 = $this->yellow->page->getRequest("email"); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + $name = $this->yellow->user->getUser("name", $email); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "active"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + $this->yellow->toolbox->log($this->response->status=="ok" ? "info" : "error", "Add user '".strtok($name, " ")."'"); + } + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "welcome") ? "done" : "error"; + 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($this->yellow->page->getRequest("email")); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid"; + if ($this->response->status=="ok" && !$this->yellow->user->isExisting($email)) $this->response->status = "next"; + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "recover") ? "next" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $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($this->yellow->page->getRequest("email")); + $password = trim($this->yellow->page->getRequest("password")); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + if (is_string_empty($password)) $this->response->status = "password"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("hash" => $this->response->createHash($password), "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $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 = $this->yellow->page->getRequest("email"); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "active"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + 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 = $this->yellow->page->getRequest("email"); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + $emailSource = $this->yellow->user->getUser("pending", $email); + if ($this->yellow->user->getUser("status", $emailSource)!="active") $this->response->status = "done"; + } + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "unchanged"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $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($this->yellow->page->getRequest("email")); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + list($email, $hash) = $this->yellow->toolbox->getTextList($this->yellow->user->getUser("pending", $email), ":", 2); + if (!$this->yellow->user->isExisting($email) || is_string_empty($hash)) $this->response->status = "done"; + } + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "hash" => $hash, + "pending" => "none", + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time()), + "status" => "active"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok" && $email!=$emailSource) { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $this->response->status = $this->yellow->user->remove($fileNameUser, $emailSource) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $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 quit account + public function processRequestQuit($scheme, $address, $base, $location, $fileName) { + $this->response->action = "quit"; + $this->response->status = "ok"; + $name = trim($this->yellow->page->getRequest("name")); + $email = $this->response->userEmail; + if (is_string_empty($name)) $this->response->status = "none"; + if ($this->response->status=="ok" && $name!=$this->yellow->user->getUser("name", $email)) $this->response->status = "mismatch"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, ""); + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "remove") ? "next" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + $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 = $this->yellow->page->getRequest("email"); + $this->response->status = $this->getUserStatus($email, $this->yellow->page->getRequest("action")); + if ($this->response->status=="ok") { + $name = $this->yellow->user->getUser("name", $email); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("failed" => "0", "modified" => date("Y-m-d H:i:s", time()), "status" => "removed"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + $this->yellow->toolbox->log($this->response->status=="ok" ? "info" : "error", "Remove user '".strtok($name, " ")."'"); + } + if ($this->response->status=="ok") { + $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "goodbye") ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!"); + } + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $this->response->status = $this->yellow->user->remove($fileNameUser, $email) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $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 change account settings + public function processRequestAccount($scheme, $address, $base, $location, $fileName) { + $this->response->action = "account"; + $this->response->status = "ok"; + $email = trim($this->yellow->page->getRequest("email")); + $emailSource = $this->response->userEmail; + $password = trim($this->yellow->page->getRequest("password")); + $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $this->yellow->page->getRequest("name"))); + $language = trim($this->yellow->page->getRequest("language")); + if ($email!=$emailSource || !is_string_empty($password)) { + if (is_string_empty($email)) $this->response->status = "invalid"; + if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($this->response->action, $email, $password); + if ($this->response->status=="ok" && $email!=$emailSource && $this->isUserAccountTaken($email)) $this->response->status = "taken"; + if ($this->response->status=="ok" && $email!=$emailSource) { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "name" => $name, + "description" => $this->yellow->user->getUser("description", $emailSource), + "language" => $language, + "access" => $this->yellow->user->getUser("access", $emailSource), + "home" => $this->yellow->user->getUser("home", $emailSource), + "hash" => $this->response->createHash("none"), + "stamp" => $this->response->createStamp(), + "pending" => $emailSource, + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time()), + "status" => "unverified"); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "name" => $name, + "language" => $language, + "pending" => $email.":".(is_string_empty($password) ? $this->yellow->user->getUser("hash", $emailSource) : $this->response->createHash($password)), + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time())); + $this->response->status = $this->yellow->user->save($fileNameUser, $emailSource, $settings) ? "ok" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($this->response->status=="ok") { + $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("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array("name" => $name, "language" => $language, "failed" => "0", "modified" => date("Y-m-d H:i:s", time())); + $this->response->status = $this->yellow->user->save($fileNameUser, $email, $settings) ? "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 change settings + public function processRequestConfigure($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->response->isUserAccess("configure")) { + $this->response->action = "configure"; + $this->response->status = "ok"; + $sitename = trim($this->yellow->page->getRequest("sitename")); + $author = trim($this->yellow->page->getRequest("author")); + $email = trim($this->yellow->page->getRequest("email")); + if ($email!=$this->yellow->system->get("email")) { + if (is_string_empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid"; + } + if ($this->response->status=="ok") { + $fileNameSystem = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + $settings = array("sitename" => $sitename, "author" => $author, "email" => $email); + $file = $this->response->getFileSystem($scheme, $address, $base, $location, $fileNameSystem, $settings); + $this->response->status = (!$file->isError() && $this->yellow->system->save($fileNameSystem, $settings)) ? "done" : "error"; + if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameSystem'!"); + } + if ($this->response->status=="done") { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $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->response->isUserAccess("update")) { + $this->response->action = "update"; + $this->response->status = "ok"; + if ($this->yellow->page->getRequest("option")=="check") { + list($statusCode, $rawData) = $this->response->getUpdateInformation(); + $this->response->status = is_string_empty($rawData) ? "ok" : "updates"; + $this->response->rawDataOutput = $rawData; + if ($statusCode!=200) { + $this->response->status = "error"; + $this->response->rawDataOutput = ""; + } + } else { + $this->response->status = $this->yellow->command("update all")==0 ? "done" : "error"; + } + 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 create page + public function processRequestCreate($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->response->isUserAccess("create", $location) && !is_string_empty($this->yellow->page->getRequest("rawdataedit"))) { + $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); + $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdatasource"); + $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("rawdataendofline"); + $rawData = $this->yellow->page->getRequest("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->errorMessage); + $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->isUserAccess("edit", $location) && !is_string_empty($this->yellow->page->getRequest("rawdataedit"))) { + $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); + $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdataedit"); + $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("rawdataendofline"); + $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)) { + $ok = $this->yellow->toolbox->renameFile($fileName, $page->fileName, true) && + $this->yellow->toolbox->createFile($page->fileName, $page->rawData); + } else { + $ok = $this->yellow->toolbox->renameDirectory(dirname($fileName), dirname($page->fileName), true) && + $this->yellow->toolbox->createFile($page->fileName, $page->rawData); + } + if ($ok) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't write file '$page->fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } else { + $this->yellow->page->error(500, $page->errorMessage); + $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->isUserAccess("delete", $location) && is_file($fileName)) { + $this->response->rawDataSource = $this->yellow->page->getRequest("rawdatasource"); + $this->response->rawDataEdit = $this->yellow->page->getRequest("rawdatasource"); + $this->response->rawDataEndOfLine = $this->yellow->page->getRequest("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)) { + $ok = $this->response->deleteFileLocation($location, $fileName); + } else { + $ok = $this->response->deleteDirectoryLocation($location, $fileName); + } + if ($ok) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't delete file '$fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } else { + $this->yellow->page->error(500, $page->errorMessage); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request to restore deleted page + public function processRequestRestore($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->response->isUserAccess("restore", $location) && !is_file($fileName)) { + $page = $this->response->getPageRestore($scheme, $address, $base, $location, $fileName); + if (!$page->isError()) { + if ($this->yellow->lookup->isFileLocation($location)) { + $ok = $this->response->restoreFileLocation($location); + } else { + $ok = $this->response->restoreDirectoryLocation($location); + } + if ($ok) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $this->yellow->page->error(500, "Can't restore file '$fileName'!"); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } else { + $this->yellow->page->error(500, $page->errorMessage); + $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false); + } + } + return $statusCode; + } + + // Process request to show preview + public function processRequestPreview($scheme, $address, $base, $location, $fileName) { + $page = $this->response->getPagePreview($scheme, $address, $base, $location, $fileName, + $this->yellow->page->getRequest("rawdataedit"), $this->yellow->page->getRequest("rawdataendofline")); + $page->headerData = array( + "Cache-Control"=>"no-cache, no-store", + "Content-Type"=>$this->yellow->toolbox->getMimeContentType("a.html"), + "Last-Modified"=>$this->yellow->toolbox->getHttpDateFormatted(time())); + $statusCode = $this->yellow->sendData($page->statusCode, $page->headerData, $page->outputData); + if ($this->yellow->system->get("coreDebugMode")>=1) echo "YellowEdit::processRequestPreview file:$fileName<br/>\n"; + return $statusCode; + } + + // 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->isUserAccess("upload", $location) && 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'!"; + } + $headerData = array( + "Cache-Control"=>"no-cache, no-store", + "Content-Type"=>$this->yellow->toolbox->getMimeContentType("a.json"), + "Last-Modified"=>$this->yellow->toolbox->getHttpDateFormatted(time())); + return $this->yellow->sendData(isset($data["error"]) ? 500 : 200, $headerData, json_encode($data)); + } + + // Check user authentication + public function checkUserAuth($scheme, $address, $base, $location, $fileName) { + $action = $this->yellow->page->getRequest("action"); + $authToken = $this->yellow->toolbox->getCookie("authtoken"); + $csrfToken = $this->yellow->toolbox->getCookie("csrftoken"); + if (is_string_empty($action) || $this->isRequestSameSite("POST", $scheme, $address)) { + if ($action=="login") { + $email = $this->yellow->page->getRequest("email"); + $password = $this->yellow->page->getRequest("password"); + if ($this->response->checkAuthLogin($email, $password)) { + $this->response->createCookies($scheme, $address, $base, $email); + $this->response->userEmail = $email; + $this->response->language = $this->getUserLanguage($email); + } else { + $this->response->userFailedError = "login"; + $this->response->userFailedEmail = $email; + $this->response->userFailedExpire = PHP_INT_MAX; + } + } elseif (!is_string_empty($authToken) && !is_string_empty($csrfToken)) { + $csrfTokenReceived = isset($_POST["csrftoken"]) ? $_POST["csrftoken"] : ""; + $csrfTokenIrrelevant = is_string_empty($action); + if ($this->response->checkAuthToken($authToken, $csrfToken, $csrfTokenReceived, $csrfTokenIrrelevant)) { + $this->response->userEmail = $email = $this->response->getAuthEmail($authToken); + $this->response->language = $this->getUserLanguage($email); + } else { + $this->response->userFailedError = "auth"; + $this->response->userFailedEmail = $this->response->getAuthEmail($authToken); + $this->response->userFailedExpire = $this->response->getAuthExpire($authToken); + } + } + $this->yellow->user->set($this->response->userEmail); + } + return $this->response->isUser(); + } + + // Check user without authentication + public function checkUserUnauth($scheme, $address, $base, $location, $fileName) { + $ok = false; + $action = $this->yellow->page->getRequest("action"); + if (is_string_empty($action) || $action=="signup" || $action=="forgot") { + $ok = true; + } elseif ($this->yellow->page->isRequest("actiontoken")) { + $actionToken = $this->yellow->page->getRequest("actiontoken"); + $email = $this->yellow->page->getRequest("email"); + $action = $this->yellow->page->getRequest("action"); + $expire = $this->yellow->page->getRequest("expire"); + $language = $this->yellow->page->getRequest("language"); + if ($this->response->checkActionToken($actionToken, $email, $action, $expire)) { + $ok = true; + $this->response->language = $this->getActionLanguage($language); + } else { + $this->response->userFailedError = "action"; + $this->response->userFailedEmail = $email; + $this->response->userFailedExpire = $expire; + } + } + return $ok; + } + + // Check user failed + public function checkUserFailed($scheme, $address, $base, $location, $fileName) { + if (!is_string_empty($this->response->userFailedError)) { + if ($this->response->userFailedExpire>time() && $this->yellow->user->isExisting($this->response->userFailedEmail)) { + $email = $this->response->userFailedEmail; + $failed = $this->yellow->user->getUser("failed", $email)+1; + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $status = $this->yellow->user->save($fileNameUser, $email, array("failed" => $failed)) ? "ok" : "error"; + if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + if ($failed==$this->yellow->system->get("editBruteForceProtection")) { + $statusBeforeProtection = $this->yellow->user->getUser("status", $email); + $statusAfterProtection = ($statusBeforeProtection=="active" || $statusBeforeProtection=="inactive") ? "inactive" : "failed"; + if ($status=="ok") { + $status = $this->yellow->user->save($fileNameUser, $email, array("status" => $statusAfterProtection)) ? "ok" : "error"; + if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!"); + } + if ($status=="ok" && $statusBeforeProtection=="active") { + $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->yellow->user->getUser("status", $email)==$statusExpected ? "ok" : "done"; + } + + // Return user account changes + public function getUserAccount($action, $email, $password) { + $status = null; + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditUserAccount")) { + $status = $value["object"]->onEditUserAccount($action, $email, $password); + if (!is_null($status)) break; + } + } + if (is_null($status)) { + $status = "ok"; + if (!is_string_empty($password) && strlenu($password)<$this->yellow->system->get("editUserPasswordMinLength")) $status = "short"; + if (!is_string_empty($password) && $password==$email) $status = "weak"; + if (!is_string_empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) $status = "invalid"; + } + return $status; + } + + // Return user language + public function getUserLanguage($email) { + $language = $this->yellow->user->getUser("language", $email); + if (!$this->yellow->language->isExisting($language)) $language = $this->yellow->system->get("language"); + return $language; + } + + // Return action language + public function getActionLanguage($language) { + if (!$this->yellow->language->isExisting($language)) $language = $this->yellow->system->get("language"); + return $language; + } + + // Check if user account is taken + public function isUserAccountTaken($email) { + $taken = false; + if ($this->yellow->user->isExisting($email)) { + $status = $this->yellow->user->getUser("status", $email); + $reserved = strtotime($this->yellow->user->getUser("modified", $email)) + 60*60*24; + if ($status=="active" || $status=="inactive" || $reserved>time()) $taken = true; + } + return $taken; + } + + // Check if request came from same site + public function isRequestSameSite($method, $scheme, $address) { + $origin = ""; + if (preg_match("#^(\w+)://([^/]+)(.*)$#", $this->yellow->toolbox->getServer("HTTP_REFERER"), $matches)) $origin = "$matches[1]://$matches[2]"; + if ($this->yellow->toolbox->getServer("HTTP_ORIGIN")) $origin = $this->yellow->toolbox->getServer("HTTP_ORIGIN"); + return $this->yellow->toolbox->getServer("REQUEST_METHOD")==$method && $origin=="$scheme://$address"; + } + + // Check if edit location + public function isEditLocation($location) { + $locationLength = strlenu($this->yellow->system->get("editLocation")); + return substru($location, 0, $locationLength)==$this->yellow->system->get("editLocation"); + } +} + +class YellowEditResponse { + public $yellow; // access to API + public $extension; // access to extension + public $userEmail; // user email + public $userFailedError; // error of failed authentication + public $userFailedEmail; // email of failed authentication + public $userFailedExpire; // expiration time of failed authentication + public $rawDataSource; // raw data of page for comparison + public $rawDataEdit; // raw data of page for editing + public $rawDataOutput; // raw data of dynamic output + public $rawDataReadonly; // raw data is read only? (boolean) + public $rawDataEndOfLine; // end of line format for raw data + public $language; // response language + public $action; // response action + public $status; // response status + + public function __construct($yellow) { + $this->yellow = $yellow; + $this->extension = $yellow->extension->get("edit"); + $this->userEmail = ""; + } + + // Process page data + public function processPageData($page) { + if ($this->isUser()) { + if (is_string_empty($this->rawDataSource)) $this->rawDataSource = $page->rawData; + if (is_string_empty($this->rawDataEdit)) $this->rawDataEdit = $page->rawData; + if (is_string_empty($this->rawDataEndOfLine)) $this->rawDataEndOfLine = $this->getEndOfLine($page->rawData); + if ($page->statusCode==404 || $this->yellow->toolbox->isLocationArguments()) { + $this->rawDataEdit = $this->getRawDataGenerated($page); + $this->rawDataReadonly = true; + } + if ($page->statusCode==434 || $page->statusCode==435) { + $this->rawDataEdit = $this->getRawDataNew($page, true); + $this->rawDataReadonly = false; + } + } + if (is_string_empty($this->language)) $this->language = $page->get("language"); + if (is_string_empty($this->action)) $this->action = $this->isUser() ? "none" : "login"; + if (is_string_empty($this->status)) $this->status = "none"; + if ($this->status=="error") $this->action = "error"; + } + + // Return new page + public function getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $page->parseMeta($rawData); + $this->editContentFile($page, "create", $this->userEmail); + if ($this->yellow->content->find($page->location)) { + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation")); + $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); + while ($this->yellow->content->find($page->location) || is_string_empty($page->fileName)) { + $page->rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData)); + $page->rawData = $this->yellow->lookup->normaliseLines($page->rawData, $endOfLine); + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation")); + $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); + if (++$pageCounter>999) break; + } + if ($this->yellow->content->find($page->location) || is_string_empty($page->fileName)) { + $page->error(500, "Page '".$page->get("title")."' is not possible!"); + } + } else { + $page->fileName = $this->getPageNewFile($page->location); + } + if (!$this->isUserAccess("create", $page->location)) { + $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) { + $rawDataSource = $this->yellow->lookup->normaliseLines($rawDataSource, $endOfLine); + $rawDataEdit = $this->yellow->lookup->normaliseLines($rawDataEdit, $endOfLine); + $rawDataFile = $this->yellow->lookup->normaliseLines($rawDataFile, $endOfLine); + $rawData = $this->extension->merge->merge($rawDataSource, $rawDataEdit, $rawDataFile); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $page->parseMeta($rawData); + $pageSource = new YellowPage($this->yellow); + $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $pageSource->parseMeta($rawDataSource); + $this->editContentFile($page, "edit", $this->userEmail); + if ($this->isMetaModified($pageSource, $page) && $page->location!=$this->yellow->content->getHomeLocation($page->location)) { + $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("editNewLocation"), true); + $page->fileName = $this->getPageNewFile($page->location, $page->fileName, $page->get("published")); + if ($page->location!=$pageSource->location && ($this->yellow->content->find($page->location) || is_string_empty($page->fileName))) { + $page->error(500, "Page '".$page->get("title")."' is not possible!"); + } + } + if (is_string_empty($page->rawData)) $page->error(500, "Page has been modified by someone else!"); + if (!$this->isUserAccess("edit", $page->location) || + !$this->isUserAccess("edit", $pageSource->location)) { + $page->error(500, "Page '".$page->get("title")."' is restricted!"); + } + return $page; + } + + // Return deleted page + public function getPageDelete($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $page->parseMeta($rawData); + $this->editContentFile($page, "delete", $this->userEmail); + if (!$this->isUserAccess("delete", $page->location)) { + $page->error(500, "Page '".$page->get("title")."' is restricted!"); + } + return $page; + } + + // Return restored page + public function getPageRestore($scheme, $address, $base, $location, $fileName) { + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $page->parseMeta(""); + $this->editContentFile($page, "restore", $this->userEmail); + if (!$this->isUserAccess("restore", $page->location)) { + $page->error(500, "Page '".$page->get("title")."' is restricted!"); + } + return $page; + } + + // Return preview page + public function getPagePreview($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) { + $rawData = $this->yellow->lookup->normaliseLines($rawData, $endOfLine); + $page = new YellowPage($this->yellow); + $page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $page->parseMeta($rawData, 200); + $this->yellow->language->set($page->get("language")); + $class = "page-preview layout-".$page->get("layout"); + $output = "<div class=\"".htmlspecialchars($class)."\"><div class=\"content\"><div class=\"main\">"; + if ($this->yellow->system->get("editToolbarButtons")!="none") $output .= "<h1>".$page->getHtml("titleContent")."</h1>\n"; + $output .= $page->getContentHtml(); + $output .= "</div></div></div>"; + $page->statusCode = 200; + $page->outputData = $output; + return $page; + } + + // Return uploaded file + public function getFileUpload($scheme, $address, $base, $pageLocation, $fileNameTemp, $fileNameShort) { + $file = new YellowPage($this->yellow); + $file->setRequestInformation($scheme, $address, $base, "/".$fileNameTemp, $fileNameTemp, false); + $file->parseMeta(null); + $file->set("fileNameShort", $fileNameShort); + $file->set("type", $this->yellow->toolbox->getFileType($fileNameShort)); + if ($file->get("type")=="html" || $file->get("type")=="svg") { + $fileData = $this->yellow->toolbox->readFile($fileNameTemp); + $fileData = $this->yellow->lookup->normaliseData($fileData, $file->get("type")); + if (is_string_empty($fileData) || !$this->yellow->toolbox->createFile($fileNameTemp, $fileData)) { + $file->error(500, "Can't write file '$fileNameTemp'!"); + } + } + $this->editMediaFile($file, "upload", $this->userEmail); + $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 system file + public function getFileSystem($scheme, $address, $base, $pageLocation, $fileNameSystem, $settings) { + $file = new YellowPage($this->yellow); + $file->setRequestInformation($scheme, $address, $base, "/".$fileNameSystem, $fileNameSystem, false); + $file->parseMeta(null); + foreach ($settings as $key=>$value) $file->set($key, $value); + $this->editSystemFile($file, "configure", $this->userEmail); + return $file; + } + + // Return page data including status information + public function getPageData($page) { + $data = array(); + $data["scheme"] = $this->yellow->page->scheme; + $data["address"] = $this->yellow->page->address; + $data["base"] = $this->yellow->page->base; + $data["location"] = $this->yellow->page->location; + if ($this->isUser()) { + $data["title"] = $this->yellow->toolbox->getMetaData($this->rawDataEdit, "title"); + $data["rawDataSource"] = $this->rawDataSource; + $data["rawDataEdit"] = $this->rawDataEdit; + $data["rawDataNew"] = $this->getRawDataNew($page); + $data["rawDataOutput"] = strval($this->rawDataOutput); + $data["rawDataReadonly"] = intval($this->rawDataReadonly); + $data["rawDataEndOfLine"] = $this->rawDataEndOfLine; + } + 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 + public function getSystemData() { + $data = array(); + $data["coreServerScheme"] = $this->yellow->system->get("coreServerScheme"); + $data["coreServerAddress"] = $this->yellow->system->get("coreServerAddress"); + $data["coreServerBase"] = $this->yellow->system->get("coreServerBase"); + $data["coreDebugMode"] = $this->yellow->system->get("coreDebugMode"); + $data = array_merge($data, $this->yellow->system->getSettings("", "Location")); + if ($this->isUser()) { + $data["coreFileSizeMax"] = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize")); + $data["coreProductRelease"] = "Datenstrom Yellow ".YellowCore::RELEASE; + $data["coreExtensions"] = array(); + foreach ($this->yellow->extension->data as $key=>$value) { + $data["coreExtensions"][$key] = $value["class"]; + } + $data["coreLanguages"] = array(); + foreach ($this->yellow->system->getAvailable("language") as $language) { + $data["coreLanguages"][$language] = $this->yellow->language->getTextHtml("languageDescription", $language); + } + $data["editSettingsActions"] = $this->getSettingsActions(); + $data["editUploadExtensions"] = $this->yellow->system->get("editUploadExtensions"); + $data["editKeyboardShortcuts"] = $this->yellow->system->get("editKeyboardShortcuts"); + $data["editToolbarButtons"] = $this->getToolbarButtons(); + $data["editStatusValues"] = $this->getStatusValues(); + $data["emojiToolbarButtons"] = $this->yellow->system->get("emojiToolbarButtons"); + $data["iconToolbarButtons"] = $this->yellow->system->get("iconToolbarButtons"); + if ($this->isUserAccess("configure")) { + $data["sitename"] = $this->yellow->system->get("sitename"); + $data["author"] = $this->yellow->system->get("author"); + $data["email"] = $this->yellow->system->get("email"); + } + } else { + $data["editLoginEmail"] = $this->yellow->page->get("editLoginEmail"); + $data["editLoginPassword"] = $this->yellow->page->get("editLoginPassword"); + $data["editLoginRestriction"] = intval($this->isLoginRestriction()); + } + return $data; + } + + // Return user data + public function getUserData() { + $data = array(); + if ($this->isUser()) { + $data["email"] = $this->userEmail; + $data["name"] = $this->yellow->user->getUser("name", $this->userEmail); + $data["description"] = $this->yellow->user->getUser("description", $this->userEmail); + $data["language"] = $this->yellow->user->getUser("language", $this->userEmail); + $data["status"] = $this->yellow->user->getUser("status", $this->userEmail); + $data["access"] = $this->yellow->user->getUser("access", $this->userEmail); + $data["home"] = $this->yellow->user->getUser("home", $this->userEmail); + } + return $data; + } + + // Return language data + public function getLanguageData() { + $dataLanguage = $this->yellow->language->getSettings("language", "", $this->language); + $dataEdit = $this->yellow->language->getSettings("edit", "", $this->language); + return array_merge($dataLanguage, $dataEdit); + } + + // Return request data + public function getRequestData() { + $data = array(); + foreach ($_REQUEST as $key=>$value) { + if ($key=="password" || $key=="authtoken" || $key=="csrftoken" || $key=="actiontoken" || substru($key, 0, 7)=="rawdata") continue; + $data["request".ucfirst($key)] = trim($value); + } + return $data; + } + + // Return settings actions + public function getSettingsActions() { + $settingsActions = "account"; + if ($this->isUserAccess("configure")) $settingsActions .= ", configure"; + if ($this->isUserAccess("update")) $settingsActions .= ", update"; + return $settingsActions=="account" ? "none" : $settingsActions; + } + + // Return toolbar buttons + public function getToolbarButtons() { + $toolbarButtons = $this->yellow->system->get("editToolbarButtons"); + if ($toolbarButtons=="auto") { + $toolbarButtons = "format, bold, italic, strikethrough, code, separator, list, link, file"; + if ($this->yellow->extension->isExisting("emoji")) $toolbarButtons .= ", emoji"; + if ($this->yellow->extension->isExisting("icon")) $toolbarButtons .= ", icon"; + $toolbarButtons .= ", status, preview"; + } + return $toolbarButtons; + } + + // Return status values + public function getStatusValues() { + $statusValues = ""; + if ($this->yellow->extension->isExisting("private")) $statusValues .= ", private"; + if ($this->yellow->extension->isExisting("draft")) $statusValues .= ", draft"; + $statusValues .= ", unlisted"; + return ltrim($statusValues, ", "); + } + + // Return end of line format + public function getEndOfLine($rawData = "") { + $endOfLine = $this->yellow->system->get("editEndOfLine"); + if ($endOfLine=="auto") { + $rawData = is_string_empty($rawData) ? PHP_EOL : substru($rawData, 0, 4096); + $endOfLine = strposu($rawData, "\r")===false ? "lf" : "crlf"; + } + return $endOfLine; + } + + // Return update information + public function getUpdateInformation() { + $statusCode = 200; + $rawData = ""; + if ($this->yellow->extension->isExisting("update")) { + list($statusCodeCurrent, $settingsCurrent) = $this->yellow->extension->get("update")->getExtensionSettings(false); + list($statusCodeLatest, $settingsLatest) = $this->yellow->extension->get("update")->getExtensionSettings(true); + $statusCode = max($statusCodeCurrent, $statusCodeLatest); + foreach ($settingsCurrent as $key=>$value) { + if ($settingsLatest->isExisting($key)) { + $versionCurrent = $settingsCurrent[$key]->get("version"); + $versionLatest = $settingsLatest[$key]->get("version"); + if (strnatcasecmp($versionCurrent, $versionLatest)<0) { + $rawData .= htmlspecialchars(ucfirst($key)." $versionLatest")."<br />"; + } + } + } + if (!is_string_empty($rawData)) $rawData = "<p>$rawData</p>\n"; + } + return array($statusCode, $rawData); + } + + // Return raw data for generated page + public function getRawDataGenerated($page) { + $title = $page->get("title"); + $text = $this->yellow->language->getText("editDataGenerated", $page->get("language")); + return "---\nTitle: $title\n---\n$text"; + } + + // Return raw data for new page + public function getRawDataNew($page, $customTitle = false) { + $fileName = ""; + 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)."shared/"; + $fileName = $this->yellow->lookup->findFileFromContentLocation($location, true).$this->yellow->system->get("editNewFile"); + $fileName = str_replace("(.*)", $name, $fileName); + if (is_file($fileName)) break; + } + } + if (!is_file($fileName)) { + $name = $this->yellow->lookup->normaliseName($this->yellow->system->get("layout")); + $location = $this->yellow->content->getHomeLocation($page->location)."shared/"; + $fileName = $this->yellow->lookup->findFileFromContentLocation($location, true).$this->yellow->system->get("editNewFile"); + $fileName = str_replace("(.*)", $name, $fileName); + } + if (is_file($fileName)) { + $rawData = $this->yellow->toolbox->readFile($fileName); + $rawData = preg_replace("/@timestamp/i", time(), $rawData); + $rawData = preg_replace("/@datetime/i", date("Y-m-d H:i:s"), $rawData); + $rawData = preg_replace("/@date/i", date("Y-m-d"), $rawData); + $rawData = preg_replace("/@usershort/i", strtok($this->yellow->user->getUser("name", $this->userEmail), " "), $rawData); + $rawData = preg_replace("/@username/i", $this->yellow->user->getUser("name", $this->userEmail), $rawData); + $rawData = preg_replace("/@userlanguage/i", $this->yellow->user->getUser("language", $this->userEmail), $rawData); + } else { + $rawData = "---\nTitle: Page\n---\n"; + } + 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, $editNewLocation, $pageMatchLocation = false) { + $location = is_string_empty($editNewLocation) ? "@title" : $editNewLocation; + $location = preg_replace("/@title/i", $this->getPageNewTitle($rawData), $location); + $location = preg_replace("/@timestamp/i", $this->getPageNewData($rawData, "published", "U"), $location); + $location = preg_replace("/@date/i", $this->getPageNewData($rawData, "published", "Y-m-d"), $location); + $location = preg_replace("/@year/i", $this->getPageNewData($rawData, "published", "Y"), $location); + $location = preg_replace("/@month/i", $this->getPageNewData($rawData, "published", "m"), $location); + $location = preg_replace("/@day/i", $this->getPageNewData($rawData, "published", "d"), $location); + $location = preg_replace("/@tag/i", $this->getPageNewData($rawData, "tag"), $location); + $location = preg_replace("/@author/i", $this->getPageNewData($rawData, "author"), $location); + if (!preg_match("/^\//", $location)) { + if ($this->yellow->lookup->isFileLocation($pageLocation) || !$pageMatchLocation) { + $location = $this->yellow->lookup->getDirectoryLocation($pageLocation).$location; + } else { + $location = $this->yellow->lookup->getDirectoryLocation(rtrim($pageLocation, "/")).$location; + } + } + if (preg_match("/\d/", $location)) { + $locationNew = ""; + $tokens = explode("/", $location); + for ($i=1; $i<count($tokens); ++$i) { + $locationNew .= "/".$this->yellow->lookup->normaliseToken($tokens[$i]); + } + $location = $locationNew; + } + if ($pageMatchLocation) { + $location = rtrim($location, "/").($this->yellow->lookup->isFileLocation($pageLocation) ? "" : "/"); + } + 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 = is_string_empty($titleSlug) ? $title : $titleSlug; + $value = $this->yellow->lookup->normaliseName($value, false, false, true); + return trim(preg_replace("/-+/", "-", $value), "-"); + } + + // Return data for new/modified page + public function getPageNewData($rawData, $key, $dateFormat = "") { + $value = $this->yellow->toolbox->getMetaData($rawData, $key); + if (preg_match("/^(.*?)\,(.*)$/", $value, $matches)) $value = $matches[1]; + if (!is_string_empty($dateFormat)) $value = date($dateFormat, strtotime($value)); + if (is_string_empty($value)) $value = "none"; + $value = $this->yellow->lookup->normaliseName($value, false, false, true); + return trim(preg_replace("/-+/", "-", $value), "-"); + } + + // Return file name for new/modified page + public function getPageNewFile($location, $pageFileName = "", $pagePrefix = "") { + $fileName = $this->yellow->lookup->findFileFromContentLocation($location); + if (!is_string_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 = is_string_empty($pageFileName) ? $fileName : $pageFileName; + } + $prefix = $this->getPageNewPrefix($location, $pageFileName, $pagePrefix); + if ($this->yellow->lookup->isFileLocation($location)) { + if (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("coreContentExtension"); + } + } else { + if (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("coreContentDefaultFile"); + } + } + } + return $fileName; + } + + // Return prefix for new/modified page + public function getPageNewPrefix($location, $pageFileName, $pagePrefix) { + if (is_string_empty($pagePrefix)) { + if ($this->yellow->lookup->isFileLocation($location)) { + if (preg_match("#^(.*)\/(.+?)$#", $pageFileName, $matches)) $pagePrefix = $matches[2]; + } else { + if (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 = is_string_empty($fileNewLocation) ? $this->yellow->system->get("editUploadNewLocation") : $fileNewLocation; + $location = preg_replace("/@timestamp/i", time(), $location); + $location = preg_replace("/@date/i", date("Y-m-d"), $location); + $location = preg_replace("/@type/i", $this->yellow->toolbox->getFileType($fileNameShort), $location); + $location = preg_replace("/@group/i", $this->getFileNewGroup($fileNameShort), $location); + $location = preg_replace("/@folder/i", $this->getFileNewFolder($pageLocation), $location); + $location = preg_replace("/@filename/i", strtoloweru($fileNameShort), $location); + if (!preg_match("/^\//", $location)) { + $location = $this->yellow->system->get("coreMediaLocation").$location; + } + return $location; + } + + // Return group for new file + public function getFileNewGroup($fileNameShort) { + $group = "none"; + $fileType = $this->yellow->toolbox->getFileType($fileNameShort); + $locationMedia = $this->yellow->system->get("coreMediaLocation"); + $locationGroup = $this->yellow->system->get("coreDownloadLocation"); + if (preg_match("/(gif|jpg|png|svg)$/", $fileType)) { + $locationGroup = $this->yellow->system->get("coreImageLocation"); + } + if (preg_match("#^$locationMedia(.+?)\/#", $locationGroup, $matches)) { + $group = strtoloweru($matches[1]); + } + return $group; + } + + // 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) { + $fileText = $fileNumber = $fileExtension = ""; + if (preg_match("/^(.*?)(\d*)(\..*?)?$/", $fileNameShort, $matches)) { + $fileText = $matches[1]; + $fileNumber = is_string_empty($matches[2]) ? "-2" : $matches[2]+1; + $fileExtension = $matches[3]; + } + return $fileText.$fileNumber.$fileExtension; + } + + // Return next title + public function getTitleNext($rawData) { + $titleText = $titleNumber = ""; + if (preg_match("/^(.*?)(\d*)$/", $this->yellow->toolbox->getMetaData($rawData, "title"), $matches)) { + $titleText = $matches[1]; + $titleNumber = is_string_empty($matches[2]) ? " 2" : $matches[2]+1; + } + return $titleText.$titleNumber; + } + + // Send mail to user + public function sendMail($scheme, $address, $base, $email, $action) { + if ($action=="approve") { + $userName = $this->yellow->system->get("author"); + $userEmail = $this->yellow->system->get("email"); + $userLanguage = $this->extension->getUserLanguage($userEmail); + } else { + $userName = $this->yellow->user->getUser("name", $email); + $userEmail = $email; + $userLanguage = $this->extension->getUserLanguage($email); + } + if ($action=="welcome" || $action=="goodbye") { + $url = "$scheme://$address$base/"; + } else { + $expire = time() + 60*60*24; + $actionToken = $this->createActionToken($email, $action, $expire); + $locationArguments = "/action:$action/email:$email/expire:$expire/language:$userLanguage/actiontoken:$actionToken/"; + $url = "$scheme://$address$base".$this->yellow->lookup->normaliseArguments($locationArguments, false, false); + } + $prefix = "edit".ucfirst($action); + $message = $this->yellow->language->getText("{$prefix}Message", $userLanguage); + $message = str_replace("\\n", "\r\n", $message); + $message = preg_replace("/@useraccount/i", $email, $message); + $message = preg_replace("/@usershort/i", strtok($userName, " "), $message); + $message = preg_replace("/@username/i", $userName, $message); + $message = preg_replace("/@userlanguage/i", $userLanguage, $message); + $sitename = $this->yellow->system->get("sitename"); + $siteEmail = $this->yellow->system->get("editSiteEmail"); + $subject = $this->yellow->language->getText("{$prefix}Subject", $userLanguage); + $footer = $this->yellow->language->getText("editMailFooter", $userLanguage); + $footer = str_replace("\\n", "\r\n", $footer); + $footer = preg_replace("/@sitename/i", $sitename, $footer); + $mailHeaders = array( + "To" => $this->yellow->lookup->normaliseAddress("$userName <$userEmail>"), + "From" => $this->yellow->lookup->normaliseAddress("$sitename <$siteEmail>"), + "Subject" => $subject, + "Date" => date(DATE_RFC2822), + "Mime-Version" => "1.0", + "Content-Type" => "text/plain; charset=utf-8", + "X-Request-Url" => "$scheme://$address$base"); + $mailMessage = "$message\r\n\r\n$url\r\n-- \r\n$footer"; + return $this->yellow->toolbox->mail($action, $mailHeaders, $mailMessage); + } + + // Create browser cookies + public function createCookies($scheme, $address, $base, $email) { + $expire = time() + $this->yellow->system->get("editLoginSessionTimeout"); + $authToken = $this->createAuthToken($email, $expire); + $csrfToken = $this->createCsrfToken(); + setcookie("authtoken", $authToken, $expire, "$base/", "", $scheme=="https", true); + setcookie("csrftoken", $csrfToken, $expire, "$base/", "", $scheme=="https", false); + } + + // Destroy browser cookies + public function destroyCookies($scheme, $address, $base) { + setcookie("authtoken", "", 1, "$base/"); + setcookie("csrftoken", "", 1, "$base/"); + } + + // Create authentication token + public function createAuthToken($email, $expire) { + $hash = $this->yellow->user->getUser("hash", $email); + $signature = $this->yellow->toolbox->createHash($hash."auth".$expire, "sha256"); + if (is_string_empty($signature)) $signature = "padd"."error-hash-algorithm-sha256"; + return substrb($signature, 4).$this->yellow->user->getUser("stamp", $email).dechex($expire); + } + + // Create action token + public function createActionToken($email, $action, $expire) { + $hash = $this->yellow->user->getUser("hash", $email); + $signature = $this->yellow->toolbox->createHash($hash.$action.$expire, "sha256"); + if (is_string_empty($signature)) $signature = "padd"."error-hash-algorithm-sha256"; + return substrb($signature, 4); + } + + // 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 (is_string_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; + } + + // Check user authentication from email and password + public function checkAuthLogin($email, $password) { + $algorithm = $this->yellow->system->get("editUserHashAlgorithm"); + $hash = $this->yellow->user->getUser("hash", $email); + return $this->yellow->user->getUser("status", $email)=="active" && + $this->yellow->toolbox->verifyHash($password, $algorithm, $hash); + } + + // Check user authentication from tokens + public function checkAuthToken($authToken, $csrfTokenExpected, $csrfTokenReceived, $csrfTokenIrrelevant) { + $signature = "$5y$".substrb($authToken, 0, 96); + $email = $this->getAuthEmail($authToken); + $expire = $this->getAuthExpire($authToken); + $hash = $this->yellow->user->getUser("hash", $email); + return $expire>time() && $this->yellow->user->getUser("status", $email)=="active" && + $this->yellow->toolbox->verifyHash($hash."auth".$expire, "sha256", $signature) && + ($this->yellow->toolbox->verifyToken($csrfTokenExpected, $csrfTokenReceived) || $csrfTokenIrrelevant); + } + + // Check action token + public function checkActionToken($actionToken, $email, $action, $expire) { + $signature = "$5y$".$actionToken; + $hash = $this->yellow->user->getUser("hash", $email); + return $expire>time() && $this->yellow->user->isExisting($email) && + $this->yellow->toolbox->verifyHash($hash.$action.$expire, "sha256", $signature); + } + + // Return user email from authentication, timing attack safe email lookup + public function getAuthEmail($authToken, $stamp = "") { + $email = ""; + if (is_string_empty($stamp)) $stamp = substrb($authToken, 96, 20); + foreach ($this->yellow->user->settings as $key=>$value) { + if ($this->yellow->toolbox->verifyToken($value["stamp"], $stamp)) $email = $key; + } + return $email; + } + + // Return expiration time from authentication + public function getAuthExpire($authToken) { + return hexdec(substrb($authToken, 96+20)); + } + + // Change content file + public function editContentFile($page, $action, $email) { + if (!$page->isError()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditContentFile")) $value["object"]->onEditContentFile($page, $action, $email); + } + } + } + + // Change media file + public function editMediaFile($file, $action, $email) { + if (!$file->isError()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditMediaFile")) $value["object"]->onEditMediaFile($file, $action, $email); + } + } + } + + // Change system file + public function editSystemFile($file, $action, $email) { + if (!$file->isError()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onEditSystemFile")) $value["object"]->onEditSystemFile($file, $action, $email); + } + } + } + + // Delete file + public function deleteFileLocation($location, $fileName) { + $rawData = $this->yellow->toolbox->readFile($fileName); + $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalLocation", $location); + $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalFileName", $fileName); + return $this->yellow->toolbox->createFile($fileName, $rawData) && + $this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory")); + } + + // Delete directory + public function deleteDirectoryLocation($location, $fileName) { + $rawData = $this->yellow->toolbox->readFile($fileName); + $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalLocation", $location); + $rawData = $this->yellow->toolbox->setMetaData($rawData, "pageOriginalFileName", $fileName); + return $this->yellow->toolbox->createFile($fileName, $rawData) && + $this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->system->get("coreTrashDirectory")); + } + + // Restore deleted file from trash + public function restoreFileLocation($location) { + $fileNameDeleted = $fileNameRestored = ""; + $deleted = 0; + $pathTrash = $this->yellow->system->get("coreTrashDirectory"); + $regex = "/^.*\\".$this->yellow->system->get("coreContentExtension")."$/"; + foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, $regex, false, false) as $entry) { + $rawDataOriginal = $this->yellow->toolbox->readFile($entry); + $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation"); + $fileNameOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalFileName"); + $deletedOriginal = $this->yellow->toolbox->getFileDeleted($entry); + if ($location==$locationOriginal && $deleted<=$deletedOriginal) { + $fileNameDeleted = $entry; + $fileNameRestored = $fileNameOriginal; + $rawDataRestored = $rawDataOriginal; + $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalLocation"); + $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalFileName"); + $deleted = $deletedOriginal; + } + } + return !is_string_empty($fileNameDeleted) && $this->yellow->lookup->isContentFile($fileNameRestored) && + $this->yellow->toolbox->renameFile($fileNameDeleted, $fileNameRestored, true) && + $this->yellow->toolbox->createFile($fileNameRestored, $rawDataRestored); + } + + // Restore deleted directory from trash + public function restoreDirectoryLocation($location) { + $pathDeleted = $fileNameRestored = ""; + $deleted = 0; + $pathTrash = $this->yellow->system->get("coreTrashDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, "/.*/", false, true) as $entry) { + $fileName = $entry."/".$this->yellow->system->get("coreContentDefaultFile"); + if (!is_file($fileName)) continue; + $rawDataOriginal = $this->yellow->toolbox->readFile($fileName); + $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation"); + $fileNameOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalFileName"); + $deletedOriginal = $this->yellow->toolbox->getFileDeleted($entry); + if ($location==$locationOriginal && $deleted<=$deletedOriginal) { + $pathDeleted = $entry; + $fileNameRestored = $fileNameOriginal; + $rawDataRestored = $rawDataOriginal; + $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalLocation"); + $rawDataRestored = $this->yellow->toolbox->unsetMetaData($rawDataRestored, "pageOriginalFileName"); + $deleted = $deletedOriginal; + } + } + return !is_string_empty($pathDeleted) && $this->yellow->lookup->isContentFile($fileNameRestored) && + $this->yellow->toolbox->renameDirectory($pathDeleted, dirname($fileNameRestored), true) && + $this->yellow->toolbox->createFile($fileNameRestored, $rawDataRestored); + } + + // Check if location has been deleted + public function isDeletedLocation($location) { + $found = false; + $pathTrash = $this->yellow->system->get("coreTrashDirectory"); + $regex = "/^.*\\".$this->yellow->system->get("coreContentExtension")."$/"; + $fileNames = $this->yellow->toolbox->getDirectoryEntries($pathTrash, $regex, false, false); + foreach ($this->yellow->toolbox->getDirectoryEntries($pathTrash, "/.*/", false, true) as $entry) { + $fileName = $entry."/".$this->yellow->system->get("coreContentDefaultFile"); + if (is_file($fileName)) array_push($fileNames, $fileName); + } + foreach ($fileNames as $fileName) { + $rawDataOriginal = $this->yellow->toolbox->readFile($fileName, 4096); + $locationOriginal = $this->yellow->toolbox->getMetaData($rawDataOriginal, "pageOriginalLocation"); + if ($location==$locationOriginal) { + $found = true; + break; + } + } + return $found; + } + + // Check if meta data has been modified + public function isMetaModified($pageSource, $pageOther) { + return substrb($pageSource->rawData, 0, $pageSource->metaDataOffsetBytes) != + substrb($pageOther->rawData, 0, $pageOther->metaDataOffsetBytes); + } + + // Check if login with restriction + public function isLoginRestriction() { + return $this->yellow->system->get("editLoginRestriction"); + } + + // Check if user is logged in + public function isUser() { + return !is_string_empty($this->userEmail); + } + + // Check if user with access + public function isUserAccess($action, $location = "") { + $userHome = $this->yellow->user->getUser("home", $this->userEmail); + $tokens = preg_split("/\s*,\s*/", $this->yellow->user->getUser("access", $this->userEmail)); + return in_array($action, $tokens) && (is_string_empty($location) || substru($location, 0, strlenu($userHome))==$userHome); + } +} + +class YellowEditMerge { + public $yellow; // access to API + const ADD = "+"; // merge types + 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(YellowEditMerge::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(YellowEditMerge::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]!=YellowEditMerge::REMOVE) { + array_push($diff, array(YellowEditMerge::ADD, $textOther[$textStart+$x], false)); + $lastRemove = -1; + } else { + $diff[$lastRemove] = array(YellowEditMerge::MODIFY, $textOther[$textStart+$x], false); + ++$lastRemove; + if (count($diff)==$lastRemove) $lastRemove = -1; + } + ++$x; + continue; + } + array_push($diff, array(YellowEditMerge::SAME, $textSource[$textStart+$y], false)); + $lastRemove = -1; + ++$x; + ++$y; + } + for ($pos=$sourceEnd;$pos<$sourceSize; ++$pos) { + array_push($diff, array(YellowEditMerge::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==YellowEditMerge::SAME) { + array_push($diff, $diffYours[$posYours]); + } elseif ($typeYours==YellowEditMerge::SAME) { + array_push($diff, $diffMine[$posMine]); + } elseif ($typeMine==YellowEditMerge::ADD && $typeYours==YellowEditMerge::ADD) { + $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false); + } elseif ($typeMine==YellowEditMerge::MODIFY && $typeYours==YellowEditMerge::MODIFY) { + $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false); + } elseif ($typeMine==YellowEditMerge::REMOVE && $typeYours==YellowEditMerge::REMOVE) { + array_push($diff, $diffMine[$posMine]); + } elseif ($typeMine==YellowEditMerge::ADD) { + array_push($diff, $diffMine[$posMine]); + } elseif ($typeYours==YellowEditMerge::ADD) { + array_push($diff, $diffYours[$posYours]); + } else { + $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], true); + } + if ($typeMine==YellowEditMerge::ADD || $typeYours==YellowEditMerge::ADD) { + if ($typeMine==YellowEditMerge::ADD) ++$posMine; + if ($typeYours==YellowEditMerge::ADD) ++$posYours; + } else { + ++$posMine; + ++$posYours; + } + } + for (;$posMine<count($diffMine); ++$posMine) { + array_push($diff, $diffMine[$posMine]); + $typeMine = $diffMine[$posMine][0]; + $typeYours = " "; + } + for (;$posYours<count($diffYours); ++$posYours) { + array_push($diff, $diffYours[$posYours]); + $typeYours = $diffYours[$posYours][0]; + $typeMine = " "; + } + 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 = ""; + $conflict = false; + if (!$showDiff) { + for ($i=0; $i<count($diff); ++$i) { + if ($diff[$i][0]!=YellowEditMerge::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/workers/generate.php b/system/workers/generate.php @@ -0,0 +1,438 @@ +<?php +// Generate extension, https://github.com/annaesvensson/yellow-generate + +class YellowGenerate { + const VERSION = "0.9.1"; + public $yellow; // access to API + public $files; // number of files + public $errors; // number of errors + public $locationsArguments; // locations with location arguments detected + public $locationsArgumentsPagination; // locations with pagination arguments detected + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("generateStaticUrl", "auto"); + $this->yellow->system->setDefault("generateStaticDirectory", "public/"); + $this->yellow->system->setDefault("generateStaticDefaultFile", "index.html"); + $this->yellow->system->setDefault("generateStaticErrorFile", "404.html"); + } + + // Handle request + public function onRequest($scheme, $address, $base, $location, $fileName) { + return $this->processRequestCache($scheme, $address, $base, $location, $fileName); + } + + // Handle command + public function onCommand($command, $text) { + switch ($command) { + case "generate": $statusCode = $this->processCommandGenerate($command, $text); break; + case "clean": $statusCode = $this->processCommandClean($command, $text); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + return array("generate [directory location]", "clean [directory location]"); + } + + // Process command to generate static website + public function processCommandGenerate($command, $text) { + $statusCode = 0; + list($path, $location) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($location) || substru($location, 0, 1)=="/") { + if ($this->checkStaticSettings()) { + $statusCode = $this->generateStatic($path, $location); + } else { + $statusCode = 500; + $this->files = 0; + $this->errors = 1; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + echo "ERROR generating files: Please configure GenerateStaticUrl in file '$fileName'!\n"; + } + echo "Yellow $command: $this->files file".($this->files!=1 ? "s" : ""); + echo ", $this->errors error".($this->errors!=1 ? "s" : "")."\n"; + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Generate static website + public function generateStatic($path, $location) { + $statusCode = 200; + $this->files = $this->errors = 0; + $path = rtrim(is_string_empty($path) ? $this->yellow->system->get("generateStaticDirectory") : $path, "/"); + if (is_string_empty($location)) { + $statusCode = $this->cleanStatic($path, $location); + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("clean"); + } + } + $statusCode = max($statusCode, $this->generateStaticContent($path, $location, "\rGenerating static website", 5, 95)); + $statusCode = max($statusCode, $this->generateStaticMedia($path, $location)); + echo "\rGenerating static website 100%... done\n"; + return $statusCode; + } + + // Generate static content + public function generateStaticContent($path, $locationFilter, $progressText, $increments, $max) { + $statusCode = 200; + $this->locationsArguments = $this->locationsArgumentsPagination = array(); + $staticUrl = $this->yellow->system->get("generateStaticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $locations = $this->getContentLocations(); + $filesEstimated = count($locations); + foreach ($locations as $location) { + echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max/1.5)."%... "; + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + $statusCode = max($statusCode, $this->generateStaticFile($path, $location, true)); + } + foreach ($this->locationsArguments as $location) { + echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max/1.5)."%... "; + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + $statusCode = max($statusCode, $this->generateStaticFile($path, $location, true)); + } + $filesEstimated = $this->files + count($this->locationsArguments) + count($this->locationsArgumentsPagination); + foreach ($this->locationsArgumentsPagination as $location) { + echo "$progressText ".$this->getProgressPercent($this->files, $filesEstimated, $increments, $max)."%... "; + if (!preg_match("#^$base$locationFilter#", "$base$location")) continue; + if (substru($location, -1)!=$this->yellow->toolbox->getLocationArgumentsSeparator()) { + $statusCode = max($statusCode, $this->generateStaticFile($path, $location, false, true)); + } + for ($pageNumber=2; $pageNumber<=999; ++$pageNumber) { + $statusCodeLocation = $this->generateStaticFile($path, $location.$pageNumber, false, true); + $statusCode = max($statusCode, $statusCodeLocation); + if ($statusCodeLocation==100) break; + } + } + echo "$progressText ".$this->getProgressPercent(100, 100, $increments, $max)."%... "; + return $statusCode; + } + + // Generate static media + public function generateStaticMedia($path, $locationFilter) { + $statusCode = 200; + if (is_string_empty($locationFilter)) { + foreach ($this->getMediaLocations() as $location) { + $statusCode = max($statusCode, $this->generateStaticFile($path, $location)); + } + foreach ($this->getExtraLocations($path) as $location) { + $statusCode = max($statusCode, $this->generateStaticFile($path, $location)); + } + $statusCode = max($statusCode, $this->generateStaticFile($path, "/error/", false, false, true)); + } + return $statusCode; + } + + // Generate static file + public function generateStaticFile($path, $location, $analyse = false, $probe = false, $error = false) { + $this->yellow->content->pages = array(); + $this->yellow->page = new YellowPage($this->yellow); + $this->yellow->page->fileName = substru($location, 1); + if (!is_readable($this->yellow->page->fileName)) { + ob_start(); + $staticUrl = $this->yellow->system->get("generateStaticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $statusCode = $this->requestStaticFile($scheme, $address, $base, $location); + if ($statusCode<400 || $error) { + $fileData = ob_get_contents(); + $statusCode = $this->saveStaticFile($path, $location, $fileData, $statusCode); + } + ob_end_clean(); + } else { + $statusCode = $this->copyStaticFile($path, $location); + } + if ($statusCode==200 && $analyse) $this->analyseLocations($scheme, $address, $base, $fileData); + if ($statusCode==404 && $probe) $statusCode = 100; + if ($statusCode==404 && $error) $statusCode = 200; + if ($statusCode>=200) ++$this->files; + if ($statusCode>=400) { + ++$this->errors; + echo "\rERROR generating location '$location', ".$this->yellow->page->getStatusCode(true)."\n"; + } + if ($this->yellow->system->get("coreDebugMode")>=1) { + echo "YellowGenerate::generateStaticFile status:$statusCode location:$location<br/>\n"; + } + return $statusCode; + } + + // Request static file + public function requestStaticFile($scheme, $address, $base, $location) { + list($serverName, $serverPort) = $this->yellow->toolbox->getTextList($address, ":", 2); + if (is_string_empty($serverPort)) $serverPort = $scheme=="https" ? 443 : 80; + $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1"; + $_SERVER["SERVER_NAME"] = $serverName; + $_SERVER["SERVER_PORT"] = $serverPort; + $_SERVER["REQUEST_METHOD"] = "GET"; + $_SERVER["REQUEST_SCHEME"] = $scheme; + $_SERVER["REQUEST_URI"] = $base.$location; + $_SERVER["SCRIPT_NAME"] = $base."/yellow.php"; + $_SERVER["REMOTE_ADDR"] = "127.0.0.1"; + $_REQUEST = array(); + return $this->yellow->request(); + } + + // Save static file + public function saveStaticFile($path, $location, $fileData, $statusCode) { + $modified = strtotime($this->yellow->page->getHeader("Last-Modified")); + if ($modified==0) $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName); + if ($statusCode>=301 && $statusCode<=303) { + $fileData = $this->getStaticRedirect($this->yellow->page->getHeader("Location")); + $modified = time(); + } + $fileName = $this->getStaticFile($path, $location, $statusCode); + if (is_file($fileName)) $this->yellow->toolbox->deleteFile($fileName); + if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || + !$this->yellow->toolbox->modifyFile($fileName, $modified)) { + $statusCode = 500; + $this->yellow->page->statusCode = $statusCode; + $this->yellow->page->errorMessage = "Can't write file '$fileName'!"; + } + return $statusCode; + } + + // Copy static file + public function copyStaticFile($path, $location) { + $statusCode = 200; + $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName); + $fileName = $this->getStaticFile($path, $location, $statusCode); + if (is_file($fileName)) $this->yellow->toolbox->deleteFile($fileName); + if (!$this->yellow->toolbox->copyFile($this->yellow->page->fileName, $fileName, true) || + !$this->yellow->toolbox->modifyFile($fileName, $modified)) { + $statusCode = 500; + $this->yellow->page->statusCode = $statusCode; + $this->yellow->page->errorMessage = "Can't write file '$fileName'!"; + } + return $statusCode; + } + + // Analyse locations with arguments + public function analyseLocations($scheme, $address, $base, $rawData) { + preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $rawData, $matches); + foreach ($matches[2] as $match) { + $location = rawurldecode($match); + if (preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1]; + if (preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $tokens)) { + if ($tokens[1]!=$scheme) continue; + if ($tokens[2]!=$address) continue; + $location = $tokens[3]; + } + if (substru($location, 0, strlenu($base))!=$base) continue; + if (substru($location, strlenu($base), 1)!="/") continue; + $location = substru($location, strlenu($base)); + if (!$this->yellow->toolbox->isLocationArguments($location)) continue; + if (!$this->yellow->toolbox->isLocationArgumentsPagination($location)) { + $location = rtrim($location, "/")."/"; + if (!isset($this->locationsArguments[$location])) { + $this->locationsArguments[$location] = $location; + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowGenerate::analyseLocations detected location:$location<br/>\n"; + } + } + } else { + $location = rtrim($location, "0..9"); + if (!isset($this->locationsArgumentsPagination[$location])) { + $this->locationsArgumentsPagination[$location] = $location; + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowGenerate::analyseLocations detected location:$location<br/>\n"; + } + } + } + } + } + + // Process command to clean static website + public function processCommandClean($command, $text) { + $statusCode = 0; + list($path, $location) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($location) || substru($location, 0, 1)=="/") { + $statusCode = $this->cleanStatic($path, $location); + echo "Yellow $command: Static website"; + echo " ".($statusCode!=200 ? "not " : "")."cleaned\n"; + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Clean static website + public function cleanStatic($path, $location) { + $statusCode = 200; + $path = rtrim(is_string_empty($path) ? $this->yellow->system->get("generateStaticDirectory") : $path, "/"); + if (is_string_empty($location)) { + $statusCode = max($statusCode, $this->cleanStaticDirectory($path)); + } else { + if ($this->yellow->lookup->isFileLocation($location)) { + $fileName = $this->getStaticFile($path, $location, $statusCode); + $statusCode = $this->cleanStaticFile($fileName); + } else { + $statusCode = $this->cleanStaticDirectory($path.$location); + } + } + return $statusCode; + } + + // Clean static directory + public function cleanStaticDirectory($path) { + $statusCode = 200; + if (is_dir($path) && $this->checkStaticDirectory($path)) { + if (!$this->yellow->toolbox->deleteDirectory($path)) { + $statusCode = 500; + echo "ERROR cleaning files: Can't delete directory '$path'!\n"; + } + } + return $statusCode; + } + + // Clean static file + public function cleanStaticFile($fileName) { + $statusCode = 200; + if (is_file($fileName)) { + if (!$this->yellow->toolbox->deleteFile($fileName)) { + $statusCode = 500; + echo "ERROR cleaning files: Can't delete file '$fileName'!\n"; + } + } + return $statusCode; + } + + // Process request for cached files + public function processRequestCache($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if (is_dir($this->yellow->system->get("coreCacheDirectory"))) { + $location .= $this->yellow->toolbox->getLocationArguments(); + $fileName = rtrim($this->yellow->system->get("coreCacheDirectory"), "/").$location; + if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->system->get("generateStaticDefaultFile"); + if (is_file($fileName) && is_readable($fileName) && !$this->yellow->lookup->isCommandLine()) { + $statusCode = $this->yellow->sendFile(200, $fileName, true); + } + } + return $statusCode; + } + + // Check static settings + public function checkStaticSettings() { + return preg_match("/^(http|https):/", $this->yellow->system->get("generateStaticUrl")); + } + + // Check static directory + public function checkStaticDirectory($path) { + $ok = false; + if (!is_string_empty($path)) { + if ($path==rtrim($this->yellow->system->get("generateStaticDirectory"), "/")) $ok = true; + if ($path==rtrim($this->yellow->system->get("coreCacheDirectory"), "/")) $ok = true; + if ($path==rtrim($this->yellow->system->get("coreTrashDirectory"), "/")) $ok = true; + if (is_file("$path/".$this->yellow->system->get("generateStaticDefaultFile"))) $ok = true; + if (is_file("$path/yellow.php")) $ok = false; + } + return $ok; + } + + // Return progress in percent + public function getProgressPercent($now, $total, $increments, $max) { + $max = intval($max/$increments) * $increments; + $percent = intval(($max/$total) * $now); + if ($increments>1) $percent = intval($percent/$increments) * $increments; + return min($max, $percent); + } + + // Return static file + public function getStaticFile($path, $location, $statusCode) { + if ($statusCode<400) { + $fileName = $path.$location; + if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->system->get("generateStaticDefaultFile"); + } elseif ($statusCode==404) { + $fileName = $path."/".$this->yellow->system->get("generateStaticErrorFile"); + } else { + $fileName = $path."/error.html"; + } + return $fileName; + } + + // Return static redirect + public function getStaticRedirect($location) { + $output = "<!DOCTYPE html><html>\n<head>\n"; + $output .= "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n"; + $output .= "<meta http-equiv=\"refresh\" content=\"0;url=".htmlspecialchars($location)."\" />\n"; + $output .= "</head>\n</html>"; + return $output; + } + + // Return content locations + public function getContentLocations($includeAll = false) { + $locations = array(); + $staticUrl = $this->yellow->system->get("generateStaticUrl"); + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl); + $this->yellow->page->setRequestInformation($scheme, $address, $base, "", "", false); + foreach ($this->yellow->content->index(true, true) as $page) { + if (preg_match("/exclude/i", $page->get("generate")) && !$includeAll) continue; + if ($page->get("status")=="private" || $page->get("status")=="draft") continue; + array_push($locations, $page->location); + } + if (!$this->yellow->content->find("/") && $this->yellow->system->get("coreMultiLanguageMode")) array_unshift($locations, "/"); + return $locations; + } + + // Return media locations + public function getMediaLocations() { + $locations = array(); + $mediaPath = $this->yellow->system->get("coreMediaDirectory"); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($mediaPath, "/.*/", false, false); + foreach ($fileNames as $fileName) { + array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName)); + } + $themePath = $this->yellow->system->get("coreThemeDirectory"); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($themePath, "/.*/", false, false); + foreach ($fileNames as $fileName) { + array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName)); + } + $workerPath = $this->yellow->system->get("coreWorkerDirectory"); + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($workerPath, "/.*/", false, false); + foreach ($fileNames as $fileName) { + array_push($locations, $this->yellow->lookup->findMediaLocationFromFile($fileName)); + } + return array_diff($locations, $this->getMediaLocationsIgnore()); + } + + // Return media locations to ignore + public function getMediaLocationsIgnore() { + $locations = array(""); + $workerPath = $this->yellow->system->get("coreWorkerDirectory"); + $workerDirectoryLength = strlenu($this->yellow->system->get("coreWorkerDirectory")); + if ($this->yellow->extension->isExisting("bundle")) { + foreach ($this->yellow->toolbox->getDirectoryEntries($workerPath, "/^bundle-(.*)/", false, false) as $entry) { + list($locationsBundle) = $this->yellow->extension->get("bundle")->getBundleInformation($entry); + $locations = array_merge($locations, $locationsBundle); + } + } + if ($this->yellow->extension->isExisting("edit")) { + foreach ($this->yellow->toolbox->getDirectoryEntries($workerPath, "/^edit\.(.*)/", false, false) as $entry) { + $location = $this->yellow->system->get("coreExtensionLocation").substru($entry, $workerDirectoryLength); + array_push($locations, $location); + } + } + return array_unique($locations); + } + + // Return extra locations + public function getExtraLocations($path) { + $locations = array(); + $pathIgnore = "($path/|". + $this->yellow->system->get("generateStaticDirectory")."|". + $this->yellow->system->get("coreContentDirectory")."|". + $this->yellow->system->get("coreMediaDirectory")."|". + $this->yellow->system->get("coreSystemDirectory").")"; + $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive(".", "/.*/", false, false); + foreach ($fileNames as $fileName) { + $fileName = substru($fileName, 2); + if (preg_match("#^$pathIgnore#", $fileName) || $fileName=="yellow.php") continue; + array_push($locations, "/".$fileName); + } + return $locations; + } +} diff --git a/system/workers/image.php b/system/workers/image.php @@ -0,0 +1,199 @@ +<?php +// Image extension, https://github.com/annaesvensson/yellow-image + +class YellowImage { + const VERSION = "0.9.1"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("imageUploadWidthMax", "1280"); + $this->yellow->system->setDefault("imageUploadHeightMax", "1280"); + $this->yellow->system->setDefault("imageUploadJpgQuality", "80"); + $this->yellow->system->setDefault("imageThumbnailJpgQuality", "80"); + } + + // Handle update + public function onUpdate($action) { + if ($action=="clean") { + $statusCode = 200; + $path = $this->yellow->lookup->findMediaDirectory("coreThumbnailLocation"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) { + if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + } + if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!"); + } + } + + // Handle page content element + public function onParseContentElement($page, $name, $text, $attributes, $type) { + $output = null; + if ($name=="image" && $type=="inline") { + list($name, $alt, $style, $width, $height) = $this->yellow->toolbox->getTextArguments($text); + if (!preg_match("/^\w+:/", $name)) { + if (is_string_empty($alt)) $alt = $this->yellow->language->getText("imageDefaultAlt"); + if (is_string_empty($width)) $width = "100%"; + if (is_string_empty($height)) $height = $width; + $path = $this->yellow->lookup->findMediaDirectory("coreImageLocation"); + list($src, $width, $height) = $this->getImageInformation($path.$name, $width, $height); + } else { + if (is_string_empty($alt)) $alt = $this->yellow->language->getText("imageDefaultAlt"); + $src = $this->yellow->lookup->normaliseUrl("", "", "", $name); + $width = $height = 0; + } + $output = "<img src=\"".htmlspecialchars($src)."\""; + if ($width && $height) $output .= " width=\"".htmlspecialchars($width)."\" height=\"".htmlspecialchars($height)."\""; + if (!is_string_empty($alt)) $output .= " alt=\"".htmlspecialchars($alt)."\" title=\"".htmlspecialchars($alt)."\""; + if (!is_string_empty($style)) $output .= " class=\"".htmlspecialchars($style)."\""; + $output .= " />"; + } + return $output; + } + + // Handle media file changes + public function onEditMediaFile($file, $action, $email) { + if ($action=="upload") { + $fileName = $file->fileName; + list($widthInput, $heightInput, $orientation, $type) = + $this->yellow->toolbox->detectImageInformation($fileName, $file->get("type")); + $widthMax = $this->yellow->system->get("imageUploadWidthMax"); + $heightMax = $this->yellow->system->get("imageUploadHeightMax"); + if ($type=="gif" || $type=="jpg" || $type=="png") { + if ($widthInput>$widthMax || $heightInput>$heightMax) { + list($widthOutput, $heightOutput) = $this->getImageDimensionsFit($widthInput, $heightInput, $widthMax, $heightMax); + $image = $this->loadImage($fileName, $type); + $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); + $image = $this->orientImage($image, $orientation); + if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) { + $file->error(500, "Can't write file '$fileName'!"); + } + } elseif ($orientation>1) { + $image = $this->loadImage($fileName, $type); + $image = $this->orientImage($image, $orientation); + if (!$this->saveImage($image, $fileName, $type, $this->yellow->system->get("imageUploadJpgQuality"))) { + $file->error(500, "Can't write file '$fileName'!"); + } + } + } + } + } + + // Return image information, create thumbnail on demand + public function getImageInformation($fileName, $widthOutput, $heightOutput) { + $fileNameShort = substru($fileName, strlenu($this->yellow->lookup->findMediaDirectory("coreImageLocation"))); + list($widthInput, $heightInput, $orientation, $type) = $this->yellow->toolbox->detectImageInformation($fileName); + $widthOutput = $this->convertValueAndUnit($widthOutput, $widthInput); + $heightOutput = $this->convertValueAndUnit($heightOutput, $heightInput); + if (($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg" || $type=="") { + $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreImageLocation").$fileNameShort; + $width = $widthOutput; + $height = $heightOutput; + } else { + $pathThumb = $this->yellow->lookup->findMediaDirectory("coreThumbnailLocation"); + $fileNameThumb = ltrim(str_replace(array("/", "\\", "."), "-", dirname($fileNameShort)."/".pathinfo($fileName, PATHINFO_FILENAME)), "-"); + $fileNameThumb .= "-".$widthOutput."x".$heightOutput; + $fileNameThumb .= ".".pathinfo($fileName, PATHINFO_EXTENSION); + $fileNameOutput = $pathThumb.$fileNameThumb; + if ($this->isFileNotUpdated($fileName, $fileNameOutput)) { + $image = $this->loadImage($fileName, $type); + $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput); + $image = $this->orientImage($image, $orientation); + if (is_file($fileNameOutput)) $this->yellow->toolbox->deleteFile($fileNameOutput); + if (!$this->saveImage($image, $fileNameOutput, $type, $this->yellow->system->get("imageThumbnailJpgQuality")) || + !$this->yellow->toolbox->modifyFile($fileNameOutput, $this->yellow->toolbox->getFileModified($fileName))) { + $this->yellow->page->error(500, "Can't write file '$fileNameOutput'!"); + } + } + $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreThumbnailLocation").$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; + } + + // Orient image automatically + public function orientImage($image, $orientation) { + switch ($orientation) { + case 2: imageflip($image, IMG_FLIP_HORIZONTAL); break; + case 3: $image = imagerotate($image, 180, 0); break; + case 4: imageflip($image, IMG_FLIP_VERTICAL); break; + case 5: $image = imagerotate($image, 90, 0); imageflip($image, IMG_FLIP_VERTICAL); break; + case 6: $image = imagerotate($image, -90, 0); break; + case 7: $image = imagerotate($image, 90, 0); imageflip($image, IMG_FLIP_HORIZONTAL); break; + case 8: $image = imagerotate($image, 90, 0); break; + } + return $image; + } + + // 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); + } + + // Check if file needs to be updated + public function isFileNotUpdated($fileNameInput, $fileNameOutput) { + return $this->yellow->toolbox->getFileModified($fileNameInput)!=$this->yellow->toolbox->getFileModified($fileNameOutput); + } +} diff --git a/system/workers/install-blog.bin b/system/workers/install-blog.bin Binary files differ. diff --git a/system/workers/install-language.bin b/system/workers/install-language.bin Binary files differ. diff --git a/system/workers/install-wiki.bin b/system/workers/install-wiki.bin Binary files differ. diff --git a/system/workers/install.php b/system/workers/install.php @@ -0,0 +1,575 @@ +<?php +// Install extension, https://github.com/annaesvensson/yellow-install + +class YellowInstall { + const VERSION = "0.9.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) { + return $this->processRequestInstall($scheme, $address, $base, $location, $fileName); + } + + // Handle command + public function onCommand($command, $text) { + return $this->processCommandInstall($command, $text); + } + + // Process request to install website + public function processRequestInstall($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->yellow->lookup->isContentFile($fileName) || is_string_empty($fileName)) { + if ($this->yellow->system->get("updateCurrentRelease")=="none") { + $this->checkServerRequirements(); + $author = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $this->yellow->page->getRequest("author"))); + $email = trim($this->yellow->page->getRequest("email")); + $password = trim($this->yellow->page->getRequest("password")); + $language = trim($this->yellow->page->getRequest("language")); + $extension = trim($this->yellow->page->getRequest("extension")); + $status = trim($this->yellow->page->getRequest("status")); + $statusCode = $this->updateLog(); + $statusCode = max($statusCode, $this->updateLanguages("small")); + $errorMessage = $this->yellow->page->errorMessage; + $this->yellow->content->pages["root/"] = array(); + $this->yellow->page = new YellowPage($this->yellow); + $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName, false); + $this->yellow->page->parseMeta($this->getRawDataInstall(), $statusCode, $errorMessage); + $this->yellow->page->parseContent(); + $this->yellow->page->parsePage(); + if ($status=="install") $status = $this->updateExtensions("small", $extension)==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateUser($email, $password, $author, $language)==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateAuthentication($scheme, $address, $base, $email)==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateContent($language, "installHome", "/")==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateContent($language, "installAbout", "/about/")==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateContent($language, "installDefault", "/shared/page-new-default")==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateContent($language, "installWiki", "/shared/page-new-wiki")==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateContent($language, "installBlog", "/shared/page-new-blog")==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateContent($language, "coreError404", "/shared/page-error-404")==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->updateSettings()==200 ? "ok" : "error"; + if ($status=="ok") $status = $this->removeInstall()==200 ? "done" : "error"; + } else { + $status = $this->removeInstall(true)==200 ? "done" : "error"; + } + if ($status=="done") { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, "/"); + $statusCode = $this->yellow->sendStatus(303, $location); + } else { + $statusCode = $this->yellow->sendData($this->yellow->page->statusCode, $this->yellow->page->headerData, $this->yellow->page->outputData); + } + } + return $statusCode; + } + + // Process command to install website + public function processCommandInstall($command, $text) { + $statusCode = 0; + if ($this->yellow->system->get("updateCurrentRelease")=="none") { + $this->checkCommandRequirements(); + list($installation, $option) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($command)) { + $statusCode = 200; + echo "Datenstrom Yellow is for people who make small websites. https://datenstrom.se/yellow/\n"; + echo "Syntax: php yellow.php\n"; + echo " php yellow.php about [extension]\n"; + echo " php yellow.php serve [url]\n"; + echo " php yellow.php skip installation [option]\n"; + } elseif ($command=="about" || $command=="serve") { + $statusCode = 0; + } elseif ($command=="skip" && $installation=="installation") { + $statusCode = $this->updateLog(); + if ($statusCode==200) $statusCode = $this->updateLanguages($option); + if ($statusCode==200) $statusCode = $this->updateExtensions($option, ""); + if ($statusCode==200) $statusCode = $this->updateSettings(true); + if ($statusCode==200) $statusCode = $this->removeInstall(); + if ($statusCode>=400) { + echo "ERROR installing files: ".$this->yellow->page->errorMessage."\n"; + echo "The installation has not been completed. Please run command again.\n"; + } else { + $extensionsCount = $this->getExtensionsCount(); + echo "Yellow $command: $extensionsCount extension".($extensionsCount!=1 ? "s" : "").", 0 errors\n"; + } + } else { + $statusCode = 500; + echo "The installation has not been completed. Please type 'php yellow.php serve' or 'php yellow.php skip installation`.\n"; + } + } else { + $statusCode = $this->removeInstall(true); + if ($statusCode==200) $statusCode = 0; + if ($statusCode>=400) { + echo "ERROR installing files: ".$this->yellow->page->errorMessage."\n"; + echo "Detected ZIP files, 0 extensions installed. Please run command again.\n"; + } + } + return $statusCode; + } + + // Update log file + public function updateLog() { + $statusCode = 200; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreWebsiteFile"); + if (!is_file($fileName)) { + list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation(); + $product = "Datenstrom Yellow ".YellowCore::RELEASE; + $this->yellow->toolbox->log("info", "Install $product, PHP ".PHP_VERSION.", $name $version, $os"); + foreach ($this->yellow->extension->data as $key=>$value) { + if ($key=="install") continue; + $this->yellow->toolbox->log("info", "Install extension '".ucfirst($key)." $value[version]'"); + } + if (!is_file($fileName)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + return $statusCode; + } + + // Update languages + public function updateLanguages($option) { + $statusCode = 200; + $path = $this->yellow->system->get("coreWorkerDirectory")."install-language.bin"; + $zip = new ZipArchive(); + if ($zip->open($path)===true) { + $pathBase = ""; + if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1]; + $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateExtensionFile")); + foreach ($this->getLanguageExtensionsRequired($fileData, $option) as $extension) { + $fileDataPhp = $zip->getFromName($pathBase."translations/$extension/$extension.php"); + $fileDataIni = $zip->getFromName($pathBase."translations/$extension/extension.ini"); + $statusCode = max($statusCode, $this->updateLanguageArchive($fileDataPhp, $fileDataIni, $pathBase, "install")); + } + $this->yellow->extension->load($this->yellow->system->get("coreWorkerDirectory")); + $this->yellow->language->load($this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile")); + $zip->close(); + } else { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't open file '$path'!"); + } + return $statusCode; + } + + // Update language archive + public function updateLanguageArchive($fileDataPhp, $fileDataIni, $pathBase, $action) { + $statusCode = 200; + if ($this->yellow->extension->isExisting("update")) { + $settings = $this->yellow->toolbox->getTextSettings($fileDataIni, ""); + $extension = lcfirst($settings->get("extension")); + $version = $settings->get("version"); + $modified = strtotime($settings->get("published")); + $fileNamePhp = $this->yellow->system->get("coreWorkerDirectory").$extension.".php"; + if (!is_string_empty($extension) && !is_string_empty($version) && !is_file($fileNamePhp)) { + $statusCode = max($statusCode, $this->yellow->extension->get("update")->updateExtensionSettings($extension, $action, $settings)); + $statusCode = max($statusCode, $this->yellow->extension->get("update")->updateExtensionFile( + $fileNamePhp, $fileDataPhp, $modified, 0, 0, "create", $extension)); + $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); + } + } + return $statusCode; + } + + // Update extensions + public function updateExtensions($option, $extension) { + $statusCode = 200; + if ($this->yellow->extension->isExisting("update")) { + if (!is_string_empty($option)) { + if ($option=="medium" || $option=="large") { + $path = $this->yellow->system->get("coreExtensionDirectory"); + $fileData = $this->yellow->toolbox->readFile($path.$this->yellow->system->get("updateAvailableFile")); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + $extensions = $this->getAvailableExtensionsRequired($settings, $option); + $statusCode = $this->downloadExtensionsAvailable($settings, $extensions); + $path = $this->yellow->system->get("coreWorkerDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^install-.*\.bin$/", true, false) as $entry) { + if (basename($entry)=="install-language.bin") continue; + if (preg_match("/^install-(.*?)\.bin/", basename($entry), $matches) && !in_array($matches[1], $extensions)) continue; + $statusCode = max($statusCode, $this->yellow->extension->get("update")->updateExtensionArchive($entry, "install")); + } + } + if (!($option=="small" || $option=="medium" || $option=="large")) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Option '$option' not supported!"); + } + } + if (!is_string_empty($extension)) { + $path = $this->yellow->system->get("coreWorkerDirectory")."install-".$extension.".bin"; + if (is_file($path)) { + $statusCode = $this->yellow->extension->get("update")->updateExtensionArchive($path, "install"); + } + } + } + return $statusCode; + } + + // Update user + public function updateUser($email, $password, $name, $language) { + $statusCode = 200; + if ($this->yellow->extension->isExisting("edit") && !is_string_empty($email) && !is_string_empty($password)) { + if (is_string_empty($name)) $name = $this->yellow->system->get("sitename"); + $fileNameUser = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreUserFile"); + $settings = array( + "name" => $name, + "description" => $this->yellow->language->getText("editUserDescription", $language), + "language" => $language, + "access" => "create, edit, delete, restore, upload, configure, update", + "home" => "/", + "hash" => $this->yellow->extension->get("edit")->response->createHash($password), + "stamp" => $this->yellow->extension->get("edit")->response->createStamp(), + "pending" => "none", + "failed" => "0", + "modified" => date("Y-m-d H:i:s", time()), + "status" => "active"); + if (!$this->yellow->user->save($fileNameUser, $email, $settings)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileNameUser'!"); + } + $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", "Add user '".strtok($name, " ")."'"); + } + return $statusCode; + } + + // Update authentication + public function updateAuthentication($scheme, $address, $base, $email) { + if ($this->yellow->extension->isExisting("edit") && $this->yellow->user->isExisting($email)) { + $base = rtrim($base.$this->yellow->system->get("editLocation"), "/"); + $this->yellow->extension->get("edit")->response->createCookies($scheme, $address, $base, $email); + } + return 200; + } + + // Update content + public function updateContent($language, $name, $location) { + $statusCode = 200; + $fileName = $this->yellow->lookup->findFileFromContentLocation($location); + $fileData = str_replace("\r\n", "\n", $this->yellow->toolbox->readFile($fileName)); + if (!is_string_empty($fileData) && $language!="en") { + $titleOld = "Title: ".$this->yellow->language->getText("{$name}Title", "en")."\n"; + $titleNew = "Title: ".$this->yellow->language->getText("{$name}Title", $language)."\n"; + $fileData = str_replace($titleOld, $titleNew, $fileData); + $textOld = str_replace("\\n", "\n", $this->yellow->language->getText("{$name}Text", "en")); + $textNew = str_replace("\\n", "\n", $this->yellow->language->getText("{$name}Text", $language)); + $fileData = str_replace($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($skipInstallation = false) { + $statusCode = 200; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, $this->getSystemSettings($skipInstallation))) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + $language = $this->yellow->system->get("language"); + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile"); + $fileData = $this->yellow->toolbox->readFile($fileName); + if (strposu($fileData, "Language:")===false) { + if (!is_string_empty($fileData)) $fileData .= "\n"; + $fileData .= "Language: $language\n"; + $fileData .= "media/images/photo.jpg: ".$this->yellow->language->getText("installExampleImage", $language)."\n"; + if (!$this->yellow->toolbox->createFile($fileName, $fileData)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + return $statusCode; + } + + // Remove files used by installation + public function removeInstall($log = false) { + $statusCode = 200; + if (function_exists("opcache_reset")) opcache_reset(); + $path = $this->yellow->system->get("coreWorkerDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^install-.*\.bin$/", true, false) as $entry) { + if (!$this->yellow->toolbox->deleteFile($entry)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$entry'!"); + } + } + $fileName = $this->yellow->system->get("coreWorkerDirectory")."install.php"; + if ($statusCode==200 && !$this->yellow->toolbox->deleteFile($fileName)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); + } + if ($statusCode==200) unset($this->yellow->extension->data["install"]); + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreExtensionFile"); + $fileData = $this->yellow->toolbox->readFile($fileName); + $fileDataNew = $this->yellow->toolbox->unsetTextSettings($fileData, "extension", "install"); + if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + if ($log) $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", "Uninstall extension 'Install ".YellowInstall::VERSION."'"); + return $statusCode; + } + + // Check web server requirements + public function checkServerRequirements() { + if ($this->yellow->system->get("coreDebugMode")>=1) { + list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation(); + echo "YellowInstall::checkServerRequirements for $name $version, $os<br/>\n"; + } + if (!$this->checkServerComplete()) $this->yellow->exitFatalError("Datenstrom Yellow requires complete upload!"); + if (!$this->checkServerWrite()) $this->yellow->exitFatalError("Datenstrom Yellow requires write access!"); + if (!$this->checkServerConfiguration()) $this->yellow->exitFatalError("Datenstrom Yellow requires configuration file!"); + if (!$this->checkServerRewrite()) $this->yellow->exitFatalError("Datenstrom Yellow requires rewrite support!"); + } + + // Check command line requirements + public function checkCommandRequirements() { + if ($this->yellow->system->get("coreDebugMode")>=1) { + list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation(); + echo "YellowInstall::checkCommandRequirements for $name $version, $os<br/>\n"; + } + if (!$this->checkServerComplete()) $this->yellow->exitFatalError("Datenstrom Yellow requires complete upload!"); + if (!$this->checkServerWrite()) $this->yellow->exitFatalError("Datenstrom Yellow requires write access!"); + } + + // Check web server complete upload + public function checkServerComplete() { + $complete = true; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreExtensionFile"); + $fileData = $this->yellow->toolbox->readFile($fileName); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + $fileNames = array($fileName); + foreach ($settings as $extension=>$block) { + foreach ($block as $key=>$value) { + if (strposu($key, "/")) { + list($entry, $flags) = $this->yellow->toolbox->getTextList($value, ",", 2); + if (!preg_match("/create/i", $flags)) continue; + if (preg_match("/delete/i", $flags)) continue; + if (preg_match("/additional/i", $flags)) continue; + array_push($fileNames, $key); + } + } + } + foreach ($fileNames as $fileName) { + if (!is_file($fileName) || filesize($fileName)==0) { + $complete = false; + if ($this->yellow->system->get("coreDebugMode")>=1) { + echo "YellowInstall::checkServerComplete detected missing file:$fileName<br/>\n"; + } + } + } + return $complete; + } + + // Check web server write access + public function checkServerWrite() { + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + return $this->yellow->system->save($fileName, array()); + } + + // Check web server configuration file + public function checkServerConfiguration() { + list($name) = $this->yellow->toolbox->detectServerInformation(); + return strtoloweru($name)!="apache" || is_file(".htaccess"); + } + + // Check web server rewrite support + public function checkServerRewrite() { + $rewrite = true; + if (!$this->isServerBuiltin()) { + $curlHandle = curl_init(); + list($scheme, $address, $base) = $this->yellow->lookup->getRequestInformation(); + $location = $this->yellow->system->get("coreThemeLocation").$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; YellowInstall/".YellowInstall::VERSION).")"; + curl_setopt($curlHandle, CURLOPT_NOBODY, 1); + curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); + curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, false); + curl_exec($curlHandle); + $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); + curl_close($curlHandle); + if ($statusCode!=200) { + $rewrite = false; + if ($this->yellow->system->get("coreDebugMode")>=1 && !$rewrite) { + echo "YellowInstall::checkServerRewrite detected failed url:$url<br/>\n"; + } + } + } + return $rewrite; + } + + // Download available extension files + public function downloadExtensionsAvailable($settings, $extensions) { + $statusCode = 200; + if ($this->yellow->extension->isExisting("update")) { + $path = $this->yellow->system->get("coreWorkerDirectory"); + $extensionsNow = 0; + $extensionsTotal = count($extensions); + $curlHandle = curl_init(); + foreach ($extensions as $extension) { + echo "\rDownloading available extensions ".$this->getProgressPercent(++$extensionsNow, $extensionsTotal, 5, 95)."%... "; + $fileName = $path."install-".$this->yellow->lookup->normaliseName($extension, true, false, true).".bin"; + if (is_file($fileName)) continue; + $url = $settings[$extension]->get("downloadUrl"); + curl_setopt($curlHandle, CURLOPT_URL, $this->yellow->extension->get("update")->getExtensionDownloadUrl($url)); + curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowInstall/".YellowInstall::VERSION).")"; + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); + $fileData = curl_exec($curlHandle); + $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); + $redirectUrl = ($statusCode>=300 && $statusCode<=399) ? curl_getinfo($curlHandle, CURLINFO_REDIRECT_URL) : ""; + if ($statusCode==0) { + $statusCode = 450; + $this->yellow->page->error($statusCode, "Can't connect to the update server!"); + } + if ($statusCode!=450 && $statusCode!=200) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't download file '$url'!"); + } + if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileName, $fileData)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + if ($this->yellow->system->get("coreDebugMode")>=2 && !is_string_empty($redirectUrl)) { + echo "YellowInstall::downloadExtensionsAvailable redirected to url:$redirectUrl<br/>\n"; + } + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowInstall::downloadExtensionsAvailable status:$statusCode url:$url<br/>\n"; + } + if ($statusCode!=200) break; + } + curl_close($curlHandle); + echo "\rDownloading available extensions 100%... done\n"; + } + return $statusCode; + } + + // Return available extensions required + public function getAvailableExtensionsRequired($settings, $option) { + $extensions = array(); + if ($option=="medium") { + $text = "help highlight search toc"; + $extensions = array_unique(array_filter($this->yellow->toolbox->getTextArguments($text), "strlen")); + } elseif ($option=="large") { + foreach ($settings as $key=>$value) { + if (preg_match("/language/i", $value->get("tag"))) continue; + array_push($extensions, strtoloweru($key)); + } + } + return $extensions; + } + + // Return language extensions required + public function getLanguageExtensionsRequired($fileData, $option) { + $extensions = array(); + $languages = array(); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2]) && strposu($matches[1], "/")) { + $extension = basename($matches[1]); + $extension = $this->yellow->lookup->normaliseName($extension, true, true); + list($entry, $flags) = $this->yellow->toolbox->getTextList($matches[2], ",", 2); + $arguments = preg_split("/\s*,\s*/", trim($flags)); + $language = array_pop($arguments); + if (preg_match("/^(.*)\.php$/", basename($entry))) { + $languages[$language] = $extension; + } + } + } + } + if ($option=="large") { + foreach ($languages as $language=>$extension) { + array_push($extensions, $extension); + } + } else { + foreach ($this->getSystemLanguages("en, de, sv") as $language) { + if (isset($languages[$language])) array_push($extensions, $languages[$language]); + } + $extensions = array_slice($extensions, 0, 3); + } + return $extensions; + } + + // Return extensions installed + public function getExtensionsCount() { + $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreExtensionFile"); + $fileData = $this->yellow->toolbox->readFile($fileNameCurrent); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + return count($settings); + } + + // Return system languages + public function getSystemLanguages($languagesDefault) { + $languages = array(); + foreach (preg_split("/\s*,\s*/", $this->yellow->toolbox->getServer("HTTP_ACCEPT_LANGUAGE")) as $string) { + list($language, $dummy) = $this->yellow->toolbox->getTextList($string, ";", 2); + if (!is_string_empty($language)) array_push($languages, $language); + } + foreach (preg_split("/\s*,\s*/", $languagesDefault) as $language) { + if (!is_string_empty($language)) array_push($languages, $language); + } + return array_unique($languages); + } + + // Return system settings + public function getSystemSettings($skipInstallation) { + $settings = array(); + foreach ($_REQUEST as $key=>$value) { + if (!$this->yellow->system->isExisting($key)) continue; + if ($key=="password" || $key=="status") continue; + $settings[$key] = trim($value); + } + if ($this->yellow->system->get("sitename")=="Datenstrom Yellow") $settings["sitename"] = $this->yellow->toolbox->detectServerSitename(); + if ($this->yellow->system->get("generateStaticUrl")=="auto" && getenv("URL")!==false) $settings["generateStaticUrl"] = getenv("URL"); + if ($this->yellow->system->get("generateStaticUrl")=="auto" && $skipInstallation) $settings["generateStaticUrl"] = "http://localhost:8000/"; + if ($this->yellow->system->get("coreTimezone")=="UTC") $settings["coreTimezone"] = $this->yellow->toolbox->detectServerTimezone(); + if ($this->yellow->system->get("updateEventPending")=="none") $settings["updateEventPending"] = "website/install"; + $settings["updateCurrentRelease"] = YellowCore::RELEASE; + return $settings; + } + + // Return raw data for install page + public function getRawDataInstall() { + $languages = $this->yellow->system->getAvailable("language"); + $language = $this->yellow->toolbox->detectBrowserLanguage($languages, $this->yellow->system->get("language")); + $this->yellow->language->set($language); + $rawData = "---\nTitle:".$this->yellow->language->getText("installTitle")."\nLanguage:$language\nNavigation:navigation\nHeader:none\nFooter:none\nSidebar:none\n---\n"; + $rawData .= "<form class=\"install-form\" action=\"".$this->yellow->page->getLocation(true)."\" method=\"post\">\n"; + $rawData .= "<p><label for=\"author\">".$this->yellow->language->getText("editSignupName")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"author\" id=\"author\" value=\"\"></p>\n"; + $rawData .= "<p><label for=\"email\">".$this->yellow->language->getText("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->language->getText("editSignupPassword")."</label><br /><input class=\"form-control\" type=\"password\" maxlength=\"64\" name=\"password\" id=\"password\" value=\"\"></p>\n"; + $rawData .= "<p>".$this->yellow->language->getText("installLanguage")."</p>\n<p>"; + foreach ($languages as $language) { + $checked = $language==$this->yellow->language->language ? " checked=\"checked\"" : ""; + $rawData .= "<label for=\"{$language}-language\"><input type=\"radio\" name=\"language\" id=\"{$language}-language\" value=\"$language\"$checked> ".$this->yellow->language->getTextHtml("languageDescription", $language)."</label><br />"; + } + $rawData .= "</p>\n"; + $rawData .= "<p>".$this->yellow->language->getText("installExtension")."</p>\n<p>"; + foreach (array("website", "wiki", "blog") as $extension) { + $checked = $extension=="website" ? " checked=\"checked\"" : ""; + $rawData .= "<label for=\"{$extension}-extension\"><input type=\"radio\" name=\"extension\" id=\"{$extension}-extension\" value=\"$extension\"$checked> ".$this->yellow->language->getTextHtml("installExtension".ucfirst($extension))."</label><br />"; + } + $rawData .= "</p>\n"; + $rawData .= "<input class=\"btn\" type=\"submit\" value=\"".$this->yellow->language->getText("installButton")."\" />\n"; + $rawData .= "<input type=\"hidden\" name=\"status\" value=\"install\" />\n"; + $rawData .= "</form>\n"; + return $rawData; + } + + // Return progress in percent + public function getProgressPercent($now, $total, $increments, $max) { + $max = intval($max/$increments) * $increments; + $percent = intval(($max/$total) * $now); + if ($increments>1) $percent = intval($percent/$increments) * $increments; + return min($max, $percent); + } + + // Check if running built-in web server + public function isServerBuiltin() { + list($name) = $this->yellow->toolbox->detectServerInformation(); + return strtoloweru($name)=="built-in"; + } +} diff --git a/system/workers/markdown.php b/system/workers/markdown.php @@ -0,0 +1,4080 @@ +<?php +// Markdown extension, https://github.com/annaesvensson/yellow-markdown + +class YellowMarkdown { + const VERSION = "0.9.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 YellowMarkdownParser($this->yellow, $page); + $text = $markdown->transform($text); + $text = $this->yellow->lookup->normaliseData($text, "html"); + return $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.9.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|null + */ + public $url_filter_func = null; + + /** + * Optional header id="" generation callback function. + * @var callable|null + */ + public $header_id_func = null; + + /** + * Optional function for converting code block content to HTML + * @var callable|null + */ + public $code_block_content_func = null; + + /** + * Optional function for converting code span content to HTML. + * @var callable|null + */ + 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) { + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] === '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + // If the URL was of the form <s p a c e s> it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using + // the URL. + $unhashed = $this->unhash($url); + if ($unhashed !== $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + /** + * Turn Markdown image shortcuts into <img> tags. + * @param string $text + * @return string + */ + protected function doImages($text) { + // First, handle reference-style labeled images: ![alt text][id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array($this, '_doImages_reference_callback'), $text); + + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + ) + }xs', + array($this, '_doImages_inline_callback'), $text); + + return $text; + } + + /** + * Callback to parse references image tags + * @param array $matches + * @return string + */ + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); // for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeURLAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } else { + // If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + + /** + * Callback to parse inline image tags + * @param array $matches + * @return string + */ + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeURLAttribute($url); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; // $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + /** + * Parse Markdown heading elements to HTML + * @param string $text + * @return string + */ + protected function doHeaders($text) { + /** + * Setext-style headers: + * Header 1 + * ======== + * + * Header 2 + * -------- + */ + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array($this, '_doHeaders_callback_setext'), $text); + + /** + * atx-style headers: + * # Header 1 + * ## Header 2 + * ## Header 2 with closing hashes ## + * ... + * ###### Header 6 + */ + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array($this, '_doHeaders_callback_atx'), $text); + + return $text; + } + + /** + * Setext header parsing callback + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_setext($matches) { + // Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) { + return $matches[0]; + } + + $level = $matches[2][0] == '=' ? 1 : 2; + + // ID attribute generation + $idAtt = $this->_generateIdFromHeaderValue($matches[1]); + + $block = "<h$level$idAtt>".$this->runSpanGamut($matches[1])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * ATX header parsing callback + * @param array $matches + * @return string + */ + protected function _doHeaders_callback_atx($matches) { + // ID attribute generation + $idAtt = $this->_generateIdFromHeaderValue($matches[2]); + + $level = strlen($matches[1]); + $block = "<h$level$idAtt>".$this->runSpanGamut($matches[2])."</h$level>"; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + /** + * If a header_id_func property is set, we can use it to automatically + * generate an id attribute. + * + * This method returns a string in the form id="foo", or an empty string + * otherwise. + * @param string $headerValue + * @return string + */ + protected function _generateIdFromHeaderValue($headerValue) { + if (!is_callable($this->header_id_func)) { + return ""; + } + + $idValue = call_user_func($this->header_id_func, $headerValue); + if (!$idValue) { + return ""; + } + + return ' id="' . $this->encodeAttribute($idValue) . '"'; + } + + /** + * Form HTML ordered (numbered) and unordered (bulleted) lists. + * @param string $text + * @return string + */ + protected function doLists($text) { + $less_than_tab = $this->tab_width - 1; + + // Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + + $markers_relist = array( + $marker_ul_re => $marker_ol_re, + $marker_ol_re => $marker_ul_re, + ); + + foreach ($markers_relist as $marker_re => $other_marker_re) { + // Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces + ('.$marker_re.') # $4 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $5 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + | + (?= # Lookahead for another kind of list + \n + \3 # Must have the same indentation + '.$other_marker_re.'[ ]+ + ) + ) + ) + '; // mx + + // We use a different prefix before nested lists than top-level lists. + //See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array($this, '_doLists_callback'), $text); + } else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array($this, '_doLists_callback'), $text); + } + } + + return $text; + } + + /** + * List parsing callback + * @param array $matches + * @return string + */ + protected function _doLists_callback($matches) { + // Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + $marker_ol_start_re = '[0-9]+'; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $ol_start = 1; + if ($this->enhanced_ordered_list) { + // Get the start number for ordered list. + if ($list_type == 'ol') { + $ol_start_array = array(); + $ol_start_check = preg_match("/$marker_ol_start_re/", $matches[4], $ol_start_array); + if ($ol_start_check){ + $ol_start = $ol_start_array[0]; + } + } + } + + if ($ol_start > 1 && $list_type == 'ol'){ + $result = $this->hashBlock("<$list_type start=\"$ol_start\">\n" . $result . "</$list_type>"); + } else { + $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>"); + } + return "\n". $result ."\n\n"; + } + + /** + * Nesting tracker for list levels + * @var integer + */ + protected $list_level = 0; + + /** + * Process the contents of a single ordered or unordered list, splitting it + * into individual list items. + * @param string $list_str + * @param string $marker_any_re + * @return string + */ + protected function processListItems($list_str, $marker_any_re) { + /** + * The $this->list_level global keeps track of when we're inside a list. + * Each time we enter a list, we increment it; when we leave a list, + * we decrement. If it's zero, we're not in a list anymore. + * + * We do this because when we're not inside a list, we want to treat + * something like this: + * + * I recommend upgrading to version + * 8. Oops, now this line is treated + * as a sub-list. + * + * As a single paragraph, despite the fact that the second line starts + * with a digit-period-space sequence. + * + * Whereas when we're inside a list (or sub-list), that line will be + * treated as the start of a sub-list. What a kludge, huh? This is + * an aspect of Markdown's syntax that's hard to parse perfectly + * without resorting to mind-reading. Perhaps the solution is to + * change the syntax rules such that sub-lists must start with a + * starting cardinal number; e.g. "1." or "a.". + */ + $this->list_level++; + + // Trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array($this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + + /** + * List item parsing callback + * @param array $matches + * @return string + */ + protected function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + // Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } else { + // Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = $this->formParagraphs($item, false); + } + + return "<li>" . $item . "</li>\n"; + } + + /** + * Process Markdown `<pre><code>` blocks. + * @param string $text + * @return string + */ + protected function doCodeBlocks($text) { + $text = preg_replace_callback('{ + (?:\n\n|\A\n?) + ( # $1 = the code block -- one or more lines, starting with a space/tab + (?> + [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc + }xm', + array($this, '_doCodeBlocks_callback'), $text); + + return $text; + } + + /** + * Code block parsing callback + * @param array $matches + * @return string + */ + protected function _doCodeBlocks_callback($matches) { + $codeblock = $matches[1]; + + $codeblock = $this->outdent($codeblock); + if (is_callable($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 (is_callable($this->code_span_content_func)) { + $code = call_user_func($this->code_span_content_func, $code); + } else { + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + } + return $this->hashPart("<code>$code</code>"); + } + + /** + * Define the emphasis operators with their regex matches + * @var array + */ + protected $em_relist = array( + '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?![\.,:;]?\s)', + '*' => '(?<![\s*])\*(?!\*)', + '_' => '(?<![\s_])_(?!_)', + ); + + /** + * Define the strong operators with their regex matches + * @var array + */ + protected $strong_relist = array( + '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?![\.,:;]?\s)', + '**' => '(?<![\s*])\*\*(?!\*)', + '__' => '(?<![\s_])__(?!_)', + ); + + /** + * Define the emphasis + strong operators with their regex matches + * @var array + */ + protected $em_strong_relist = array( + '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?![\.,:;]?\s)', + '***' => '(?<![\s*])\*\*\*(?!\*)', + '___' => '(?<![\s_])___(?!_)', + ); + + /** + * Container for prepared regular expressions + * @var array + */ + protected $em_strong_prepared_relist; + + /** + * Prepare regular expressions for searching emphasis tokens in any + * context. + * @return void + */ + protected function prepareItalicsAndBold() { + foreach ($this->em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + // Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + // Construct master expression from list. + $token_re = '{(' . implode('|', $token_relist) . ')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + /** + * Convert Markdown italics (emphasis) and bold (strong) to HTML + * @param string $text + * @return string + */ + protected function doItalicsAndBold($text) { + if ($this->in_emphasis_processing) { + return $text; // avoid reentrency + } + $this->in_emphasis_processing = true; + + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + // Get prepared regular expression for seraching emphasis tokens + // in current context. + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + // Each loop iteration search for the next emphasis token. + // Each token is then passed to handleSpanToken. + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + // Reached end of text span: empty stack without emitting. + // any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + // Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + // Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong><em>$span</em></strong>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + // Other closing marker: close one em or strong and + // change current token state to match the other + $token_stack[0] = str_repeat($token[0], 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; // $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + // Reached closing marker for both em and strong. + // Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span</$tag>"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; // $$tag stands for $em or $strong + } + } else { + // Reached opening three-char emphasis marker. Push on token + // stack; will be handled by the special condition above. + $em = $token[0]; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + // Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + $em = ''; + } + // Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<strong>$span</strong>"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + // Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + // Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<em>$span</em>"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + $this->in_emphasis_processing = false; + return $text_stack[0]; + } + + /** + * Parse Markdown blockquotes to HTML + * @param string $text + * @return string + */ + protected function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array($this, '_doBlockQuotes_callback'), $text); + + return $text; + } + + /** + * Blockquote parsing callback + * @param array $matches + * @return string + */ + protected function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + // trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); // recurse + + $bq = preg_replace('/^/m', " ", $bq); + // These leading spaces cause problem with <pre> content, + // so we need to fix that: + $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx', + array($this, '_doBlockQuotes_callback2'), $bq); + + return "\n" . $this->hashBlock("<blockquote>\n$bq\n</blockquote>") . "\n\n"; + } + + /** + * Blockquote parsing callback + * @param array $matches + * @return string + */ + protected function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + /** + * Parse paragraphs + * + * @param string $text String to process in paragraphs + * @param boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags + * @return string + */ + protected function formParagraphs($text, $wrap_in_p = true) { + // Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + // Wrap <p> tags and unhashify HTML blocks + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + // Is a paragraph. + $value = $this->runSpanGamut($value); + if ($wrap_in_p) { + $value = preg_replace('/^([ ]*)/', "<p>", $value); + $value .= "</p>"; + } + $grafs[$key] = $this->unhash($value); + } else { + // Is a block. + // Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 = <div> tag +// <div \s+ +// [^>]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (</div>) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// // We can't call Markdown(), because that resets the hash; +// // that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// // Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + /** + * Encode text for a double-quoted HTML attribute. This function + * is *not* suitable for attributes enclosed in single quotes. + * @param string $text + * @return string + */ + protected function encodeAttribute($text) { + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '&quot;', $text); + return $text; + } + + /** + * Encode text for a double-quoted HTML attribute containing a URL, + * applying the URL filter if set. Also generates the textual + * representation for the URL (removing mailto: or tel:) storing it in $text. + * This function is *not* suitable for attributes enclosed in single quotes. + * + * @param string $url + * @param string $text Passed by reference + * @return string URL + */ + protected function encodeURLAttribute($url, &$text = null) { + if (is_callable($this->url_filter_func)) { + $url = call_user_func($this->url_filter_func, $url); + } + + if (preg_match('{^mailto:}i', $url)) { + $url = $this->encodeEntityObfuscatedAttribute($url, $text, 7); + } else if (preg_match('{^tel:}i', $url)) { + $url = $this->encodeAttribute($url); + $text = substr($url, 4); + } else { + $url = $this->encodeAttribute($url); + $text = $url; + } + + return $url; + } + + /** + * Smart processing for ampersands and angle brackets that need to + * be encoded. Valid character entities are left alone unless the + * no-entities mode is set. + * @param string $text + * @return string + */ + protected function encodeAmpsAndAngles($text) { + if ($this->no_entities) { + $text = str_replace('&', '&amp;', $text); + } else { + // Ampersand-encoding based entirely on Nat Irons's Amputator + // MT plugin: <http://bumppo.net/projects/amputator/> + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&amp;', $text); + } + // Encode remaining <'s + $text = str_replace('<', '&lt;', $text); + + return $text; + } + + /** + * Parse Markdown automatic links to anchor HTML tags + * @param string $text + * @return string + */ + protected function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i', + array($this, '_doAutoLinks_url_callback'), $text); + + // Email addresses: <address@domain.foo> + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + (?: + [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ + | + ".*?" + ) + \@ + (?: + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + | + \[[\d.a-fA-F:]+\] # IPv4 & IPv6 + ) + ) + > + }xi', + array($this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + + /** + * Parse URL callback + * @param array $matches + * @return string + */ + protected function _doAutoLinks_url_callback($matches) { + $url = $this->encodeURLAttribute($matches[1], $text); + $link = "<a href=\"$url\">$text</a>"; + return $this->hashPart($link); + } + + /** + * Parse email address callback + * @param array $matches + * @return string + */ + protected function _doAutoLinks_email_callback($matches) { + $addr = $matches[1]; + $url = $this->encodeURLAttribute("mailto:$addr", $text); + $link = "<a href=\"$url\">$text</a>"; + return $this->hashPart($link); + } + + /** + * Input: some text to obfuscate, e.g. "mailto:foo@example.com" + * + * Output: the same text but with most characters encoded as either a + * decimal or hex entity, in the hopes of foiling most address + * harvesting spam bots. E.g.: + * + * &#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111; + * &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111; + * &#x6d; + * + * Note: the additional output $tail is assigned the same value as the + * ouput, minus the number of characters specified by $head_length. + * + * Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + * With some optimizations by Milian Wolff. Forced encoding of HTML + * attribute special characters by Allan Odgaard. + * + * @param string $text + * @param string $tail Passed by reference + * @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 Passed by reference + * @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. + * @var string + */ + public $fn_link_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. + * Optionally use '^^' and '%%' to refer to the footnote number and + * reference number respectively. {@see parseFootnotePlaceholders()} + * @var string + */ + public $fn_backlink_html = '&#8617;&#xFE0E;'; + + /** + * Optional title and aria-label attributes for footnote backlinks for + * added accessibility (to ensure backlink uniqueness). + * Use '^^' and '%%' to refer to the footnote number and reference number + * respectively. {@see parseFootnotePlaceholders()} + * @var string + */ + public $fn_backlink_title = ""; + public $fn_backlink_label = ""; + + /** + * 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; + + /** + * Determines whether footnotes should be appended to the end of the document. + * If true, footnote html can be retrieved from $this->footnotes_assembled. + * @var boolean + */ + public $omit_footnotes = false; + + + /** + * After parsing, the HTML for the list of footnotes appears here. + * This is available only if $omit_footnotes == true. + * + * Note: when placing the content of `footnotes_assembled` on the page, + * consider adding the attribute `role="doc-endnotes"` to the `div` or + * `section` that will enclose the list of footnotes so they are + * reachable to accessibility tools the same way they would be with the + * default HTML output. + * @var null|string + */ + public $footnotes_assembled = null; + + /** + * 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; + + /** + * Ref attribute for links + * @var array + */ + protected $ref_attr = array(); + + /** + * 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; + $this->footnotes_assembled = null; + + 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 = ''; + + if ( ! $this->omit_footnotes ) + $this->footnotes_assembled = null; + + 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 === false || $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. + + // 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 bool $md_attr Handle `markdown="1"` attribute + * @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. + $base_tag_name_re = ''; + + // 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. + $mode = $attr_m[2] . $attr_m[3]; + $span_mode = $mode === 'span' || ($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 .= (string) $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) { + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] === '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + + // if the URL was of the form <s p a c e s> it got caught by the HTML + // tag parser and hashed. Need to reverse the process before using the URL. + $unhashed = $this->unhash($url); + if ($unhashed !== $url) + $url = preg_replace('/^<(.*)>$/', '\1', $unhashed); + + $url = $this->encodeURLAttribute($url); + + $result = "<a href=\"$url\""; + if (isset($title)) { + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $attr; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text</a>"; + + return $this->hashPart($result); + } + + /** + * Turn Markdown image shortcuts into <img> tags. + * @param string $text + * @return string + */ + protected function doImages($text) { + // First, handle reference-style labeled images: ![alt text][id] + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + (' . $this->nested_brackets_re . ') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array($this, '_doImages_reference_callback'), $text); + + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + (' . $this->nested_brackets_re . ') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + (' . $this->nested_url_parenthesis_re . ') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + (?:[ ]? ' . $this->id_class_attr_catch_re . ' )? # $8 = id/class attributes + ) + }xs', + array($this, '_doImages_inline_callback'), $text); + + return $text; + } + + /** + * Callback for referenced images + * @param array $matches + * @return string + */ + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id === "") { + $link_id = strtolower($alt_text); // for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeURLAttribute($this->urls[$link_id]); + $result = "<img src=\"$url\" alt=\"$alt_text\""; + if (isset($this->titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) { + $result .= $this->ref_attr[$link_id]; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + // If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + + /** + * Callback for inline images + * @param array $matches + * @return string + */ + protected function _doImages_inline_callback($matches) { + $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) { + + $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 ) ) { + $this->_doFootnotes(); + if ( ! $this->omit_footnotes ) { + $text .= "\n\n"; + $text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n"; + $text .= "<hr" . $this->empty_element_suffix . "\n"; + $text .= $this->footnotes_assembled; + $text .= "</div>"; + } + } + return $text; + } + + + /** + * Generates the HTML for footnotes. Called by appendFootnotes, even if + * footnotes are not being appended. + * @return void + */ + protected function _doFootnotes() { + $attr = array(); + if ($this->fn_backlink_class !== "") { + $class = $this->fn_backlink_class; + $class = $this->encodeAttribute($class); + $attr['class'] = " class=\"$class\""; + } + $attr['role'] = " role=\"doc-backlink\""; + $num = 0; + + $text = "<ol>\n\n"; + 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); + + $num++; + $note_id = $this->encodeAttribute($note_id); + + // Prepare backlink, multiple backlinks if multiple references + // Do not create empty backlinks if the html is blank + $backlink = ""; + if (!empty($this->fn_backlink_html)) { + for ($ref_num = 1; $ref_num <= $ref_count; ++$ref_num) { + if (!empty($this->fn_backlink_title)) { + $attr['title'] = ' title="' . $this->encodeAttribute($this->fn_backlink_title) . '"'; + } + if (!empty($this->fn_backlink_label)) { + $attr['label'] = ' aria-label="' . $this->encodeAttribute($this->fn_backlink_label) . '"'; + } + $parsed_attr = $this->parseFootnotePlaceholders( + implode('', $attr), + $num, + $ref_num + ); + $backlink_text = $this->parseFootnotePlaceholders( + $this->fn_backlink_html, + $num, + $ref_num + ); + $ref_count_mark = $ref_num > 1 ? $ref_num : ''; + $backlink .= " <a href=\"#fnref$ref_count_mark:$note_id\"$parsed_attr>$backlink_text</a>"; + } + $backlink = trim($backlink); + } + + // Add backlink to last paragraph; create new paragraph if needed. + if (!empty($backlink)) { + if (preg_match('{</p>$}', $footnote)) { + $footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>"; + } else { + $footnote .= "\n\n<p>$backlink</p>"; + } + } + + $text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n"; + $text .= $footnote . "\n"; + $text .= "</li>\n\n"; + } + $text .= "</ol>\n"; + + $this->footnotes_assembled = $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] . "]"; + } + + /** + * Build footnote label by evaluating any placeholders. + * - ^^ footnote number + * - %% footnote reference number (Nth reference to footnote number) + * @param string $label + * @param int $footnote_number + * @param int $reference_number + * @return string + */ + protected function parseFootnotePlaceholders($label, $footnote_number, $reference_number) { + return str_replace( + array('^^', '%%'), + array($footnote_number, $reference_number), + $label + ); + } + + + /** + * 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>"); + } + $desc = $this->encodeAttribute($desc); + return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>"); + } + return $matches[0]; + } +} + +// Markdown parser, Copyright Datenstrom, License GPLv2 + +class YellowMarkdownParser extends MarkdownExtraParser { + public $yellow; // access to API + public $page; // access to page + public $idAttributes; // id attributes + public $noticeLevel; // recursive level + + public function __construct($yellow, $page) { + $this->yellow = $yellow; + $this->page = $page; + $this->idAttributes = array(); + $this->noticeLevel = 0; + $this->url_filter_func = function($url) use ($yellow, $page) { + return $yellow->lookup->normaliseLocation($url, $page->getPage("main")->location); + }; + $this->span_gamut += array("doStrikethrough" => 55); + $this->block_gamut += array("doNoticeBlocks" => 65); + $this->document_gamut += array("doFootnotesLinks" => 55); + $this->escape_chars .= "~"; + parent::__construct(); + } + + // Handle striketrough + public function doStrikethrough($text) { + $parts = preg_split("/(?<![~])(~~)(?![~])/", $text, -1, PREG_SPLIT_DELIM_CAPTURE); + if (count($parts)>3) { + $text = ""; + $open = false; + foreach ($parts as $part) { + if ($part=="~~") { + $text .= $open ? "</del>" : "<del>"; + $open = !$open; + } else { + $text .= $part; + } + } + if ($open) $text .= "</del>"; + } + return $text; + } + + // 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->parseContentElement($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->parseContentElement($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->parseContentElement("", $matches[1], "", "symbol"); + return is_null($output) ? $matches[0] : $this->hashPart($output); + } + + // Handle fenced code blocks + public function _doFencedCodeBlocks_callback($matches) { + $name = $this->getBlockName($matches[2], $matches[3]); + $text = $matches[4]; + $attributes = $matches[3]; + $output = $this->page->parseContentElement($name, $text, $attributes, "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 (is_string_empty($attr) && $level>=2) $attr = $this->getIdAttribute($text); + $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; + return "\n".$this->hashBlock($output)."\n\n"; + } + + // 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 (is_string_empty($attr) && $level>=2) $attr = $this->getIdAttribute($text); + $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>"; + return "\n".$this->hashBlock($output)."\n\n"; + } + + // Handle inline links + public function _doAnchors_inline_callback($matches) { + $url = $matches[3]=="" ? $matches[4] : $matches[3]; + $text = $matches[2]; + $title = isset($matches[7]) ? $matches[7] : ""; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + $output = "<a href=\"".$this->encodeURLAttribute($url)."\""; + if (!is_string_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) { + $src = $matches[3]=="" ? $matches[4] : $matches[3]; + if (!preg_match("/^\w+:/", $src)) { + $src = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreImageLocation").$src; + } + $alt = $matches[2]; + $title = isset($matches[7]) ? $matches[7] : $matches[2]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + $output = "<img src=\"".$this->encodeURLAttribute($src)."\""; + if (!is_string_empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\""; + if (!is_string_empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\""; + $output .= $attr; + $output .= $this->empty_element_suffix; + return $this->hashPart($output); + } + + // Handle lists, task list + public function _processListItems_callback($matches) { + $attr = ""; + $item = $matches[4]; + $leadingLine = $matches[1]; + $tailingLine = $matches[5]; + if ($leadingLine || $tailingLine || preg_match('/\n{2,}/', $item)) + { + $item = $matches[2].str_repeat(' ', strlen($matches[3])).$item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } else { + $item = $this->doLists($this->outdent($item)); + $item = $this->formParagraphs($item, false); + $token = substr($item, 0, 4); + if ($token=="[ ] " || $token=="[x] ") { + $attr = " class=\"task-list-item\""; + $item = ($token=='[ ] ' ? "<input type=\"checkbox\" disabled=\"disabled\" /> " : + "<input type=\"checkbox\" disabled=\"disabled\" checked=\"checked\" /> ").substr($item, 4); + } + } + return "<li$attr>".$item."</li>\n"; + } + + // Handle blockquotes, CommonMark compatible + public function doBlockQuotes($text) { + return preg_replace_callback("/((?>^[ ]*>[ ]?.+\n(.+\n)*)+)/m", array($this, "_doBlockQuotes_callback"), $text); + } + + // Handle notice blocks + public function doNoticeBlocks($text) { + return preg_replace_callback("/((?>^[ ]*!(?!\[)[ ]?.+\n(.+\n)*)+)/m", array($this, "_doNoticeBlocks_callback"), $text); + } + + // Handle notice blocks over multiple lines + public function _doNoticeBlocks_callback($matches) { + $name = $attributes = $attr = ""; + $text = preg_replace("/^[ ]*![ ]?/m", "", $matches[1]); + if (preg_match("/^[ ]*".$this->id_class_attr_catch_re."[ ]*\n([\S\s]*)$/m", $text, $parts)) { + $name = $this->getBlockName("", $parts[1]); + $text = $parts[2]; + $attributes = $parts[1]; + $attr = $this->doExtraAttributes("div", $parts[1]); + } elseif ($this->noticeLevel==0) { + $level = strspn(str_replace(array("![", " "), "", $matches[1]), "!"); + $attr = " class=\"notice$level\""; + } + if (!is_string_empty($text)) { + ++$this->noticeLevel; + $output = $this->page->parseContentElement($name, "[--notice--]", $attributes, "notice"); + if (!is_null($output) && preg_match("/^(.+)(\[--notice--\])(.+)$/s", $output, $parts)) { + $output = $parts[1].$this->runBlockGamut($text).$parts[3]; + } else { + $output = "<div$attr>\n".$this->runBlockGamut($text)."\n</div>"; + } + --$this->noticeLevel; + } else { + $output = "<div$attr></div>"; + } + return "\n".$this->hashBlock($output)."\n\n"; + } + + // Handle footnotes links, normalise ids and links + public function doFootnotesLinks($text) { + if (!is_null($this->footnotes_assembled)) { + $callbackId = function ($matches) { + $id = str_replace(":", "-", $matches[2]); + return "<$matches[1] id=\"$id\" $matches[3]>"; + }; + $text = preg_replace_callback("/<(li|sup) id=\"(fn:\d+)\"(.*?)>/", $callbackId, $text); + $text = preg_replace_callback("/<(li|sup) id=\"(fnref\d*:\d+)\"(.*?)>/", $callbackId, $text); + $callbackHref = function ($matches) { + $href = $this->page->base.$this->page->location.str_replace(":", "-", $matches[2]); + return "<$matches[1] href=\"$href\" $matches[3]>"; + }; + $text = preg_replace_callback("/<(a) href=\"(#fn:\d+)\"(.*?)>/", $callbackHref, $text); + $text = preg_replace_callback("/<(a) href=\"(#fnref\d*:\d+)\"(.*?)>/", $callbackHref, $text); + } + return $text; + } + + // Return suitable name for code block or notice block + public function getBlockName($language, $attributes) { + if (!is_string_empty($language)) { + $name = ltrim($language, "."); + } else { + $name = ""; + foreach (explode(" ", $attributes) as $token) { + if (substru($token, 0, 1)==".") { $name = substru($token, 1); break; } + } + } + return $name; + } + + // Return unique id attribute + public function getIdAttribute($text) { + $attr = ""; + $text = $this->yellow->lookup->normaliseName($text, true, false, true); + $text = trim(preg_replace("/-+/", "-", $text), "-"); + if (!isset($this->idAttributes[$text])) { + $this->idAttributes[$text] = $text; + $attr = " id=\"$text\""; + } + return $attr; + } +} diff --git a/system/workers/serve.php b/system/workers/serve.php @@ -0,0 +1,61 @@ +<?php +// Serve extension, https://github.com/annaesvensson/yellow-serve + +class YellowServe { + const VERSION = "0.9.1"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle command + public function onCommand($command, $text) { + switch ($command) { + case "serve": $statusCode = $this->processCommandServe($command, $text); break; + default: $statusCode = 0; + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + return "serve [url]"; + } + + // Process command to start web server + public function processCommandServe($command, $text) { + list($url) = $this->yellow->toolbox->getTextArguments($text); + if (is_string_empty($url)) $url = "http://localhost:8000/"; + list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($url); + if ($scheme=="http" && !is_string_empty($address) && is_string_empty($base)) { + if (!preg_match("/\:\d+$/", $address)) $address .= ":8000"; + if ($this->checkServerSettings("$scheme://$address/")) { + echo "Starting web server. Open a web browser and go to $scheme://$address/\n"; + echo "Press Ctrl+C to quit...\n"; + exec(PHP_BINARY." -S $address yellow.php 2>&1", $outputLines, $returnStatus); + $statusCode = $returnStatus!=0 ? 500 : 200; + if ($statusCode!=200) { + $output = !is_array_empty($outputLines) ? end($outputLines) : "Please check arguments!"; + if (preg_match("/^\[(.*?)\]\s*(.*)$/", $output, $matches)) $output = $matches[2]; + echo "ERROR starting web server: $output\n"; + } + } else { + $statusCode = 400; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + echo "ERROR starting web server: Please configure `CoreServerUrl: auto` in file '$fileName'!\n"; + } + } else { + $statusCode = 400; + echo "Yellow $command: Invalid arguments\n"; + } + return $statusCode; + } + + // Check server settings + public function checkServerSettings($url) { + return $this->yellow->system->get("coreServerUrl")=="auto" || + $this->yellow->system->get("coreServerUrl")==$url; + } +} diff --git a/system/workers/stockholm.php b/system/workers/stockholm.php @@ -0,0 +1,22 @@ +<?php +// Stockholm extension, https://github.com/annaesvensson/yellow-stockholm + +class YellowStockholm { + const VERSION = "0.9.1"; + public $yellow; // access to API + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + } + + // Handle update + public function onUpdate($action) { + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + if ($action=="install") { + $this->yellow->system->save($fileName, array("theme" => "stockholm")); + } elseif ($action=="uninstall" && $this->yellow->system->get("theme")=="stockholm") { + $this->yellow->system->save($fileName, array("theme" => $this->yellow->system->getDifferent("theme"))); + } + } +} diff --git a/system/workers/update.php b/system/workers/update.php @@ -0,0 +1,940 @@ +<?php +// Update extension, https://github.com/annaesvensson/yellow-update + +class YellowUpdate { + const VERSION = "0.9.1"; + const PRIORITY = "2"; + public $yellow; // access to API + public $extensions; // number of extensions + + // Handle initialisation + public function onLoad($yellow) { + $this->yellow = $yellow; + $this->yellow->system->setDefault("updateCurrentRelease", "none"); + $this->yellow->system->setDefault("updateAvailableUrl", "auto"); + $this->yellow->system->setDefault("updateAvailableFile", "update-available.ini"); + $this->yellow->system->setDefault("updateExtensionFile", "extension.ini"); + $this->yellow->system->setDefault("updateEventPending", "none"); + $this->yellow->system->setDefault("updateEventDaily", "0"); + $this->yellow->system->setDefault("updateTrashTimeout", "7776660"); + } + + // Handle update + public function onUpdate($action) { + if ($action=="clean" || $action=="update") { //TODO: remove later, for backwards compatibility + $fileNameOld = $this->yellow->system->get("coreExtensionDirectory")."update-latest.ini"; + if (is_file($fileNameOld) && !$this->yellow->toolbox->deleteFile($fileNameOld)) { + $this->yellow->toolbox->log("error", "Can't delete file '$fileNameOld'!"); + } + } + if ($action=="clean" || $action=="daily") { + $statusCode = 200; + $path = $this->yellow->system->get("coreExtensionDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.download$/", false, false) as $entry) { + if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + } + if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!"); + $statusCode = 200; + $path = $this->yellow->system->get("coreTrashDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) { + $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout"); + if ($expire<=time() && !$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500; + } + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, true) as $entry) { + $expire = $this->yellow->toolbox->getFileDeleted($entry) + $this->yellow->system->get("updateTrashTimeout"); + if ($expire<=time() && !$this->yellow->toolbox->deleteDirectory($entry)) $statusCode = 500; + } + if ($statusCode==500) $this->yellow->toolbox->log("error", "Can't delete files in directory '$path'!"); + } + } + + // Handle request + public function onRequest($scheme, $address, $base, $location, $fileName) { + return $this->processRequestPending($scheme, $address, $base, $location, $fileName); + } + + // Handle command + public function onCommand($command, $text) { + $statusCode = $this->processCommandPending(); + if ($statusCode==0) { + switch ($command) { + case "about": $statusCode = $this->processCommandAbout($command, $text); break; + case "install": $statusCode = $this->processCommandInstall($command, $text); break; + case "uninstall": $statusCode = $this->processCommandUninstall($command, $text); break; + case "update": $statusCode = $this->processCommandUpdate($command, $text); break; + default: $statusCode = 0; break; + } + } + return $statusCode; + } + + // Handle command help + public function onCommandHelp() { + return array("about [extension]", "install [extension]", "uninstall [extension]", "update [extension]"); + } + + // Handle page content element + public function onParseContentElement($page, $name, $text, $attributes, $type) { + $output = null; + if ($name=="yellow" && $type=="inline") { + if ($text=="about") { + list($dummy, $settingsCurrent) = $this->getExtensionSettings(true); + $output = "Datenstrom Yellow ".YellowCore::RELEASE."<br />\n"; + foreach ($settingsCurrent as $key=>$value) { + $output .= ucfirst($key)." ".$value->get("version")."<br />\n"; + } + } + if ($text=="release") $output = "Datenstrom Yellow ".YellowCore::RELEASE; + if ($text=="log") { + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreWebsiteFile"); + $fileHandle = @fopen($fileName, "rb"); + if ($fileHandle) { + clearstatcache(true, $fileName); + if (flock($fileHandle, LOCK_SH)) { + $dataBufferSize = 1024; + fseek($fileHandle, max(0, filesize($fileName) - $dataBufferSize)); + $dataBuffer = fread($fileHandle, $dataBufferSize); + if (strlenb($dataBuffer)==$dataBufferSize) { + $dataBuffer = ($pos = strposu($dataBuffer, "\n")) ? substru($dataBuffer, $pos+1) : $dataBuffer; + } + flock($fileHandle, LOCK_UN); + } + fclose($fileHandle); + } + $output = str_replace("\n", "<br />\n", htmlspecialchars($dataBuffer)); + } + } + return $output; + } + + // Process command to show current version + public function processCommandAbout($command, $text) { + $statusCode = 200; + $extensions = $this->getExtensionsFromText($text); + if (!is_array_empty($extensions)) { + list($statusCode, $settings) = $this->getExtensionAboutInformation($extensions); + if ($statusCode==200) { + foreach ($settings as $key=>$value) { + echo ucfirst($key)." ".$value->get("version")." - ".$this->getExtensionDescription($key, $value)."\n"; + if ($value->isExisting("documentationUrl")) echo "Read more at ".$value->get("documentationUrl")."\n"; + } + } + if ($statusCode>=400) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n"; + } else { + echo "Datenstrom Yellow ".YellowCore::RELEASE."\n"; + list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true); + foreach ($settingsCurrent as $key=>$value) { + echo ucfirst($key)." ".$value->get("version")."\n"; + } + } + return $statusCode; + } + + // Process command to install extensions + public function processCommandInstall($command, $text) { + $extensions = $this->getExtensionsFromText($text); + if (!is_array_empty($extensions)) { + $this->extensions = 0; + list($statusCode, $settings) = $this->getExtensionInstallInformation($extensions); + if ($statusCode==200) $statusCode = $this->downloadExtensions($settings); + if ($statusCode==200) $statusCode = $this->updateExtensions("install"); + if ($statusCode>=400) echo "ERROR installing files: ".$this->yellow->page->errorMessage."\n"; + echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; + echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed\n"; + } else { + list($statusCode, $settingsAvailable) = $this->getExtensionSettings(false); + foreach ($settingsAvailable as $key=>$value) { + echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n"; + } + if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n"; + } + return $statusCode; + } + + // Process command to uninstall extensions + public function processCommandUninstall($command, $text) { + $extensions = $this->getExtensionsFromText($text); + if (!is_array_empty($extensions)) { + $this->extensions = 0; + list($statusCode, $settings) = $this->getExtensionUninstallInformation($extensions, "core, update"); + if ($statusCode==200) $statusCode = $this->removeExtensions($settings); + if ($statusCode>=400) echo "ERROR uninstalling files: ".$this->yellow->page->errorMessage."\n"; + echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; + echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." uninstalled\n"; + } else { + list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true); + foreach ($settingsCurrent as $key=>$value) { + echo ucfirst($key)." - ".$this->getExtensionDescription($key, $value)."\n"; + } + if ($statusCode!=200) echo "ERROR checking extensions: ".$this->yellow->page->errorMessage."\n"; + } + return $statusCode; + } + + // Process command to update website + public function processCommandUpdate($command, $text) { + $extensions = $this->getExtensionsFromText($text); + if (!is_array_empty($extensions)) { + list($statusCode, $settings) = $this->getExtensionUpdateInformation($extensions); + if ($statusCode!=200 || !is_array_empty($settings)) { + $this->extensions = 0; + if ($statusCode==200) $statusCode = $this->downloadExtensions($settings); + if ($statusCode==200) $statusCode = $this->updateExtensions("update"); + if ($statusCode>=400) echo "ERROR updating files: ".$this->yellow->page->errorMessage."\n"; + echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated"; + echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." updated\n"; + } else { + echo "Your website is up to date\n"; + } + } else { + list($statusCode, $settings) = $this->getExtensionUpdateInformation(array("all")); + if (!is_array_empty($settings)) { + foreach ($settings as $key=>$value) { + echo ucfirst($key)." ".$value->get("version")."\n"; + } + echo "Yellow $command: Updates are available. Please type 'php yellow.php update all'.\n"; + } elseif ($statusCode!=200) { + echo "ERROR updating files: ".$this->yellow->page->errorMessage."\n"; + } else { + echo "Your website is up to date\n"; + } + } + return $statusCode; + } + + // Process command for pending events + public function processCommandPending() { + $statusCode = 0; + $this->extensions = 0; + $this->updatePatchPending(); + $this->updateEventPending(); + $statusCode = $this->updateExtensionPending(); + if ($statusCode==303) { + echo "Detected ZIP file".($this->extensions!=1 ? "s" : ""); + echo ", $this->extensions extension".($this->extensions!=1 ? "s" : "")." installed. Please run command again.\n"; + } + return $statusCode; + } + + // Process request for pending events + public function processRequestPending($scheme, $address, $base, $location, $fileName) { + $statusCode = 0; + if ($this->yellow->lookup->isContentFile($fileName)) { + $this->updatePatchPending(); + $this->updateEventPending(); + $statusCode = $this->updateExtensionPending(); + if ($statusCode==303) { + $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location); + $statusCode = $this->yellow->sendStatus(303, $location); + } + } + return $statusCode; + } + + // Download extensions + public function downloadExtensions($settings) { + $statusCode = 200; + $path = $this->yellow->system->get("coreExtensionDirectory"); + foreach ($settings as $key=>$value) { + $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; + list($statusCode, $fileData) = $this->getExtensionFile($value->get("downloadUrl")); + if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileName.".download", $fileData)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + if ($statusCode!=200) break; + } + if ($statusCode==200) { + foreach ($settings as $key=>$value) { + $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip"; + if (!$this->yellow->toolbox->renameFile($fileName.".download", $fileName)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + } + return $statusCode; + } + + // Update extensions + public function updateExtensions($action) { + $statusCode = 200; + if (function_exists("opcache_reset")) opcache_reset(); + $path = $this->yellow->system->get("coreExtensionDirectory"); + foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) { + $statusCode = max($statusCode, $this->updateExtensionArchive($entry, $action)); + 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, $action) { + $statusCode = 200; + $zip = new ZipArchive(); + if ($zip->open($path)===true) { + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updateExtensionArchive file:$path<br/>\n"; + $pathBase = ""; + if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1]; + $fileData = $zip->getFromName($pathBase.$this->yellow->system->get("updateExtensionFile")); + $settings = $this->yellow->toolbox->getTextSettings($fileData, ""); + list($extension, $version, $newModified, $oldModified) = $this->getExtensionInformation($settings); + if (!is_string_empty($extension) && !is_string_empty($version)) { + $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action, $settings)); + $paths = $this->getExtensionDirectories($zip, $pathBase); + foreach ($this->getExtensionFileNames($settings) as $fileName) { + list($entry, $flags) = $this->yellow->toolbox->getTextList($settings[$fileName], ",", 2); + if (!$this->yellow->lookup->isContentFile($fileName)) { + $fileNameSource = $pathBase.$entry; + $fileData = $zip->getFromName($fileNameSource); + $lastModified = $this->yellow->toolbox->getFileModified($fileName); + $statusCode = max($statusCode, $this->updateExtensionFile($fileName, $fileData, + $newModified, $oldModified, $lastModified, $flags, $extension)); + } else { + foreach ($this->getExtensionContentRootPages() as $page) { + list($fileNameSource, $fileNameDestination) = $this->getExtensionContentFileNames( + $fileName, $pathBase, $entry, $flags, $paths, $page); + $fileData = $zip->getFromName($fileNameSource); + $lastModified = $this->yellow->toolbox->getFileModified($fileNameDestination); + $statusCode = max($statusCode, $this->updateExtensionFile($fileNameDestination, $fileData, + $newModified, $oldModified, $lastModified, $flags, $extension)); + } + } + } + $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action)); + $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); + ++$this->extensions; + } else { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't detect file '$path'!"); + } + $zip->close(); + } else { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't open file '$path'!"); + } + return $statusCode; + } + + // Update extension from file + public function updateExtensionFile($fileName, $fileData, $newModified, $oldModified, $lastModified, $flags, $extension) { + $statusCode = 200; + $fileName = $this->yellow->lookup->normalisePath($fileName); + if ($this->yellow->lookup->isValidFile($fileName)) { + $create = $update = $delete = false; + if (preg_match("/create/i", $flags) && !is_file($fileName) && !is_string_empty($fileData)) $create = true; + if (preg_match("/update/i", $flags) && is_file($fileName) && !is_string_empty($fileData)) $update = true; + if (preg_match("/delete/i", $flags) && is_file($fileName)) $delete = true; + if (preg_match("/optional/i", $flags) && $this->yellow->extension->isExisting($extension)) $create = $update = $delete = false; + if (preg_match("/careful/i", $flags) && is_file($fileName) && $lastModified!=$oldModified) $update = false; + if ($create) { + if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) || + !$this->yellow->toolbox->modifyFile($fileName, $newModified)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + if ($update) { + if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory")) || + !$this->yellow->toolbox->createFile($fileName, $fileData) || + !$this->yellow->toolbox->modifyFile($fileName, $newModified)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + } + if ($delete) { + if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); + } + } + if ($this->yellow->system->get("coreDebugMode")>=2) { + $debug = "action:".($create ? "create" : "").($update ? "update" : "").($delete ? "delete" : ""); + if (!$create && !$update && !$delete) $debug = "action:none"; + echo "YellowUpdate::updateExtensionFile file:$fileName $debug<br/>\n"; + } + } + return $statusCode; + } + + // Update pending patches + public function updatePatchPending() { + $fileName = $this->yellow->system->get("coreWorkerDirectory")."updatepatch.bin"; + if (is_file($fileName)) { + if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUpdate::updatePatchPending file:$fileName<br/>\n"; + if (!$this->yellow->extension->isExisting("updatepatch")) { + require_once($fileName); + $this->yellow->extension->register("updatepatch", "YellowUpdatePatch"); + } + if ($this->yellow->extension->isExisting("updatepatch")) { + $value = $this->yellow->extension->data["updatepatch"]; + if (method_exists($value["object"], "onLoad")) $value["object"]->onLoad($this->yellow); + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("patch"); + } + unset($this->yellow->extension->data["updatepatch"]); + if (function_exists("opcache_reset")) opcache_reset(); + if (!$this->yellow->toolbox->deleteFile($fileName)) { + $this->yellow->toolbox->log("error", "Can't delete file '$fileName'!"); + } + } + } + + // Update pending events + public function updateEventPending() { + if ($this->yellow->system->get("updateCurrentRelease")!="none") { + if ($this->yellow->system->get("updateCurrentRelease")!=YellowCore::RELEASE) { + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, array("updateCurrentRelease" => YellowCore::RELEASE))) { + $this->yellow->toolbox->log("error", "Can't write file '$fileName'!"); + } else { + list($name, $version, $os) = $this->yellow->toolbox->detectServerInformation(); + $product = "Datenstrom Yellow ".YellowCore::RELEASE; + $this->yellow->toolbox->log("info", "Update $product, PHP ".PHP_VERSION.", $name $version, $os"); + } + } + if ($this->yellow->system->get("updateEventPending")!="none") { + foreach (explode(",", $this->yellow->system->get("updateEventPending")) as $token) { + list($extension, $action) = $this->yellow->toolbox->getTextList($token, "/", 2); + if ($this->yellow->extension->isExisting($extension) && $action!="uninstall") { + $value = $this->yellow->extension->data[$extension]; + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action); + } + } + $this->updateSystemSettings("all", $action); + $this->updateLanguageSettings("all", $action); + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, array("updateEventPending" => "none"))) { + $this->yellow->toolbox->log("error", "Can't write file '$fileName'!"); + } + } + if ($this->yellow->system->get("updateEventDaily")<=time()) { + foreach ($this->yellow->extension->data as $key=>$value) { + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate("daily"); + } + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, array("updateEventDaily" => $this->getTimestampDaily()))) { + $this->yellow->toolbox->log("error", "Can't write file '$fileName'!"); + } + } + } + } + + // Update pending extensions + public function updateExtensionPending() { + $statusCode = 0; + $path = $this->yellow->system->get("coreExtensionDirectory"); + if (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", false, false))) { + $statusCode = $this->updateExtensions("install"); + if ($statusCode==200) $statusCode = 303; + if ($statusCode>=400) { + $this->yellow->toolbox->log("error", $this->yellow->page->errorMessage); + $this->yellow->page->statusCode = 0; + $this->yellow->page->errorMessage = ""; + $statusCode = 303; + } + } + return $statusCode; + } + + // Update extension settings + public function updateExtensionSettings($extension, $action, $settings) { + $statusCode = 200; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreExtensionFile"); + $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName); + if ($action=="install" || $action=="update") { + $settingsCurrent = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + $settingsCurrent[$extension] = new YellowArray(); + foreach ($settings as $key=>$value) $settingsCurrent[$extension][$key] = $value; + $settingsCurrent->uksort("strnatcasecmp"); + $fileDataNew = ""; + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\#/", $line)) $fileDataNew = $line; + break; + } + foreach ($settingsCurrent as $extension=>$block) { + if (!is_string_empty($fileDataNew)) $fileDataNew .= "\n"; + foreach ($block as $key=>$value) { + $fileDataNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + } + } elseif ($action=="uninstall") { + $fileDataNew = $this->yellow->toolbox->unsetTextSettings($fileData, "extension", $extension); + } + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Update system settings + public function updateSystemSettings($extension, $action) { + $statusCode = 200; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName); + if ($action=="install" || $action=="update") { + $fileDataStart = $fileDataSettings = ""; + $settings = new YellowArray(); + $settings->exchangeArray($this->yellow->system->settingsDefaults->getArrayCopy()); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\#/", $line)) { + if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n"; + continue; + } + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + $settings[$matches[1]] = $matches[2]; + } + } + } + foreach ($settings as $key=>$value) { + $fileDataSettings .= ucfirst($key).(is_string_empty($value) ? ":\n" : ": $value\n"); + } + $fileDataNew = $fileDataStart.$fileDataSettings; + } elseif ($action=="uninstall") { + if (!is_string_empty($extension)) { + $fileDataNew = ""; + $regex = "/^".ucfirst($extension)."[A-Z]+/"; + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue; + } + $fileDataNew .= $line; + } + } + } + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Update language settings + public function updateLanguageSettings($extension, $action) { + $statusCode = 200; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreLanguageFile"); + $fileData = $fileDataNew = $this->yellow->toolbox->readFile($fileName); + if ($action=="install" || $action=="update") { + $fileDataStart = $fileDataSettings = $language = ""; + $settings = new YellowArray(); + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\#/", $line)) { + if (is_string_empty($fileDataStart)) $fileDataStart = $line."\n"; + continue; + } + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) { + if (lcfirst($matches[1])=="language") { + if (!is_array_empty($settings)) { + if (!is_string_empty($fileDataSettings)) $fileDataSettings .= "\n"; + foreach ($settings as $key=>$value) { + $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + } + $language = $matches[2]; + $settings = new YellowArray(); + $settings["language"] = $language; + $settings["languageLocale"] = "n/a"; + $settings["languageDescription"] = "n/a"; + $settings["languageTranslator"] = "Unknown"; + foreach ($this->yellow->language->settingsDefaults as $key=>$value) { + $require = preg_match("/^([a-z]*)[A-Z]+/", $key, $tokens) ? $tokens[1] : "core"; + if ($require=="language") $require = "core"; + if ($this->yellow->extension->isExisting($require)) { + if ($this->yellow->language->isText($key, $language)) { + $settings[$key] = $this->yellow->language->getText($key, $language); + } else { + $settings[$key] = $this->yellow->language->getText($key, "en"); + } + } + } + } + if (!is_string_empty($language)) { + $settings[$matches[1]] = $matches[2]; + } + } + } + } + if (!is_array_empty($settings)) { + if (!is_string_empty($fileDataSettings)) $fileDataSettings .= "\n"; + foreach ($settings as $key=>$value) { + $fileDataSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n"; + } + } + $fileDataNew = $fileDataStart.$fileDataSettings; + } elseif ($action=="uninstall") { + if (!is_string_empty($extension) && ucfirst($extension)!="Language") { + $fileDataNew = ""; + $regex = "/^".ucfirst($extension)."[A-Z]+/"; + foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) { + if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) { + if (!is_string_empty($matches[1]) && preg_match($regex, $matches[1])) continue; + } + $fileDataNew .= $line; + } + } + } + if ($fileData!=$fileDataNew && !$this->yellow->toolbox->createFile($fileName, $fileDataNew)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Update extension notification + public function updateExtensionNotification($extension, $action) { + $statusCode = 200; + if ($this->yellow->extension->isExisting($extension) && $action=="uninstall") { + $value = $this->yellow->extension->data[$extension]; + if (method_exists($value["object"], "onUpdate")) $value["object"]->onUpdate($action); + } + $updateEventPending = $this->yellow->system->get("updateEventPending"); + if ($updateEventPending=="none") $updateEventPending = ""; + if (!is_string_empty($updateEventPending)) $updateEventPending .= ","; + $updateEventPending .= "$extension/$action"; + $fileName = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreSystemFile"); + if (!$this->yellow->system->save($fileName, array("updateEventPending" => $updateEventPending))) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileName'!"); + } + return $statusCode; + } + + // Remove extensions + public function removeExtensions($settings) { + $statusCode = 200; + if (function_exists("opcache_reset")) opcache_reset(); + foreach ($settings as $extension=>$block) { + $statusCode = max($statusCode, $this->removeExtensionArchive($extension, "uninstall", $block)); + } + return $statusCode; + } + + // Remove extension archive + public function removeExtensionArchive($extension, $action, $settings) { + $statusCode = 200; + $fileNames = $this->getExtensionFileNames($settings, true); + if (!is_array_empty($fileNames)) { + $statusCode = max($statusCode, $this->updateExtensionNotification($extension, $action)); + foreach ($fileNames as $fileName) { + $statusCode = max($statusCode, $this->removeExtensionFile($fileName)); + } + if ($statusCode==200) { + $statusCode = max($statusCode, $this->updateExtensionSettings($extension, $action, $settings)); + $statusCode = max($statusCode, $this->updateSystemSettings($extension, $action)); + $statusCode = max($statusCode, $this->updateLanguageSettings($extension, $action)); + } + $version = $settings->get("version"); + $this->yellow->toolbox->log($statusCode==200 ? "info" : "error", ucfirst($action)." extension '".ucfirst($extension)." $version'"); + ++$this->extensions; + } else { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Please delete extension '$extension' manually!"); + } + return $statusCode; + } + + // Remove extension file + public function removeExtensionFile($fileName) { + $statusCode = 200; + $fileName = $this->yellow->lookup->normalisePath($fileName); + if ($this->yellow->lookup->isValidFile($fileName) && is_file($fileName)) { + if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->system->get("coreTrashDirectory"))) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!"); + } + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowUpdate::removeExtensionFile file:$fileName action:delete<br/>\n"; + } + } + return $statusCode; + } + + // Return extensions from text, space separated + public function getExtensionsFromText($text) { + return array_unique(array_filter($this->yellow->toolbox->getTextArguments($text), "strlen")); + } + + // Return extension about information + public function getExtensionAboutInformation($extensions) { + $settings = array(); + list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true); + $settingsCurrent["Datenstrom Yellow"] = new YellowArray(); + $settingsCurrent["Datenstrom Yellow"]["version"] = YellowCore::RELEASE; + $settingsCurrent["Datenstrom Yellow"]["description"] = "Datenstrom Yellow is for people who make small websites."; + $settingsCurrent["Datenstrom Yellow"]["documentationUrl"] = "https://datenstrom.se/yellow/"; + foreach ($extensions as $extension) { + $found = false; + if (strtoloweru($extension)=="yellow") $extension = "Datenstrom Yellow"; + foreach ($settingsCurrent as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension)) { + $settings[$key] = $settingsCurrent[$key]; + $found = true; + break; + } + } + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } + } + return array($statusCode, $settings); + } + + // Return extension install information + public function getExtensionInstallInformation($extensions) { + $settings = array(); + list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(true); + list($statusCodeAvailable, $settingsAvailable) = $this->getExtensionSettings(false); + $statusCode = max($statusCodeCurrent, $statusCodeAvailable); + foreach ($extensions as $extension) { + $found = false; + foreach ($settingsAvailable as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension)) { + if (!$settingsCurrent->isExisting($key)) $settings[$key] = $settingsAvailable[$key]; + $found = true; + break; + } + } + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } + } + return array($statusCode, $settings); + } + + // Return extension about information + public function getExtensionUninstallInformation($extensions, $extensionsProtected = "") { + $settings = array(); + list($statusCode, $settingsCurrent) = $this->getExtensionSettings(true); + foreach ($extensions as $extension) { + $found = false; + foreach ($settingsCurrent as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension)) { + $settings[$key] = $settingsCurrent[$key]; + $found = true; + break; + } + } + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } + } + $protected = preg_split("/\s*,\s*/", $extensionsProtected); + foreach ($settings as $key=>$value) { + if (in_array($key, $protected)) unset($settings[$key]); + } + return array($statusCode, $settings); + } + + // Return extension update information + public function getExtensionUpdateInformation($extensions) { + $settings = array(); + list($statusCodeCurrent, $settingsCurrent) = $this->getExtensionSettings(true); + list($statusCodeAvailable, $settingsAvailable) = $this->getExtensionSettings(false); + $statusCode = max($statusCodeCurrent, $statusCodeAvailable); + if (in_array("all", $extensions)) { + foreach ($settingsCurrent as $key=>$value) { + if ($settingsAvailable->isExisting($key)) { + $versionCurrent = $settingsCurrent[$key]->get("version"); + $versionAvailable = $settingsAvailable[$key]->get("version"); + if (strnatcasecmp($versionCurrent, $versionAvailable)<0) { + $settings[$key] = $settingsAvailable[$key]; + } + } + } + } else { + foreach ($extensions as $extension) { + $found = false; + foreach ($settingsCurrent as $key=>$value) { + if (strtoloweru($key)==strtoloweru($extension) && $settingsAvailable->isExisting($key)) { + $versionCurrent = $settingsCurrent[$key]->get("version"); + $versionAvailable = $settingsAvailable[$key]->get("version"); + if (strnatcasecmp($versionCurrent, $versionAvailable)<0) { + $settings[$key] = $settingsAvailable[$key]; + } + $found = true; + break; + } + } + if (!$found) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't find extension '$extension'!"); + } + } + } + return array($statusCode, $settings); + } + + // Return extension settings + public function getExtensionSettings($current) { + $statusCode = 200; + $settings = array(); + if ($current) { + $fileNameCurrent = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("coreExtensionFile"); + $fileData = $this->yellow->toolbox->readFile($fileNameCurrent); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + foreach ($settings->getArrayCopy() as $key=>$value) { + if (!$this->yellow->extension->isExisting($key)) unset($settings[$key]); + } + foreach ($this->yellow->extension->data as $key=>$value) { + if (!$settings->isExisting($key)) $settings[$key] = new YellowArray(); + $settings[$key]["extension"] = ucfirst($key); + $settings[$key]["version"] = $value["version"]; + } + } else { + $fileNameAvailable = $this->yellow->system->get("coreExtensionDirectory").$this->yellow->system->get("updateAvailableFile"); + $expire = $this->yellow->toolbox->getFileModified($fileNameAvailable) + 60*10; + if ($expire<=time()) { + $url = $this->yellow->system->get("updateAvailableUrl"); + if ($url=="auto") $url = "https://raw.githubusercontent.com/datenstrom/yellow/main/system/extensions/update-available.ini"; + list($statusCode, $fileData) = $this->getExtensionFile($url); + if ($statusCode==200 && !$this->yellow->toolbox->createFile($fileNameAvailable, $fileData)) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't write file '$fileNameAvailable'!"); + } + } + $fileData = $this->yellow->toolbox->readFile($fileNameAvailable); + $settings = $this->yellow->toolbox->getTextSettings($fileData, "extension"); + } + $settings->uksort("strnatcasecmp"); + return array($statusCode, $settings); + } + + // Return extension information + public function getExtensionInformation($settings) { + $extension = lcfirst($settings->get("extension")); + $version = $settings->get("version"); + $newModified = strtotime($settings->get("published")); + $oldModified = 0; + $invalid = false; + foreach ($settings as $key=>$value) { + if (strposu($key, "/")) { + $fileName = $this->yellow->lookup->normalisePath($key); + if (!$this->yellow->lookup->isValidFile($fileName)) $invalid = true; + if ($oldModified==0) $oldModified = $this->yellow->toolbox->getFileModified($fileName); + } + } + if ($invalid) $extension = $version = ""; + return array($extension, $version, $newModified, $oldModified); + } + + // Return extension directories + public function getExtensionDirectories($zip, $pathBase) { + $paths = array(); + for ($index=0; $index<$zip->numFiles; ++$index) { + $entry = substru($zip->getNameIndex($index), strlenu($pathBase)); + if (preg_match("#^(.*\/).*?$#", $entry, $matches)) { + array_push($paths, $matches[1]); + } + } + return array_unique($paths); + } + + // Return extension file names + public function getExtensionFileNames($settings, $reverse = false) { + $fileNames = array(); + foreach ($settings as $key=>$value) { + if (strposu($key, "/")) array_push($fileNames, $key); + } + if ($reverse) $fileNames = array_reverse($fileNames); + return $fileNames; + } + + // Return extension root pages for content files + public function getExtensionContentRootPages() { + return $this->yellow->content->scanLocation(""); + } + + // Return extension files names for content files + public function getExtensionContentFileNames($fileName, $pathBase, $entry, $flags, $paths, $page) { + if (preg_match("/multi-language/i", $flags)) { + $pathMultiLanguage = ""; + $languagesWanted = array($page->get("language"), "en"); + foreach ($languagesWanted as $language) { + foreach ($paths as $path) { + if ($this->yellow->lookup->normaliseToken(rtrim($path, "/"))==$language) { + $pathMultiLanguage = $path; + break; + } + } + if (!is_string_empty($pathMultiLanguage)) break; + } + $fileNameSource = $pathBase.$pathMultiLanguage.$entry; + } else { + $fileNameSource = $pathBase.$entry; + } + if ($this->yellow->system->get("coreMultiLanguageMode")) { + $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory")); + $fileNameDestination = $page->fileName.substru($fileName, $contentDirectoryLength); + } else { + $fileNameDestination = $fileName; + } + return array($fileNameSource, $fileNameDestination); + } + + // Return extension description including responsible developer/designer/translator + public function getExtensionDescription($key, $value) { + $description = $responsible = ""; + if ($value->isExisting("description")) $description = $value->get("description"); + if ($value->isExisting("developer")) $responsible = "Developed by ".$value["developer"]."."; + if ($value->isExisting("designer")) $responsible = "Designed by ".$value["designer"]."."; + if ($value->isExisting("translator")) $responsible = "Translated by ".$value["translator"]."."; + if (is_string_empty($description)) $description = "No description available."; + return "$description $responsible"; + } + + // Return extension file + public function getExtensionFile($url) { + $curlHandle = curl_init(); + curl_setopt($curlHandle, CURLOPT_URL, $this->getExtensionDownloadUrl($url)); + curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowUpdate/".YellowUpdate::VERSION."; SoftwareUpdater)"); + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30); + $fileData = curl_exec($curlHandle); + $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); + $redirectUrl = ($statusCode>=300 && $statusCode<=399) ? curl_getinfo($curlHandle, CURLINFO_REDIRECT_URL) : ""; + curl_close($curlHandle); + if ($statusCode==0) { + $statusCode = 450; + $this->yellow->page->error($statusCode, "Can't connect to the update server!"); + } + if ($statusCode!=450 && $statusCode!=200) { + $statusCode = 500; + $this->yellow->page->error($statusCode, "Can't download file '$url'!"); + } + if ($this->yellow->system->get("coreDebugMode")>=2 && !is_string_empty($redirectUrl)) { + echo "YellowUpdate::getExtensionFile redirected to url:$redirectUrl<br/>\n"; + } + if ($this->yellow->system->get("coreDebugMode")>=2) { + echo "YellowUpdate::getExtensionFile status:$statusCode url:$url<br/>\n"; + } + return array($statusCode, $fileData); + } + + // Return extension download URL, redirect to known URL if necessary + public function getExtensionDownloadUrl($url) { + if (preg_match("#^https://github.com/(.+)/archive/refs/heads/main.zip$#", $url, $matches)) { + $url = "https://codeload.github.com/".$matches[1]."/zip/refs/heads/main"; + } + if (preg_match("#^https://github.com/(.+)/raw/main/(.+)$#", $url, $matches)) { + $url = "https://raw.githubusercontent.com/".$matches[1]."/main/".$matches[2]; + } + return $url; + } + + // Return time of next daily update + public function getTimestampDaily() { + $timeOffset = 0; + foreach (str_split($this->yellow->system->get("sitename")) as $char) { + $timeOffset = ($timeOffset+ord($char)) % 60; + } + return mktime(0, 0, 0) + 60*60*24 + $timeOffset; + } +} diff --git a/yellow.php b/yellow.php @@ -1,7 +1,7 @@ <?php // Datenstrom Yellow, https://github.com/datenstrom/yellow -require("system/extensions/core.php"); +require("system/workers/core.php"); if (PHP_SAPI!="cli") { $yellow = new YellowCore();