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:
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> <a href="README.md">English</a> <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> <a href="README.md">English</a> <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> <a href="README.md">English</a> <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: 
- // 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('"', '"', $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('&', '&', $text);
- } else {
- // Ampersand-encoding based entirely on Nat Irons's Amputator
- // MT plugin: <http://bumppo.net/projects/amputator/>
- $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
- '&', $text);
- }
- // Encode remaining <'s
- $text = str_replace('<', '<', $text);
-
- return $text;
- }
-
- /**
- * Parse Markdown automatic links to anchor HTML tags
- * @param string $text
- * @return string
- */
- protected function doAutoLinks($text) {
- $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i',
- array($this, '_doAutoLinks_url_callback'), $text);
-
- // Email addresses: <address@domain.foo>
- $text = preg_replace_callback('{
- <
- (?:mailto:)?
- (
- (?:
- [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+
- |
- ".*?"
- )
- \@
- (?:
- [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+
- |
- \[[\d.a-fA-F:]+\] # IPv4 & IPv6
- )
- )
- >
- }xi',
- array($this, '_doAutoLinks_email_callback'), $text);
-
- return $text;
- }
-
- /**
- * Parse URL callback
- * @param array $matches
- * @return string
- */
- protected function _doAutoLinks_url_callback($matches) {
- $url = $this->encodeURLAttribute($matches[1], $text);
- $link = "<a href=\"$url\">$text</a>";
- return $this->hashPart($link);
- }
-
- /**
- * Parse email address callback
- * @param array $matches
- * @return string
- */
- protected function _doAutoLinks_email_callback($matches) {
- $addr = $matches[1];
- $url = $this->encodeURLAttribute("mailto:$addr", $text);
- $link = "<a href=\"$url\">$text</a>";
- return $this->hashPart($link);
- }
-
- /**
- * Input: some text to obfuscate, e.g. "mailto:foo@example.com"
- *
- * Output: the same text but with most characters encoded as either a
- * decimal or hex entity, in the hopes of foiling most address
- * harvesting spam bots. E.g.:
- *
- * mailto:foo
- * @example.co
- * m
- *
- * Note: the additional output $tail is assigned the same value as the
- * ouput, minus the number of characters specified by $head_length.
- *
- * Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
- * With some optimizations by Milian Wolff. Forced encoding of HTML
- * attribute special characters by Allan Odgaard.
- *
- * @param string $text
- * @param string $tail 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 = '↩︎';
-
- /**
- * 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: 
- // 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) . " $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: 
+ // 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('"', '"', $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('&', '&', $text);
+ } else {
+ // Ampersand-encoding based entirely on Nat Irons's Amputator
+ // MT plugin: <http://bumppo.net/projects/amputator/>
+ $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
+ '&', $text);
+ }
+ // Encode remaining <'s
+ $text = str_replace('<', '<', $text);
+
+ return $text;
+ }
+
+ /**
+ * Parse Markdown automatic links to anchor HTML tags
+ * @param string $text
+ * @return string
+ */
+ protected function doAutoLinks($text) {
+ $text = preg_replace_callback('{<((https?|ftp|dict|tel):[^\'">\s]+)>}i',
+ array($this, '_doAutoLinks_url_callback'), $text);
+
+ // Email addresses: <address@domain.foo>
+ $text = preg_replace_callback('{
+ <
+ (?:mailto:)?
+ (
+ (?:
+ [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+
+ |
+ ".*?"
+ )
+ \@
+ (?:
+ [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+
+ |
+ \[[\d.a-fA-F:]+\] # IPv4 & IPv6
+ )
+ )
+ >
+ }xi',
+ array($this, '_doAutoLinks_email_callback'), $text);
+
+ return $text;
+ }
+
+ /**
+ * Parse URL callback
+ * @param array $matches
+ * @return string
+ */
+ protected function _doAutoLinks_url_callback($matches) {
+ $url = $this->encodeURLAttribute($matches[1], $text);
+ $link = "<a href=\"$url\">$text</a>";
+ return $this->hashPart($link);
+ }
+
+ /**
+ * Parse email address callback
+ * @param array $matches
+ * @return string
+ */
+ protected function _doAutoLinks_email_callback($matches) {
+ $addr = $matches[1];
+ $url = $this->encodeURLAttribute("mailto:$addr", $text);
+ $link = "<a href=\"$url\">$text</a>";
+ return $this->hashPart($link);
+ }
+
+ /**
+ * Input: some text to obfuscate, e.g. "mailto:foo@example.com"
+ *
+ * Output: the same text but with most characters encoded as either a
+ * decimal or hex entity, in the hopes of foiling most address
+ * harvesting spam bots. E.g.:
+ *
+ * mailto:foo
+ * @example.co
+ * m
+ *
+ * Note: the additional output $tail is assigned the same value as the
+ * ouput, minus the number of characters specified by $head_length.
+ *
+ * Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
+ * With some optimizations by Milian Wolff. Forced encoding of HTML
+ * attribute special characters by Allan Odgaard.
+ *
+ * @param string $text
+ * @param string $tail 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 = '↩︎';
+
+ /**
+ * 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: 
+ // 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) . " $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();