mikuli.cz

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

core.php (181843B)


      1 <?php
      2 // Core extension, https://github.com/annaesvensson/yellow-core
      3 
      4 class YellowCore {
      5     const VERSION = "0.9.26";
      6     const RELEASE = "0.9";
      7     public $content;        // content files
      8     public $media;          // media files
      9     public $system;         // system settings
     10     public $language;       // language settings
     11     public $user;           // user settings
     12     public $extension;      // extensions
     13     public $lookup;         // lookup and normalisation methods
     14     public $toolbox;        // toolbox with helper methods
     15     public $page;           // current page
     16 
     17     public function __construct() {
     18         $this->content = new YellowContent($this);
     19         $this->media = new YellowMedia($this);
     20         $this->system = new YellowSystem($this);
     21         $this->language = new YellowLanguage($this);
     22         $this->user = new YellowUser($this);
     23         $this->extension = new YellowExtension($this);
     24         $this->lookup = new YellowLookup($this);
     25         $this->toolbox = new YellowToolbox($this);
     26         $this->page = new YellowPage($this);
     27         $this->checkRequirements();
     28         $this->system->setDefault("sitename", "Localhost");
     29         $this->system->setDefault("author", "Datenstrom");
     30         $this->system->setDefault("email", "webmaster");
     31         $this->system->setDefault("from", "noreply");
     32         $this->system->setDefault("language", "en");
     33         $this->system->setDefault("layout", "default");
     34         $this->system->setDefault("theme", "default");
     35         $this->system->setDefault("parser", "markdown");
     36         $this->system->setDefault("status", "public");
     37         $this->system->setDefault("coreServerUrl", "auto");
     38         $this->system->setDefault("coreTimezone", "UTC");
     39         $this->system->setDefault("coreContentExtension", ".md");
     40         $this->system->setDefault("coreContentDefaultFile", "page.md");
     41         $this->system->setDefault("coreContentErrorFile", "page-error-(.*).md");
     42         $this->system->setDefault("coreLanguageFile", "yellow-language.ini");
     43         $this->system->setDefault("coreUserFile", "yellow-user.ini");
     44         $this->system->setDefault("coreWebsiteFile", "yellow-website.log");
     45         $this->system->setDefault("coreAssetLocation", "/assets/");
     46         $this->system->setDefault("coreMediaLocation", "/media/");
     47         $this->system->setDefault("coreDownloadLocation", "/media/downloads/");
     48         $this->system->setDefault("coreImageLocation", "/media/images/");
     49         $this->system->setDefault("coreThumbnailLocation", "/media/thumbnails/");
     50         $this->system->setDefault("coreMultiLanguageMode", "0");
     51         $this->system->setDefault("coreDebugMode", "0");
     52     }
     53     
     54     public function __destruct() {
     55         $this->shutdown();
     56     }
     57     
     58     // Check requirements
     59     public function checkRequirements() {
     60         if (!version_compare(PHP_VERSION, "7.0", ">=")) $this->exitFatalError("Datenstrom Yellow requires PHP 7.0 or higher!");
     61         if (!extension_loaded("curl")) $this->exitFatalError("Datenstrom Yellow requires PHP curl extension!");
     62         if (!extension_loaded("gd")) $this->exitFatalError("Datenstrom Yellow requires PHP gd extension!");
     63         if (!extension_loaded("mbstring")) $this->exitFatalError("Datenstrom Yellow requires PHP mbstring extension!");
     64         if (!extension_loaded("zip")) $this->exitFatalError("Datenstrom Yellow requires PHP zip extension!");
     65         mb_internal_encoding("UTF-8");
     66     }
     67     
     68     // Handle initialisation
     69     public function load() {
     70         $this->system->load("system/extensions/yellow-system.ini");
     71         $this->system->set("coreSystemFile", "yellow-system.ini");
     72         $this->system->set("coreContentDirectory", "content/");
     73         $this->system->set("coreMediaDirectory", $this->lookup->findMediaDirectory("coreMediaLocation"));
     74         $this->system->set("coreSystemDirectory", "system/");
     75         $this->system->set("coreCacheDirectory", "system/cache/");
     76         $this->system->set("coreExtensionDirectory", "system/extensions/");
     77         $this->system->set("coreLayoutDirectory", "system/layouts/");
     78         $this->system->set("coreThemeDirectory", "system/themes/");
     79         $this->system->set("coreTrashDirectory", "system/trash/");
     80         $this->system->set("coreWorkerDirectory", "system/workers/");
     81         list($pathInstall, $pathRoot, $pathHome) = $this->lookup->findFileSystemInformation();
     82         $this->system->set("coreServerInstallDirectory", $pathInstall);
     83         $this->system->set("coreServerRootDirectory", $pathRoot);
     84         $this->system->set("coreServerHomeDirectory", $pathHome);
     85         register_shutdown_function(array($this, "processFatalError"));
     86         if ($this->system->get("coreDebugMode")>=1) {
     87             ini_set("display_errors", 1);
     88             error_reporting(E_ALL);
     89         }
     90         date_default_timezone_set($this->system->get("coreTimezone"));
     91         $this->extension->load($this->system->get("coreWorkerDirectory"));
     92         $this->language->load($this->system->get("coreExtensionDirectory").$this->system->get("coreLanguageFile"));
     93         $this->user->load($this->system->get("coreExtensionDirectory").$this->system->get("coreUserFile"));
     94         $this->startup();
     95     }
     96     
     97     // Handle request from web browser
     98     public function request() {
     99         $statusCode = 0;
    100         $this->toolbox->timerStart($time);
    101         ob_start();
    102         list($scheme, $address, $base, $location, $fileName) = $this->lookup->getRequestInformation();
    103         $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, true);
    104         foreach ($this->extension->data as $key=>$value) {
    105             if (method_exists($value["object"], "onRequest")) {
    106                 $this->lookup->requestHandler = $key;
    107                 $statusCode = $value["object"]->onRequest($scheme, $address, $base, $location, $fileName);
    108                 if ($statusCode!=0) break;
    109             }
    110         }
    111         if ($statusCode==0) {
    112             $this->lookup->requestHandler = "core";
    113             $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName, true);
    114         }
    115         if ($this->page->isError()) $statusCode = $this->processRequestError();
    116         ob_end_flush();
    117         $this->toolbox->timerStop($time);
    118         if ($this->system->get("coreDebugMode")>=1 && ($this->lookup->isContentFile($fileName) || $this->page->isError())) {
    119             echo "YellowCore::request status:$statusCode time:$time ms<br />\n";
    120         }
    121         return $statusCode;
    122     }
    123     
    124     // Process request
    125     public function processRequest($scheme, $address, $base, $location, $fileName, $cacheable) {
    126         $statusCode = 0;
    127         if (is_readable($fileName)) {
    128             if ($this->lookup->isRequestCleanUrl($location)) {
    129                 $location = $location.$this->toolbox->getLocationArgumentsCleanUrl();
    130                 $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location);
    131                 $statusCode = $this->sendStatus(303, $location);
    132             }
    133         } else {
    134             if ($this->lookup->isRedirectLocation($location)) {
    135                 $location = $this->lookup->getRedirectLocation($location);
    136                 $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location);
    137                 $statusCode = $this->sendStatus(301, $location);
    138             }
    139         }
    140         if ($statusCode==0) {
    141             if ($this->lookup->isContentFile($fileName)) {
    142                 $statusCode = $this->sendPage($scheme, $address, $base, $location, $fileName, $cacheable, true);
    143             } elseif (!is_string_empty($fileName)) {
    144                 $statusCode = $this->sendFile(200, $fileName, $cacheable);
    145             }
    146             if (!is_readable($fileName)) $this->page->error(404);
    147         }
    148         return $statusCode;
    149     }
    150     
    151     // Process request with error
    152     public function processRequestError() {
    153         ob_clean();
    154         $statusCode = $this->sendPage($this->page->scheme, $this->page->address, $this->page->base,
    155             $this->page->location, $this->page->fileName, false, false);
    156         return $statusCode;
    157     }
    158     
    159     // Process fatal runtime error
    160     public function processFatalError() {
    161         $error = error_get_last();
    162         if (!is_null($error) && isset($error["type"]) && ($error["type"]==E_ERROR || $error["type"]==E_PARSE)) {
    163             $fileNameAbsolute = isset($error["file"]) ? $error["file"] : "";
    164             $fileName = substru($fileNameAbsolute, strlenu($this->system->get("coreServerInstallDirectory")));
    165             $this->toolbox->log("error", "Process file '$fileName' with fatal error!");
    166             $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500));
    167             $troubleshooting = PHP_SAPI!="cli" ?
    168                 "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl();
    169             echo "<br />\nDatenstrom Yellow stopped with fatal error. Activate the debug mode for more information. $troubleshooting\n";
    170         }
    171     }
    172     
    173     // Show error message and terminate immediately
    174     public function exitFatalError($errorMessage = "") {
    175         $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted(500));
    176         $troubleshooting = PHP_SAPI!="cli" ?
    177             "<a href=\"".$this->toolbox->getTroubleshootingUrl()."\">See troubleshooting</a>." : "See ".$this->toolbox->getTroubleshootingUrl();
    178         echo "$errorMessage $troubleshooting\n";
    179         exit(1);
    180     }
    181     
    182     // Send page response
    183     public function sendPage($scheme, $address, $base, $location, $fileName, $cacheable, $regular) {
    184         $rawData = $regular ? $this->toolbox->readFile($fileName) : $this->page->getRawDataError();
    185         $statusCode = max($this->page->statusCode, 200);
    186         $errorMessage = $this->page->errorMessage;
    187         $this->page = new YellowPage($this);
    188         $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable);
    189         $this->page->parseMeta($rawData, $statusCode, $errorMessage);
    190         $this->language->set($this->page->get("language"));
    191         $this->page->parseContent();
    192         $this->page->parsePage();
    193         $statusCode = $this->sendData($this->page->statusCode, $this->page->headerData, $this->page->outputData);
    194         if ($this->system->get("coreDebugMode")>=1) {
    195             foreach ($this->page->headerData as $key=>$value) {
    196                 echo "YellowCore::sendPage $key: $value<br />\n";
    197             }
    198             $fileNameResponse = $regular ? $this->page->fileName : $this->page->getFileNameError();
    199             $layout = $this->page->get("layout");
    200             $theme = $this->page->get("theme");
    201             echo "YellowCore::sendPage file:$fileNameResponse<br />\n";
    202             echo "YellowCore::sendPage layout:$layout theme:$theme<br />\n";
    203         }
    204         return $statusCode;
    205     }
    206     
    207     // Send data response
    208     public function sendData($statusCode, $headerData, $outputData) {
    209         $lastModifiedFormatted = isset($headerData["Last-Modified"]) ? $headerData["Last-Modified"] : "";
    210         if ($statusCode==200 && !isset($headerData["Cache-Control"]) && $this->toolbox->isNotModified($lastModifiedFormatted)) {
    211             $statusCode = 304;
    212             $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
    213         } else {
    214             $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
    215             foreach ($headerData as $key=>$value) {
    216                 $this->toolbox->sendHttpHeader("$key: $value");
    217             }
    218             if (!is_null($outputData)) echo $outputData;
    219         }
    220         return $statusCode;
    221     }
    222     
    223     // Send file response
    224     public function sendFile($statusCode, $fileName, $cacheable) {
    225         $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted($this->toolbox->getFileModified($fileName));
    226         if ($statusCode==200 && $cacheable && $this->toolbox->isNotModified($lastModifiedFormatted)) {
    227             $statusCode = 304;
    228             $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
    229         } else {
    230             $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
    231             if (!$cacheable) $this->toolbox->sendHttpHeader("Cache-Control: no-cache, no-store");
    232             $this->toolbox->sendHttpHeader("Content-Type: ".$this->toolbox->getMimeContentType($fileName));
    233             $this->toolbox->sendHttpHeader("Last-Modified: ".$lastModifiedFormatted);
    234             echo $this->toolbox->readFile($fileName);
    235         }
    236         return $statusCode;
    237     }
    238     
    239     // Send status response
    240     public function sendStatus($statusCode, $location = "") {
    241         if (!is_string_empty($location)) $this->page->status($statusCode, $location);
    242         $this->toolbox->sendHttpHeader($this->toolbox->getHttpStatusFormatted($statusCode));
    243         foreach ($this->page->headerData as $key=>$value) {
    244             $this->toolbox->sendHttpHeader("$key: $value");
    245         }
    246         return $statusCode;
    247     }
    248     
    249     // Handle command from command line
    250     public function command($line = "") {
    251         $statusCode = 0;
    252         $this->toolbox->timerStart($time);
    253         list($command, $text) = $this->lookup->getCommandInformation($line);
    254         foreach ($this->extension->data as $key=>$value) {
    255             if (method_exists($value["object"], "onCommand")) {
    256                 $this->lookup->commandHandler = $key;
    257                 $statusCode = $value["object"]->onCommand($command, $text);
    258                 if ($statusCode!=0) break;
    259             }
    260         }
    261         if ($statusCode==0 && is_string_empty($command)) {
    262             $lines = array();
    263             foreach ($this->extension->data as $key=>$value) {
    264                 if (method_exists($value["object"], "onCommandHelp")) {
    265                     $this->lookup->commandHandler = $key;
    266                     $output = $value["object"]->onCommandHelp();
    267                     if (!is_null($output))  {
    268                         $lines = array_merge($lines, is_array($output) ? $output : array($output));
    269                     }
    270                 }
    271             }
    272             usort($lines, "strnatcasecmp");
    273             $this->showCommandHelp($lines);
    274             $statusCode = 200;
    275         }
    276         if ($statusCode==0) {
    277             $this->lookup->commandHandler = "core";
    278             $statusCode = 400;
    279             echo "Yellow $command: Command not found\n";
    280         }
    281         $this->toolbox->timerStop($time);
    282         if ($this->system->get("coreDebugMode")>=1) {
    283             echo "YellowCore::command status:$statusCode time:$time ms<br />\n";
    284         }
    285         return $statusCode<400 ? 0 : 1;
    286     }
    287     
    288     // Show command help
    289     public function showCommandHelp($lines) {
    290         echo "Datenstrom Yellow is for people who make small websites. https://datenstrom.se/yellow/\n";
    291         $lineCounter = 0;
    292         foreach ($lines as $line) {
    293             echo(++$lineCounter>1 ? "        " : "Syntax: ")."php yellow.php $line\n";
    294         }
    295     }
    296     
    297     // Handle startup
    298     public function startup() {
    299         if (isset($this->extension->data)) {
    300             foreach ($this->extension->data as $key=>$value) {
    301                 if (method_exists($value["object"], "onStartup")) $value["object"]->onStartup();
    302             }
    303         }
    304     }
    305     
    306     // Handle shutdown
    307     public function shutdown() {
    308         if (isset($this->extension->data)) {
    309             foreach ($this->extension->data as $key=>$value) {
    310                 if (method_exists($value["object"], "onShutdown")) $value["object"]->onShutdown();
    311             }
    312         }
    313     }
    314     
    315     // Include layout
    316     public function layout($name, $arguments = null) {
    317         $this->lookup->layoutArguments = func_get_args();
    318         $this->page->includeLayout($name);
    319     }
    320 
    321     // Return layout arguments
    322     public function getLayoutArguments($sizeMin = 9) {
    323         return array_pad($this->lookup->layoutArguments, $sizeMin, null);
    324     }
    325 }
    326 
    327 class YellowContent {
    328     public $yellow;         // access to API
    329     public $pages;          // scanned pages
    330     
    331     public function __construct($yellow) {
    332         $this->yellow = $yellow;
    333         $this->pages = array();
    334     }
    335     
    336     // Scan file system on demand
    337     public function scanLocation($location) {
    338         if (!isset($this->pages[$location])) {
    339             $this->pages[$location] = array();
    340             $scheme = $this->yellow->page->scheme;
    341             $address = $this->yellow->page->address;
    342             $base = $this->yellow->page->base;
    343             if (is_string_empty($location)) {
    344                 $rootLocations = $this->yellow->lookup->findContentRootLocations();
    345                 foreach ($rootLocations as $rootLocation=>$rootFileName) {
    346                     $page = new YellowPage($this->yellow);
    347                     $page->setRequestInformation($scheme, $address, $base, $rootLocation, $rootFileName, false);
    348                     $page->parseMeta("");
    349                     array_push($this->pages[$location], $page);
    350                 }
    351             } else {
    352                 if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowContent::scanLocation location:$location<br />\n";
    353                 $fileNames = $this->yellow->lookup->findChildrenFromContentLocation($location);
    354                 foreach ($fileNames as $fileName) {
    355                     $page = new YellowPage($this->yellow);
    356                     $page->setRequestInformation($scheme, $address, $base,
    357                         $this->yellow->lookup->findContentLocationFromFile($fileName), $fileName, false);
    358                     $page->parseMeta($this->yellow->toolbox->readFile($fileName, 4096));
    359                     if (strlenb($page->rawData)<4096) $page->statusCode = 200;
    360                     array_push($this->pages[$location], $page);
    361                 }
    362             }
    363         }
    364         return $this->pages[$location];
    365     }
    366 
    367     // Return page from, null if not found
    368     public function find($location, $absoluteLocation = false) {
    369         $found = false;
    370         if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base));
    371         foreach ($this->scanLocation($this->getParentLocation($location)) as $page) {
    372             if ($page->location==$location) {
    373                 $found = true;
    374                 break;
    375             }
    376         }
    377         return $found ? $page : null;
    378     }
    379     
    380     // Return page collection with pages of the website
    381     public function index($showInvisible = false) {
    382         $rootLocation = $this->getRootLocation($this->yellow->page->location);
    383         return $this->getChildrenRecursive($rootLocation, $showInvisible);
    384     }
    385     
    386     // Return page collection with top-level navigation
    387     public function top($showInvisible = false) {
    388         $rootLocation = $this->getRootLocation($this->yellow->page->location);
    389         return $this->getChildren($rootLocation, $showInvisible);
    390     }
    391     
    392     // Return page collection with path ancestry
    393     public function path($location, $absoluteLocation = false) {
    394         $pages = new YellowPageCollection($this->yellow);
    395         if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base));
    396         $page = null;
    397         while (!$this->yellow->lookup->isRootLocation($location)) {
    398             $page = $this->find($location);
    399             if ($page) $pages->prepend($page);
    400             $location = $this->getParentLocation($location);
    401         }
    402         if ($page) {
    403             $home = $this->find($this->getHomeLocation($page->location));
    404             if ($home && $home->location!=$page->location) $pages->prepend($home);
    405         }
    406         return $pages;
    407     }
    408     
    409     // Return page collection with multiple languages
    410     public function multi($location, $absoluteLocation = false, $showInvisible = false) {
    411         $pages = new YellowPageCollection($this->yellow);
    412         if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base));
    413         $locationEnd = substru($location, strlenu($this->getRootLocation($location)) - 4);
    414         foreach ($this->scanLocation("") as $page) {
    415             if ($content = $this->find(substru($page->location, 4).$locationEnd)) {
    416                 if ($content->isAvailable() && ($content->isVisible() || $showInvisible)) {
    417                     $pages->append($content);
    418                 }
    419             }
    420         }
    421         return $pages;
    422     }
    423     
    424     // Return page collection that's empty
    425     public function clean() {
    426         return new YellowPageCollection($this->yellow);
    427     }
    428     
    429     // Return languages in multi language mode
    430     public function getLanguages($showInvisible = false) {
    431         $languages = array();
    432         if ($this->yellow->system->get("coreMultiLanguageMode")) {
    433             foreach ($this->scanLocation("") as $page) {
    434                 if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) {
    435                     array_push($languages, $page->get("language"));
    436                 }
    437             }
    438         }
    439         return $languages;
    440     }
    441     
    442     // Return child pages
    443     public function getChildren($location, $showInvisible = false) {
    444         $pages = new YellowPageCollection($this->yellow);
    445         foreach ($this->scanLocation($location) as $page) {
    446             if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) {
    447                 $pages->append($page);
    448             }
    449         }
    450         return $pages;
    451     }
    452     
    453     // Return child pages recursively
    454     public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) {
    455         --$levelMax;
    456         $pages = new YellowPageCollection($this->yellow);
    457         foreach ($this->scanLocation($location) as $page) {
    458             if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) {
    459                 $pages->append($page);
    460             }
    461             if (!$this->yellow->lookup->isFileLocation($page->location) && $levelMax!=0) {
    462                 $pages->merge($this->getChildrenRecursive($page->location, $showInvisible, $levelMax));
    463             }
    464         }
    465         return $pages;
    466     }
    467     
    468     // Return shared pages
    469     public function getShared($location) {
    470         $pages = new YellowPageCollection($this->yellow);
    471         $sharedLocation = $this->getHomeLocation($location)."shared/";
    472         return $pages->merge($this->scanLocation($sharedLocation));
    473     }
    474     
    475     // Return root location
    476     public function getRootLocation($location) {
    477         $rootLocation = "root/";
    478         if ($this->yellow->system->get("coreMultiLanguageMode")) {
    479             foreach ($this->scanLocation("") as $page) {
    480                 $token = substru($page->location, 4);
    481                 if ($token!="/" && substru($location, 0, strlenu($token))==$token) {
    482                     $rootLocation = "root$token";
    483                     break;
    484                 }
    485             }
    486         }
    487         return $rootLocation;
    488     }
    489 
    490     // Return home location
    491     public function getHomeLocation($location) {
    492         return substru($this->getRootLocation($location), 4);
    493     }
    494     
    495     // Return parent location
    496     public function getParentLocation($location) {
    497         $parentLocation = "";
    498         $token = rtrim(substru($this->getRootLocation($location), 4), "/");
    499         if (preg_match("#^($token.*\/).+?$#", $location, $matches)) {
    500             if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1];
    501         }
    502         if (is_string_empty($parentLocation)) $parentLocation = "root$token/";
    503         return $parentLocation;
    504     }
    505     
    506     // Return top-level location
    507     public function getParentTopLocation($location) {
    508         $parentTopLocation = "";
    509         $token = rtrim(substru($this->getRootLocation($location), 4), "/");
    510         if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1];
    511         if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/";
    512         return $parentTopLocation;
    513     }
    514 }
    515     
    516 class YellowMedia {
    517     public $yellow;     // access to API
    518     public $files;      // scanned files
    519     
    520     public function __construct($yellow) {
    521         $this->yellow = $yellow;
    522         $this->files = array();
    523     }
    524 
    525     // Scan file system on demand
    526     public function scanLocation($location) {
    527         if (!isset($this->files[$location])) {
    528             $this->files[$location] = array();
    529             $scheme = $this->yellow->page->scheme;
    530             $address = $this->yellow->page->address;
    531             $base = $this->yellow->system->get("coreServerBase");
    532             if (is_string_empty($location)) {
    533                 $fileNames = array($this->yellow->system->get("coreMediaDirectory"));
    534             } else {
    535                 if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowMedia::scanLocation location:$location<br />\n";
    536                 $fileNames = $this->yellow->lookup->findChildrenFromMediaLocation($location);
    537             }
    538             foreach ($fileNames as $fileName) {
    539                 $file = new YellowPage($this->yellow);
    540                 $file->setRequestInformation($scheme, $address, $base,
    541                     $this->yellow->lookup->findMediaLocationFromFile($fileName), $fileName, false);
    542                 $file->parseMeta(null);
    543                 array_push($this->files[$location], $file);
    544             }
    545         }
    546         return $this->files[$location];
    547     }
    548     
    549     // Return page with media file information, null if not found
    550     public function find($location, $absoluteLocation = false) {
    551         $found = false;
    552         if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->system->get("coreServerBase")));
    553         foreach ($this->scanLocation($this->getParentLocation($location)) as $file) {
    554             if ($file->location==$location) {
    555                 $found = true;
    556                 break;
    557             }
    558         }
    559         return $found ? $file : null;
    560     }
    561     
    562     // Return page collection with media files
    563     public function index($showInvisible = false) {
    564         return $this->getChildrenRecursive("", $showInvisible);
    565     }
    566     
    567     // Return page collection that's empty
    568     public function clean() {
    569         return new YellowPageCollection($this->yellow);
    570     }
    571     
    572     // Return child files
    573     public function getChildren($location, $showInvisible = false) {
    574         $files = new YellowPageCollection($this->yellow);
    575         foreach ($this->scanLocation($location) as $file) {
    576             if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) {
    577                 $files->append($file);
    578             }
    579         }
    580         return $files;
    581     }
    582     
    583     // Return child files recursively
    584     public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) {
    585         --$levelMax;
    586         $files = new YellowPageCollection($this->yellow);
    587         foreach ($this->scanLocation($location) as $file) {
    588             if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) {
    589                 $files->append($file);
    590             }
    591             if (!$this->yellow->lookup->isFileLocation($file->location) && $levelMax!=0) {
    592                 $files->merge($this->getChildrenRecursive($file->location, $showInvisible, $levelMax));
    593             }
    594         }
    595         return $files;
    596     }
    597     
    598     // Return home location
    599     public function getHomeLocation($location) {
    600         return $this->yellow->system->get("coreMediaLocation");
    601     }
    602 
    603     // Return parent location
    604     public function getParentLocation($location) {
    605         $parentLocation = "";
    606         $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/");
    607         if (preg_match("#^($token.*\/).+?$#", $location, $matches)) {
    608             if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1];
    609         }
    610         return $parentLocation;
    611     }
    612     
    613     // Return top-level location
    614     public function getParentTopLocation($location) {
    615         $parentTopLocation = "";
    616         $token = rtrim($this->yellow->system->get("coreMediaLocation"), "/");
    617         if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1];
    618         if (is_string_empty($parentTopLocation)) $parentTopLocation = "$token/";
    619         return $parentTopLocation;
    620     }
    621 }
    622 
    623 class YellowSystem {
    624     public $yellow;             // access to API
    625     public $modified;           // system modification date
    626     public $settings;           // system settings
    627     public $settingsDefaults;   // system settings defaults
    628     
    629     public function __construct($yellow) {
    630         $this->yellow = $yellow;
    631         $this->modified = 0;
    632         $this->settings = new YellowArray();
    633         $this->settingsDefaults = new YellowArray();
    634     }
    635     
    636     // Load system settings from file
    637     public function load($fileName) {
    638         $this->modified = $this->yellow->toolbox->getFileModified($fileName);
    639         $fileData = $this->yellow->toolbox->readFile($fileName);
    640         $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "");
    641         if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowSystem::load file:$fileName<br />\n";
    642         if ($this->yellow->system->get("coreDebugMode")>=3) {
    643             foreach ($this->settings as $key=>$value) {
    644                 echo "YellowSystem::load ".ucfirst($key).":$value<br />\n";
    645             }
    646         }
    647     }
    648     
    649     // Save system settings to file
    650     public function save($fileName, $settings) {
    651         $this->modified = time();
    652         $settingsNew = new YellowArray();
    653         foreach ($settings as $key=>$value) {
    654             if (!is_string_empty($key) && !is_string_empty($value)) {
    655                 $this->set($key, $value);
    656                 $settingsNew[$key] = $value;
    657             }
    658         }
    659         $fileData = $this->yellow->toolbox->readFile($fileName);
    660         $fileData = $this->yellow->toolbox->setTextSettings($fileData, "", "", $settingsNew);
    661         return $this->yellow->toolbox->writeFile($fileName, $fileData);
    662     }
    663     
    664     // Set default system setting
    665     public function setDefault($key, $value) {
    666         $this->settingsDefaults[$key] = $value;
    667     }
    668 
    669     // Set default system settings
    670     public function setDefaults($lines) {
    671         foreach ($lines as $line) {
    672             if (preg_match("/^\#/", $line)) continue;
    673             if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
    674                 if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
    675                     $this->settingsDefaults[$matches[1]] = $matches[2];
    676                 }
    677             }
    678         }
    679     }
    680     
    681     // Set system setting
    682     public function set($key, $value) {
    683         $this->settings[$key] = $value;
    684     }
    685     
    686     // Return system setting
    687     public function get($key) {
    688         if (isset($this->settings[$key])) {
    689             $value = $this->settings[$key];
    690         } else {
    691             $value = isset($this->settingsDefaults[$key]) ? $this->settingsDefaults[$key] : "";
    692         }
    693         return $value;
    694     }
    695     
    696     // Return system setting, HTML encoded
    697     public function getHtml($key) {
    698         return htmlspecialchars($this->get($key));
    699     }
    700     
    701     // Return different value for system setting
    702     public function getDifferent($key) {
    703         $array = array_diff($this->yellow->toolbox->enumerate($key), array($this->get($key)));
    704         return reset($array);
    705     }
    706 
    707     // TODO: Remove later, this is only for backwards compatibility
    708     public function getAvailable($key) { return $this->yellow->toolbox->enumerate($key); }
    709     
    710     // Return system settings
    711     public function getSettings($filterStart = "", $filterEnd = "") {
    712         $settings = array();
    713         if (is_string_empty($filterStart) && is_string_empty($filterEnd)) {
    714             $settings = array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy());
    715         } else {
    716             foreach (array_merge($this->settingsDefaults->getArrayCopy(), $this->settings->getArrayCopy()) as $key=>$value) {
    717                 if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value;
    718                 if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value;
    719             }
    720         }
    721         return $settings;
    722     }
    723     
    724     // Return system settings modification date, Unix time or HTTP format
    725     public function getModified($httpFormat = false) {
    726         return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
    727     }
    728     
    729     // Check if system setting exists
    730     public function isExisting($key) {
    731         return isset($this->settings[$key]);
    732     }
    733 }
    734 
    735 class YellowLanguage {
    736     public $yellow;             // access to API
    737     public $modified;           // language modification date
    738     public $settings;           // language settings
    739     public $settingsDefaults;   // language settings defaults
    740     public $language;           // current language
    741     
    742     public function __construct($yellow) {
    743         $this->yellow = $yellow;
    744         $this->modified = 0;
    745         $this->settings = new YellowArray();
    746         $this->settingsDefaults = new YellowArray();
    747         $this->language = "";
    748     }
    749     
    750     // Load language settings from file
    751     public function load($fileName) {
    752         $this->modified = $this->yellow->toolbox->getFileModified($fileName);
    753         $fileData = $this->yellow->toolbox->readFile($fileName);
    754         $settings = $this->yellow->toolbox->getTextSettings($fileData, "language");
    755         foreach ($settings as $language=>$block) {
    756             if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray();
    757             foreach ($block as $key=>$value) {
    758                 $this->settings[$language][$key] = $value;
    759             }
    760         }
    761         if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowLanguage::load file:$fileName<br />\n";
    762         foreach ($this->settings->getArrayCopy() as $key=>$value) {
    763             if (!isset($this->settings[$key]["languageDescription"])) {
    764                 unset($this->settings[$key]);
    765             }
    766         }
    767         $callback = function ($a, $b) {
    768             return strnatcmp($a["languageDescription"], $b["languageDescription"]);
    769         };
    770         $this->settings->uasort($callback);
    771     }
    772     
    773     // Set current language
    774     public function set($language) {
    775         $this->language = $language;
    776     }
    777     
    778     // Set default language setting
    779     public function setDefault($key, $value, $language) {
    780         if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray();
    781         $this->settings[$language][$key] = $value;
    782         $this->settingsDefaults[$key] = true;
    783     }
    784     
    785     // Set default language settings
    786     public function setDefaults($lines) {
    787         $language = "";
    788         foreach ($lines as $line) {
    789             if (preg_match("/^\#/", $line)) continue;
    790             if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
    791                 if (lcfirst($matches[1])=="language" && !is_string_empty($matches[2])) {
    792                     $language = $matches[2];
    793                     if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray();
    794                 }
    795                 if (!is_string_empty($language) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
    796                     $this->settings[$language][$matches[1]] = $matches[2];
    797                     $this->settingsDefaults[$matches[1]] = true;
    798                 }
    799             }
    800         }
    801     }
    802     
    803     // Set language setting
    804     public function setText($key, $value, $language) {
    805         if (!isset($this->settings[$language])) $this->settings[$language] = new YellowArray();
    806         $this->settings[$language][$key] = $value;
    807     }
    808     
    809     // Return language setting
    810     public function getText($key, $language = "") {
    811         if (is_string_empty($language)) $language = $this->language;
    812         return $this->isText($key, $language) ? $this->settings[$language][$key] : "[$key]";
    813     }
    814     
    815     // Return language setting, HTML encoded
    816     public function getTextHtml($key, $language = "") {
    817         return htmlspecialchars($this->getText($key, $language));
    818     }
    819     
    820     // Return text as language specific date, convert to one of the standard formats
    821     public function getDateStandard($text, $language = "") {
    822         if (preg_match("/^\d+$/", $text)) {
    823             $output = $text;
    824         } elseif (preg_match("/^\d+\-\d+$/", $text)) {
    825             $format = $this->getText("coreDateFormatShort", $language);
    826             $output = $this->getDateFormatted(strtotime($text), $format, $language);
    827         } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) {
    828             $format = $this->getText("coreDateFormatMedium", $language);
    829             $output = $this->getDateFormatted(strtotime($text), $format, $language);
    830         } else {
    831             $format = $this->getText("coreDateFormatLong", $language);
    832             $output = $this->getDateFormatted(strtotime($text), $format, $language);
    833         }
    834         return $output;
    835     }
    836     
    837     // Return Unix time as date, relative to today
    838     public function getDateRelative($timestamp, $format, $daysLimit, $language = "") {
    839         $timeDifference = mktime(0, 0, 0) - strtotime(date("Y-m-d", $timestamp));
    840         $days = abs(intval($timeDifference/86400));
    841         $key = $timeDifference>=0 ? "coreDatePast" : "coreDateFuture";
    842         $tokens = preg_split("/\s*,\s*/", $this->getText($key, $language));
    843         if (count($tokens)>=8) {
    844             if ($days<=$daysLimit || $daysLimit==0) {
    845                 if ($days==0) {
    846                     $output = $tokens[0];
    847                 } elseif ($days==1) {
    848                     $output = $tokens[1];
    849                 } elseif ($days>=2 && $days<=29) {
    850                     $output = preg_replace("/@x/i", $days, $tokens[2]);
    851                 } elseif ($days>=30 && $days<=59) {
    852                     $output = $tokens[3];
    853                 } elseif ($days>=60 && $days<=364) {
    854                     $output = preg_replace("/@x/i", intval($days/30), $tokens[4]);
    855                 } elseif ($days>=365 && $days<=729) {
    856                     $output = $tokens[5];
    857                 } else {
    858                     $output = preg_replace("/@x/i", intval($days/365.25), $tokens[6]);
    859                 }
    860             } else {
    861                 $output = preg_replace("/@x/i", $this->getDateFormatted($timestamp, $format, $language), $tokens[7]);
    862             }
    863         } else {
    864             $output = "[$key]";
    865         }
    866         return $output;
    867     }
    868     
    869     // Return Unix time as date
    870     public function getDateFormatted($timestamp, $format, $language = "") {
    871         $dateMonthsNominative = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsNominative", $language));
    872         $dateMonthsGenitive = preg_split("/\s*,\s*/", $this->getText("coreDateMonthsGenitive", $language));
    873         $dateWeekdays = preg_split("/\s*,\s*/", $this->getText("coreDateWeekdays", $language));
    874         $monthNominative = $dateMonthsNominative[date("n", $timestamp) - 1];
    875         $monthGenitive = $dateMonthsGenitive[date("n", $timestamp) - 1];
    876         $weekday = $dateWeekdays[date("N", $timestamp) - 1];
    877         $timeZone = $this->yellow->system->get("coreTimezone");
    878         $timeZoneHelper = new DateTime("now", new DateTimeZone($timeZone));
    879         $timeZoneOffset = $timeZoneHelper->getOffset();
    880         $timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600));
    881         $format = preg_replace("/(?<!\\\)F/", addcslashes($monthNominative, "A..Za..z"), $format);
    882         $format = preg_replace("/(?<!\\\)V/", addcslashes($monthGenitive, "A..Za..z"), $format);
    883         $format = preg_replace("/(?<!\\\)M/", addcslashes(substru($monthNominative, 0, 3), "A..Za..z"), $format);
    884         $format = preg_replace("/(?<!\\\)D/", addcslashes(substru($weekday, 0, 3), "A..Za..z"), $format);
    885         $format = preg_replace("/(?<!\\\)l/", addcslashes($weekday, "A..Za..z"), $format);
    886         $format = preg_replace("/(?<!\\\)T/", addcslashes($timeZoneAbbreviation, "A..Za..z"), $format);
    887         return date($format, $timestamp);
    888     }
    889     
    890     // Return language settings
    891     public function getSettings($filterStart = "", $filterEnd = "", $language = "") {
    892         $settings = array();
    893         if (is_string_empty($language)) $language = $this->language;
    894         if (isset($this->settings[$language])) {
    895             if (is_string_empty($filterStart) && is_string_empty($filterEnd)) {
    896                 $settings = $this->settings[$language]->getArrayCopy();
    897             } else {
    898                 foreach ($this->settings[$language] as $key=>$value) {
    899                     if (!is_string_empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $settings[$key] = $value;
    900                     if (!is_string_empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $settings[$key] = $value;
    901                 }
    902             }
    903         }
    904         return $settings;
    905     }
    906     
    907     // Return language settings modification date, Unix time or HTTP format
    908     public function getModified($httpFormat = false) {
    909         return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
    910     }
    911     
    912     // Check if language setting exists
    913     public function isText($key, $language = "") {
    914         if (is_string_empty($language)) $language = $this->language;
    915         return isset($this->settings[$language]) && isset($this->settings[$language][$key]);
    916     }
    917 
    918     // Check if language exists
    919     public function isExisting($language) {
    920         return isset($this->settings[$language]);
    921     }
    922 }
    923 
    924 class YellowUser {
    925     public $yellow;         // access to API
    926     public $modified;       // user modification date
    927     public $settings;       // user settings
    928     public $email;          // current email
    929     
    930     public function __construct($yellow) {
    931         $this->yellow = $yellow;
    932         $this->modified = 0;
    933         $this->settings = new YellowArray();
    934         $this->email = "";
    935     }
    936 
    937     // Load user settings from file
    938     public function load($fileName) {
    939         $this->modified = $this->yellow->toolbox->getFileModified($fileName);
    940         $fileData = $this->yellow->toolbox->readFile($fileName);
    941         $this->settings = $this->yellow->toolbox->getTextSettings($fileData, "email");
    942         if ($this->yellow->system->get("coreDebugMode")>=2) echo "YellowUser::load file:$fileName<br />\n";
    943     }
    944 
    945     // Save user settings to file
    946     public function save($fileName, $email, $settings) {
    947         $this->modified = time();
    948         $settingsNew = new YellowArray();
    949         $settingsNew["email"] = $email;
    950         foreach ($settings as $key=>$value) {
    951             if (!is_string_empty($key) && !is_string_empty($value)) {
    952                 $this->setUser($key, $value, $email);
    953                 $settingsNew[$key] = $value;
    954             }
    955         }
    956         $fileData = $this->yellow->toolbox->readFile($fileName);
    957         $fileData = $this->yellow->toolbox->setTextSettings($fileData, "email", $email, $settingsNew);
    958         return $this->yellow->toolbox->writeFile($fileName, $fileData);
    959     }
    960     
    961     // Remove user settings from file
    962     public function remove($fileName, $email) {
    963         $this->modified = time();
    964         if (isset($this->settings[$email])) unset($this->settings[$email]);
    965         $fileData = $this->yellow->toolbox->readFile($fileName);
    966         $fileData = $this->yellow->toolbox->unsetTextSettings($fileData, "email", $email);
    967         return $this->yellow->toolbox->writeFile($fileName, $fileData);
    968     }
    969     
    970     // Set current email
    971     public function set($email) {
    972         $this->email = $email;
    973     }
    974     
    975     // Set user setting
    976     public function setUser($key, $value, $email) {
    977         if (!isset($this->settings[$email])) $this->settings[$email] = new YellowArray();
    978         $this->settings[$email][$key] = $value;
    979     }
    980     
    981     // Return user setting
    982     public function getUser($key, $email = "") {
    983         if (is_string_empty($email)) $email = $this->email;
    984         return isset($this->settings[$email]) && isset($this->settings[$email][$key]) ? $this->settings[$email][$key] : "";
    985     }
    986 
    987     // Return user setting, HTML encoded
    988     public function getUserHtml($key, $email = "") {
    989         return htmlspecialchars($this->getUser($key, $email));
    990     }
    991 
    992     // Return user settings
    993     public function getSettings($email = "") {
    994         $settings = array();
    995         if (is_string_empty($email)) $email = $this->email;
    996         if (isset($this->settings[$email])) $settings = $this->settings[$email]->getArrayCopy();
    997         return $settings;
    998     }
    999     
   1000     // Return user settings modification date, Unix time or HTTP format
   1001     public function getModified($httpFormat = false) {
   1002         return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
   1003     }
   1004     
   1005     // Check if user setting exists
   1006     public function isUser($key, $email = "") {
   1007         if (is_string_empty($email)) $email = $this->email;
   1008         return isset($this->settings[$email]) && isset($this->settings[$email][$key]);
   1009     }
   1010     
   1011     // Check if user exists
   1012     public function isExisting($email) {
   1013         return isset($this->settings[$email]);
   1014     }
   1015 }
   1016     
   1017 class YellowExtension {
   1018     public $yellow;     // access to API
   1019     public $modified;   // extension modification date
   1020     public $data;       // extension data
   1021 
   1022     public function __construct($yellow) {
   1023         $this->yellow = $yellow;
   1024         $this->modified = 0;
   1025         $this->data = array();
   1026     }
   1027     
   1028     // Load extensions
   1029     public function load($path) {
   1030         foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) {
   1031             $this->modified = max($this->modified, $this->yellow->toolbox->getFileModified($entry));
   1032             require_once($entry);
   1033             $name = $this->yellow->lookup->normaliseName(basename($entry), true, true);
   1034             $this->register(lcfirst($name), "Yellow".ucfirst($name));
   1035             if ($this->yellow->system->get("coreDebugMode")>=3) echo "YellowExtension::load file:$entry<br />\n";
   1036         }
   1037         $callback = function ($a, $b) {
   1038             return $a["priority"] - $b["priority"];
   1039         };
   1040         uasort($this->data, $callback);
   1041         foreach ($this->data as $key=>$value) {
   1042             if (method_exists($this->data[$key]["object"], "onLoad")) $this->data[$key]["object"]->onLoad($this->yellow);
   1043         }
   1044     }
   1045     
   1046     // Register extension
   1047     public function register($key, $class) {
   1048         if (!$this->isExisting($key) && class_exists($class)) {
   1049             $this->data[$key] = array();
   1050             $this->data[$key]["object"] = $class=="YellowCore" ? new stdClass : new $class;
   1051             $this->data[$key]["class"] = $class;
   1052             $this->data[$key]["version"] = defined("$class::VERSION") ? $class::VERSION : 0;
   1053             $this->data[$key]["priority"] = defined("$class::PRIORITY") ? $class::PRIORITY : count($this->data) + 10;
   1054         }
   1055     }
   1056     
   1057     // Return extension
   1058     public function get($key) {
   1059         return $this->data[$key]["object"];
   1060     }
   1061     
   1062     // Return extensions modification date, Unix time or HTTP format
   1063     public function getModified($httpFormat = false) {
   1064         return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
   1065     }
   1066     
   1067     // Check if extension exists
   1068     public function isExisting($key) {
   1069         return isset($this->data[$key]);
   1070     }
   1071 }
   1072 
   1073 class YellowLookup {
   1074     public $yellow;             // access to API
   1075     public $requestHandler;     // request handler name
   1076     public $commandHandler;     // command handler name
   1077     public $layoutArguments;    // layout arguments
   1078     
   1079     public function __construct($yellow) {
   1080         $this->yellow = $yellow;
   1081     }
   1082     
   1083     // Return file system information
   1084     public function findFileSystemInformation() {
   1085         $pathInstall = substru(__DIR__, 0, 1-strlenu($this->yellow->system->get("coreWorkerDirectory")));
   1086         $pathBase = $this->yellow->system->get("coreContentDirectory");
   1087         $pathRoot = $this->yellow->system->get("coreMultiLanguageMode") ? "default/" : "";
   1088         $pathHome = "home/";
   1089         if (!is_string_empty($pathRoot)) {
   1090             $firstRoot = "";
   1091             $token = $root = rtrim($pathRoot, "/");
   1092             foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) {
   1093                 if (is_string_empty($firstRoot)) $firstRoot = $token = $entry;
   1094                 if ($this->normaliseToken($entry)==$root) {
   1095                     $token = $entry;
   1096                     break;
   1097                 }
   1098             }
   1099             $pathRoot = $this->normaliseToken($token)."/";
   1100             $pathBase .= "$firstRoot/";
   1101         }
   1102         if (!is_string_empty($pathHome)) {
   1103             $firstHome = "";
   1104             $token = $home = rtrim($pathHome, "/");
   1105             foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) {
   1106                 if (is_string_empty($firstHome)) $firstHome = $token = $entry;
   1107                 if ($this->normaliseToken($entry)==$home) {
   1108                     $token = $entry;
   1109                     break;
   1110                 }
   1111             }
   1112             $pathHome = $this->normaliseToken($token)."/";
   1113         }
   1114         return array($pathInstall, $pathRoot, $pathHome);
   1115     }
   1116     
   1117     // Return content language
   1118     public function findContentLanguage($fileName, $languageDefault) {
   1119         $language = $languageDefault;
   1120         $pathBase = $this->yellow->system->get("coreContentDirectory");
   1121         $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
   1122         if (!is_string_empty($pathRoot)) {
   1123             $fileName = substru($fileName, strlenu($pathBase));
   1124             if (preg_match("/^(.+?)\//", $fileName, $matches)) {
   1125                 $name = $this->normaliseToken($matches[1]);
   1126                 if (strlenu($name)==2) $language = $name;
   1127             }
   1128         }
   1129         return $language;
   1130     }
   1131 
   1132     // Return content root locations
   1133     public function findContentRootLocations() {
   1134         $rootLocations = array();
   1135         $pathBase = $this->yellow->system->get("coreContentDirectory");
   1136         $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
   1137         if (!is_string_empty($pathRoot)) {
   1138             foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) {
   1139                 $token = $this->normaliseToken($entry)."/";
   1140                 if ($token==$pathRoot) $token = "";
   1141                 $rootLocations["root/$token"] = "$pathBase$entry/";
   1142             }
   1143         } else {
   1144             $rootLocations["root/"] = "$pathBase";
   1145         }
   1146         if ($this->yellow->system->get("coreDebugMode")>=3) {
   1147             foreach ($rootLocations as $key=>$value) {
   1148                 echo "YellowLookup::findContentRootLocations $key -> $value<br />\n";
   1149             }
   1150         }
   1151         return $rootLocations;
   1152     }
   1153     
   1154     // Return content location from file path
   1155     public function findContentLocationFromFile($fileName) {
   1156         $invalid = false;
   1157         $location = "/";
   1158         $pathBase = $this->yellow->system->get("coreContentDirectory");
   1159         $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
   1160         $pathHome = $this->yellow->system->get("coreServerHomeDirectory");
   1161         $fileDefault = $this->yellow->system->get("coreContentDefaultFile");
   1162         $fileExtension = $this->yellow->system->get("coreContentExtension");
   1163         if (substru($fileName, 0, strlenu($pathBase))==$pathBase && mb_check_encoding($fileName, "UTF-8")) {
   1164             $fileName = substru($fileName, strlenu($pathBase));
   1165             $tokens = explode("/", $fileName);
   1166             if (!is_string_empty($pathRoot)) {
   1167                 $token = $this->normaliseToken($tokens[0])."/";
   1168                 if ($token!=$pathRoot) $location .= $token;
   1169                 array_shift($tokens);
   1170             }
   1171             for ($i=0; $i<count($tokens)-1; ++$i) {
   1172                 $token = $this->normaliseToken($tokens[$i])."/";
   1173                 if ($i || $token!=$pathHome) $location .= $token;
   1174             }
   1175             $token = $this->normaliseToken($tokens[$i], $fileExtension);
   1176             if ($token!=$fileDefault) {
   1177                 $location .= $this->normaliseToken($tokens[$i], $fileExtension, true);
   1178             }
   1179             $extension = ($pos = strrposu($fileName, ".")) ? substru($fileName, $pos) : "";
   1180             if ($extension!=$fileExtension) $invalid = true;
   1181         } else {
   1182             $invalid = true;
   1183         }
   1184         if ($this->yellow->system->get("coreDebugMode")>=2) {
   1185             $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName";
   1186             echo "YellowLookup::findContentLocationFromFile $debug<br />\n";
   1187         }
   1188         return $invalid ? "" : $location;
   1189     }
   1190     
   1191     // Return file path from content location
   1192     public function findFileFromContentLocation($location, $directory = false) {
   1193         $found = $invalid = false;
   1194         $path = $this->yellow->system->get("coreContentDirectory");
   1195         $pathRoot = $this->yellow->system->get("coreServerRootDirectory");
   1196         $pathHome = $this->yellow->system->get("coreServerHomeDirectory");
   1197         $fileDefault = $this->yellow->system->get("coreContentDefaultFile");
   1198         $fileExtension = $this->yellow->system->get("coreContentExtension");
   1199         $tokens = explode("/", $location);
   1200         if ($this->isRootLocation($location)) {
   1201             if (!is_string_empty($pathRoot)) {
   1202                 $token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, "/");
   1203                 $path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid);
   1204             }
   1205         } else {
   1206             if (!is_string_empty($pathRoot)) {
   1207                 if (count($tokens)>2) {
   1208                     if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathRoot, "/"))) $invalid = true;
   1209                     $path .= $this->findFileDirectory($path, $tokens[1], "", true, false, $found, $invalid);
   1210                     if ($found) array_shift($tokens);
   1211                 }
   1212                 if (!$found) {
   1213                     $path .= $this->findFileDirectory($path, rtrim($pathRoot, "/"), "", true, true, $found, $invalid);
   1214                 }
   1215             }
   1216             if (count($tokens)>2) {
   1217                 if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathHome, "/"))) $invalid = true;
   1218                 for ($i=1; $i<count($tokens)-1; ++$i) {
   1219                     $path .= $this->findFileDirectory($path, $tokens[$i], "", true, true, $found, $invalid);
   1220                 }
   1221             } else {
   1222                 $i = 1;
   1223                 $tokens[0] = rtrim($pathHome, "/");
   1224                 $path .= $this->findFileDirectory($path, $tokens[0], "", true, true, $found, $invalid);
   1225             }
   1226             if (!$directory) {
   1227                 if (!is_string_empty($tokens[$i])) {
   1228                     $token = $tokens[$i].$fileExtension;
   1229                     if ($token==$fileDefault) $invalid = true;
   1230                     $path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid);
   1231                 } else {
   1232                     $path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false);
   1233                 }
   1234                 if ($this->yellow->system->get("coreDebugMode")>=2) {
   1235                     $debug = "$location -> ".($invalid ? "INVALID" : $path);
   1236                     echo "YellowLookup::findFileFromContentLocation $debug<br />\n";
   1237                 }
   1238             }
   1239         }
   1240         return $invalid ? "" : $path;
   1241     }
   1242     
   1243     // Return children from content location
   1244     public function findChildrenFromContentLocation($location) {
   1245         $fileNames = array();
   1246         if (!$this->isFileLocation($location)) {
   1247             $path = $this->findFileFromContentLocation($location, true);
   1248             $fileDefault = $this->yellow->system->get("coreContentDefaultFile");
   1249             $fileExtension = $this->yellow->system->get("coreContentExtension");
   1250             foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) {
   1251                 $token = $this->findFileDefault($path.$entry, $fileDefault, $fileExtension, false);
   1252                 array_push($fileNames, $path.$entry."/".$token);
   1253             }
   1254             if (!$this->isRootLocation($location)) {
   1255                 $regex = "/^.*\\".$fileExtension."$/";
   1256                 foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) {
   1257                     if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue;
   1258                     array_push($fileNames, $path.$entry);
   1259                 }
   1260             }
   1261         }
   1262         return $fileNames;
   1263     }
   1264     
   1265     // Return media location from file path
   1266     public function findMediaLocationFromFile($fileName) {
   1267         $location = "";
   1268         $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory"));
   1269         if (substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory")) {
   1270             $location = "/".$fileName;
   1271         }
   1272         return $location;
   1273     }
   1274 
   1275     // Return file path from media location
   1276     public function findFileFromMediaLocation($location) {
   1277         $fileName = "";
   1278         $mediaLocationLength = strlenu($this->yellow->system->get("coreMediaLocation"));
   1279         if (substru($location, 0, $mediaLocationLength)==$this->yellow->system->get("coreMediaLocation")) {
   1280             $fileName = substru($location, 1);
   1281         }
   1282         return $fileName;
   1283     }
   1284     
   1285     // Return children from media location
   1286     public function findChildrenFromMediaLocation($location) {
   1287         $fileNames = array();
   1288         if (!$this->isFileLocation($location)) {
   1289             $path = $this->findFileFromMediaLocation($location);
   1290             foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, true) as $entry) {
   1291                 array_push($fileNames, $entry."/");
   1292             }
   1293             foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, false, true) as $entry) {
   1294                 array_push($fileNames, $entry);
   1295             }
   1296         }
   1297         return $fileNames;
   1298     }
   1299     
   1300     // Return media directory from a system setting
   1301     public function findMediaDirectory($key) {
   1302         return substru($key, -8, 8)=="Location" ? $this->findFileFromMediaLocation($this->yellow->system->get($key)) : "";
   1303     }
   1304     
   1305     // Return system location from file path, for virtually mapped system files
   1306     public function findSystemLocationFromFile($fileName) {
   1307         $location = "";
   1308         $layoutDirectoryLength = strlenu($this->yellow->system->get("coreLayoutDirectory"));
   1309         $themeDirectoryLength = strlenu($this->yellow->system->get("coreThemeDirectory"));
   1310         $workerDirectoryLength = strlenu($this->yellow->system->get("coreWorkerDirectory"));
   1311         if (substru($fileName, 0, $layoutDirectoryLength)==$this->yellow->system->get("coreLayoutDirectory")) {
   1312             if ($this->isSafeFile($fileName)) {
   1313                 $location = $this->yellow->system->get("coreAssetLocation").substru($fileName, $layoutDirectoryLength);
   1314             }
   1315         } elseif (substru($fileName, 0, $themeDirectoryLength)==$this->yellow->system->get("coreThemeDirectory")) {
   1316             if ($this->isSafeFile($fileName)) {
   1317                 $location = $this->yellow->system->get("coreAssetLocation").substru($fileName, $themeDirectoryLength);
   1318             }
   1319         } elseif (substru($fileName, 0, $workerDirectoryLength)==$this->yellow->system->get("coreWorkerDirectory")) {
   1320             if ($this->isSafeFile($fileName)) {
   1321                 $location = $this->yellow->system->get("coreAssetLocation").substru($fileName, $workerDirectoryLength);
   1322             }
   1323         }
   1324         return $location;
   1325     }
   1326     
   1327     // Return file path from system location, for virtually mapped system files
   1328     public function findFileFromSystemLocation($location) {
   1329         $fileName = "";
   1330         $assetLocationLength = strlenu($this->yellow->system->get("coreAssetLocation"));
   1331         if (substru($location, 0, $assetLocationLength)==$this->yellow->system->get("coreAssetLocation")) {
   1332             if ($this->isSafeFile($location)) {
   1333                 $fileNameLayout = $this->yellow->system->get("coreLayoutDirectory").substru($location, $assetLocationLength);
   1334                 $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").substru($location, $assetLocationLength);
   1335                 $fileNameWorker = $this->yellow->system->get("coreWorkerDirectory").substru($location, $assetLocationLength);
   1336                 if (is_file($fileNameLayout)) {
   1337                     $fileName = $fileNameLayout;
   1338                 } elseif (is_file($fileNameTheme)) {
   1339                     $fileName = $fileNameTheme;
   1340                 } elseif (is_file($fileNameWorker)) {
   1341                     $fileName = $fileNameWorker;
   1342                 }
   1343             }
   1344         }
   1345         return $fileName;
   1346     }
   1347     
   1348     // Return file or directory that matches token
   1349     public function findFileDirectory($path, $token, $fileExtension, $directory, $default, &$found, &$invalid) {
   1350         if ($this->normaliseToken($token, $fileExtension)!=$token) $invalid = true;
   1351         if (!$invalid) {
   1352             $regex = "/^[\d\-\_\.]*".str_replace("-", ".", $token)."$/";
   1353             foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, $directory, false) as $entry) {
   1354                 if ($this->normaliseToken($entry, $fileExtension)==$token) {
   1355                     $token = $entry;
   1356                     $found = true;
   1357                     break;
   1358                 }
   1359             }
   1360         }
   1361         if ($directory) $token .= "/";
   1362         return ($default || $found) ? $token : "";
   1363     }
   1364     
   1365     // Return default file in directory
   1366     public function findFileDefault($path, $fileDefault, $fileExtension, $includePath = true) {
   1367         $token = $fileDefault;
   1368         if (!is_file($path."/".$fileDefault)) {
   1369             $regex = "/^[\d\-\_\.]*($fileDefault)$/";
   1370             foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) {
   1371                 if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) {
   1372                     $token = $entry;
   1373                     break;
   1374                 }
   1375             }
   1376         }
   1377         return $includePath ? "$path/$token" : $token;
   1378     }
   1379     
   1380     // Normalise file/directory token
   1381     public function normaliseToken($text, $fileExtension = "", $removeExtension = false) {
   1382         if (!is_string_empty($fileExtension)) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text;
   1383         if (preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_empty($matches[1])) $text = $matches[1];
   1384         return preg_replace("/[^\pL\d\-\_]/u", "-", $text).($removeExtension ? "" : $fileExtension);
   1385     }
   1386     
   1387     // Normalise name
   1388     public function normaliseName($text, $removePrefix = false, $removeExtension = false, $filterStrict = false) {
   1389         if ($removeExtension) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text;
   1390         if ($removePrefix && preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !is_string_empty($matches[1])) $text = $matches[1];
   1391         if ($filterStrict) $text = strtoloweru($text);
   1392         return preg_replace("/[^\pL\d\-\_]/u", "-", $text);
   1393     }
   1394     
   1395     // Normalise prefix
   1396     public function normalisePrefix($text) {
   1397         $prefix = "";
   1398         if (preg_match("/^([\d\-\_\.]*)(.*)$/", $text, $matches)) $prefix = $matches[1];
   1399         if (!is_string_empty($prefix) && !preg_match("/[\-\_\.]$/", $prefix)) $prefix .= "-";
   1400         return $prefix;
   1401     }
   1402     
   1403     // Normalise elements and attributes in HTML/SVG data
   1404     public function normaliseData($text, $type = "html", $filterStrict = true) {
   1405         $output = "";
   1406         $elementsHtml = array(
   1407             "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");
   1408         $elementsSvg = array(
   1409             "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");
   1410         $attributesHtml = array(
   1411             "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");
   1412         $attributesSvg = array(
   1413             "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");
   1414         $attributesAllowEmptyString = array("alt", "download", "open", "sandbox", "value");
   1415         $elementsSafe = $elementsHtml;
   1416         $attributesSafe = $attributesHtml;
   1417         if ($type=="svg") {
   1418             $elementsSafe = array_merge($elementsHtml, $elementsSvg);
   1419             $attributesSafe = array_merge($attributesHtml, $attributesSvg);
   1420         }
   1421         $offsetBytes = 0;
   1422         while (true) {
   1423             $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes);
   1424             $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes);
   1425             $elementStart = $elementFound ? $matches[1][0] : "";
   1426             $elementName = $elementFound ? $matches[2][0]: "";
   1427             $elementMiddle = $elementFound ? $matches[3][0]: "";
   1428             $elementEnd = $elementFound ? $matches[4][0]: "";
   1429             $output .= $elementBefore;
   1430             if (substrb($elementName, 0, 1)=="!") {
   1431                 $output .= "<$elementName$elementMiddle>";
   1432             } elseif (in_array(strtolower($elementName), $elementsSafe)) {
   1433                 $elementAttributes = $this->getTextAttributes($elementMiddle, $attributesAllowEmptyString);
   1434                 foreach ($elementAttributes as $key=>$value) {
   1435                     if (!in_array(strtolower($key), $attributesSafe) && !preg_match("/^(aria|data)-/i", $key)) {
   1436                         unset($elementAttributes[$key]);
   1437                     }
   1438                 }
   1439                 if ($filterStrict) {
   1440                     $href = isset($elementAttributes["href"]) ? $elementAttributes["href"] : "";
   1441                     if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) {
   1442                         $elementAttributes["href"] = "error-xss-filter";
   1443                     }
   1444                     $href = isset($elementAttributes["xlink:href"]) ? $elementAttributes["xlink:href"] : "";
   1445                     if (preg_match("/^\w+:/", $href) && !$this->isSafeUrl($href)) {
   1446                         $elementAttributes["xlink:href"] = "error-xss-filter";
   1447                     }
   1448                 }
   1449                 $output .= "<$elementStart$elementName";
   1450                 foreach ($elementAttributes as $key=>$value) $output .= " $key=\"$value\"";
   1451                 if (!is_string_empty($elementEnd)) $output .= " ";
   1452                 $output .= "$elementEnd>";
   1453             }
   1454             if (!$elementFound) break;
   1455             $offsetBytes = $matches[0][1] + strlenb($matches[0][0]);
   1456         }
   1457         return $output;
   1458     }
   1459     
   1460     // Normalise name and email for a single address
   1461     public function normaliseAddress($input, $type = "mail", $filterStrict = true) {
   1462         $output = "";
   1463         if ($type=="mail") {
   1464             if (preg_match("/^(.*?)(\s*)<(.*?)>$/", $input, $matches)) {
   1465                 $name = $matches[1];
   1466                 $email = $matches[3];
   1467             } else {
   1468                 $name = "";
   1469                 $email = $input;
   1470             }
   1471             $name = preg_replace("/[^\pL\d\-\. ]/u", "", $name);
   1472             $name = preg_replace("/\s+/s", " ", $name);
   1473             if ($filterStrict && !preg_match("/^[\w\+\-\.\@]+$/", $email)) {
   1474                 $email = "error-mail-address";
   1475             }
   1476             $output = is_string_empty($name) ? "<$email>" : "$name <$email>";
   1477         }
   1478         return $output;
   1479     }
   1480     
   1481     // Normalise fields in MIME headers
   1482     public function normaliseHeaders($input, $type = "mime", $filterStrict = true) {
   1483         $output = "";
   1484         if ($type=="mime") {
   1485             $keysMixedEncoding = array("To", "From", "Reply-To", "Cc", "Bcc");
   1486             foreach ($input as $key=>$value) {
   1487                 $key = ucwords(preg_replace("/[^a-zA-Z\-]/u", "-", $key), "-");
   1488                 if (in_array($key, $keysMixedEncoding)) {
   1489                     $text = "$key: ";
   1490                     foreach (preg_split("/\s*,\s*/", $value) as $email) {
   1491                         if (!preg_match("/^(.*?)(\s*)<(.*?)>$/", $email, $matches)) {
   1492                             $matches[1] = $matches[2] = "";
   1493                             $matches[3] = $email;
   1494                         }
   1495                         if (!is_string_empty($matches[1]) && !preg_match("/^[\pL\d\-\. ]+$/u", $matches[1])) {
   1496                             $matches[1] = $matches[2] = "";
   1497                             $matches[3] = "error-mail-address";
   1498                         }
   1499                         if ($filterStrict && !preg_match("/^[\w\+\-\.\@]+$/", $matches[3])) {
   1500                             $matches[3] = "error-mail-address";
   1501                         }
   1502                         if (substru($text, -2, 2)!=": ") $text .= ",\r\n ";
   1503                         $text = $this->getMimeHeader($text, $matches[1]);
   1504                         $text = $this->getMimeHeader($text, "$matches[2]<$matches[3]>", false);
   1505                     }
   1506                     $text .= "\r\n";
   1507                 } else {
   1508                     $text = $this->getMimeHeader("$key: ", $value)."\r\n";
   1509                 }
   1510                 $output .= $text;
   1511             }
   1512         }
   1513         return $output;
   1514     }
   1515     
   1516     // Normalise CSS class
   1517     public function normaliseClass($text) {
   1518         return str_replace(array(" ", "_"), array("-", "-"), strtoloweru($text));
   1519     }
   1520     
   1521     // Normalise relative path tokens
   1522     public function normalisePath($text) {
   1523         $textFiltered = "";
   1524         $textLength = strlenb($text);
   1525         for ($pos=0; $pos<$textLength; ++$pos) {
   1526             if ($text[$pos]=="." && ($pos==0 || $text[$pos-1]=="/")) {
   1527                 while ($text[$pos]==".") ++$pos;
   1528                 if ($text[$pos]=="/") ++$pos;
   1529                 --$pos;
   1530                 continue;
   1531             }
   1532             $textFiltered .= $text[$pos];
   1533         }
   1534         return $textFiltered;
   1535     }
   1536     
   1537     // Normalise text lines, convert line endings
   1538     public function normaliseLines($text, $endOfLine = "lf") {
   1539         if ($endOfLine=="lf") {
   1540             $text = preg_replace("/\R/u", "\n", $text);
   1541         } else {
   1542             $text = preg_replace("/\R/u", "\r\n", $text);
   1543         }
   1544         return $text;
   1545     }
   1546     
   1547     // Normalise text into UTF-8 NFC
   1548     public function normaliseUnicode($text) {
   1549         if (PHP_OS=="Darwin" && !mb_check_encoding($text, "ASCII")) {
   1550             $utf8nfc = preg_match("//u", $text) && !preg_match("/[^\\x00-\\x{2FF}]/u", $text);
   1551             if (!$utf8nfc) $text = iconv("UTF-8-MAC", "UTF-8", $text);
   1552         }
   1553         return $text;
   1554     }
   1555     
   1556     // Normalise location, make absolute location
   1557     public function normaliseLocation($location, $pageLocation, $filterStrict = true) {
   1558         if (!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) {
   1559             $pageBase = $this->yellow->page->base;
   1560             $mediaBase = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreMediaLocation");
   1561             if (!preg_match("/^\#/", $location)) {
   1562                 if (!preg_match("/^\//", $location)) {
   1563                     $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location;
   1564                 } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) {
   1565                     $location = $pageBase.$location;
   1566                 }
   1567             } else {
   1568                 $location = $pageBase.$pageLocation.$location;
   1569             }
   1570             $location = str_replace("/./", "/", $location);
   1571             $location = str_replace(":", $this->yellow->toolbox->getLocationArgumentsSeparator(), $location);
   1572         } else {
   1573             if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter";
   1574         }
   1575         return $location;
   1576     }
   1577     
   1578     // Normalise location arguments
   1579     public function normaliseArguments($text, $filterStrict = true) {
   1580         if ($filterStrict) $text = str_replace(" ", "-", strtoloweru($text));
   1581         $separator = $this->yellow->toolbox->getLocationArgumentsSeparator();
   1582         $text = str_replace(":", $separator, $text);
   1583         if (preg_match("/^(.*\/)?page$separator.*$/", $text)) {
   1584             $text = rtrim($text, "/");
   1585         } else {
   1586             $text = rtrim($text, "/")."/";
   1587         }
   1588         return str_replace(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text));
   1589     }
   1590     
   1591     // Normalise URL, make absolute URL
   1592     public function normaliseUrl($scheme, $address, $base, $location, $filterStrict = true) {
   1593         if (!preg_match("/^\w+:/", $location)) {
   1594             $url = "$scheme://$address$base$location";
   1595         } else {
   1596             if ($filterStrict && !$this->isSafeUrl($location)) $location = "error-xss-filter";
   1597             $url = $location;
   1598         }
   1599         return $url;
   1600     }
   1601     
   1602     // Return URL information
   1603     public function getUrlInformation($url) {
   1604         $scheme = $address = $base = "";
   1605         if (preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, "/"), $matches)) {
   1606             $scheme = $matches[1];
   1607             $address = $matches[2];
   1608             $base = $matches[3];
   1609         }
   1610         return array($scheme, $address, $base);
   1611     }
   1612     
   1613     // Return request information
   1614     public function getRequestInformation($scheme = "", $address = "", $base = "") {
   1615         if (is_string_empty($scheme) && is_string_empty($address) && is_string_empty($base)) {
   1616             $url = $this->yellow->system->get("coreServerUrl");
   1617             if ($url=="auto" || $this->isCommandLine()) $url = $this->yellow->toolbox->detectServerUrl();
   1618             list($scheme, $address, $base) = $this->getUrlInformation($url);
   1619             $this->yellow->system->set("coreServerScheme", $scheme);
   1620             $this->yellow->system->set("coreServerAddress", $address);
   1621             $this->yellow->system->set("coreServerBase", $base);
   1622             if ($this->yellow->system->get("coreDebugMode")>=3) {
   1623                 echo "YellowLookup::getRequestInformation $scheme://$address$base<br />\n";
   1624             }
   1625         }
   1626         $location = substru($this->yellow->toolbox->detectServerLocation(), strlenu($base));
   1627         $fileName = "";
   1628         if (is_string_empty($fileName)) $fileName = $this->findFileFromSystemLocation($location);
   1629         if (is_string_empty($fileName)) $fileName = $this->findFileFromMediaLocation($location);
   1630         if (is_string_empty($fileName)) $fileName = $this->findFileFromContentLocation($location);
   1631         return array($scheme, $address, $base, $location, $fileName);
   1632     }
   1633     
   1634     // Return command information
   1635     public function getCommandInformation($line = "") {
   1636         if (is_string_empty($line)) {
   1637             $line = $this->yellow->toolbox->getTextString(array_slice($this->yellow->toolbox->getServer("argv"), 1));
   1638             if ($this->yellow->system->get("coreDebugMode")>=3) {
   1639                 echo "YellowLookup::getCommandInformation $line<br />\n";
   1640             }
   1641         }
   1642         return $this->yellow->toolbox->getTextList($line, " ", 2);
   1643     }
   1644 
   1645     // Return request handler
   1646     public function getRequestHandler() {
   1647         return $this->requestHandler;
   1648     }
   1649 
   1650     // Return command handler
   1651     public function getCommandHandler() {
   1652         return $this->commandHandler;
   1653     }
   1654     
   1655     // Return attributes from text
   1656     public function getTextAttributes($text, $attributesAllowEmptyString) {
   1657         $tokens = array();
   1658         $posStart = $posQuote = 0;
   1659         $textLength = strlenb($text);
   1660         for ($pos=0; $pos<$textLength; ++$pos) {
   1661             if ($text[$pos]==" " && !$posQuote) {
   1662                 if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart));
   1663                 $posStart = $pos+1;
   1664             }
   1665             if ($text[$pos]=="=" && !$posQuote) {
   1666                 if ($pos>$posStart) array_push($tokens, substrb($text, $posStart, $pos-$posStart));
   1667                 array_push($tokens, "=");
   1668                 $posStart = $pos+1;
   1669             }
   1670             if ($text[$pos]=="\"") {
   1671                 if ($posQuote) {
   1672                     if ($pos>$posQuote) array_push($tokens, substrb($text, $posQuote+1, $pos-$posQuote-1));
   1673                     $posQuote = 0;
   1674                     $posStart = $pos+1;
   1675                 } else {
   1676                     if ($pos==$posStart) $posQuote = $pos;
   1677                 }
   1678             }
   1679         }
   1680         if ($pos>$posStart && !$posQuote) {
   1681             array_push($tokens, substrb($text, $posStart, $pos-$posStart));
   1682         }
   1683         $attributes = array();
   1684         for ($i=0; $i<count($tokens); ++$i) {
   1685             if ($i+2<count($tokens) && $tokens[$i+1]=="=") {
   1686                 $key = $tokens[$i];
   1687                 $value = $tokens[$i+2];
   1688                 $i += 2;
   1689             } else {
   1690                 $key = $value = $tokens[$i];
   1691             }
   1692             if (!is_string_empty($key) && (!is_string_empty($value) || in_array(strtolower($key), $attributesAllowEmptyString))) {
   1693                 $attributes[$key] = $value;
   1694             }
   1695         }
   1696         return $attributes;
   1697     }
   1698     
   1699     // Return HTML attributes from generic Markdown attributes
   1700     public function getHtmlAttributes($text) {
   1701         $htmlAttributes = "";
   1702         $htmlAttributesData = array();
   1703         foreach (explode(" ", $text) as $token) {
   1704             if (substru($token, 0, 1)==".") {
   1705                 if (!isset($htmlAttributesData["class"])) {
   1706                     $htmlAttributesData["class"] = substru($token, 1);
   1707                 } else {
   1708                     $htmlAttributesData["class"] .= " ".substru($token, 1);
   1709                 }
   1710             }
   1711             if (substru($token, 0, 1)=="#") $htmlAttributesData["id"] = substru($token, 1);
   1712             if (preg_match("/^([\w]+)=(.+)/", $token, $matches)) $htmlAttributesData[$matches[1]] = $matches[2];
   1713         }
   1714         foreach ($htmlAttributesData as $key=>$value) {
   1715             $htmlAttributes .= " $key=\"".htmlspecialchars($value)."\"";
   1716         }
   1717         return $htmlAttributes;
   1718     }
   1719     
   1720     // Return MIME header field, encode and fold if necessary
   1721     public function getMimeHeader($text, $field, $allowEncode = true) {
   1722         if ($allowEncode) {
   1723             $encode = preg_match("/[\x7F-\xFF]/", $field);
   1724             $fieldPos = 0;
   1725             while (true) {
   1726                 $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0);
   1727                 $bytesAvailable = max(0, 78-$textPos);
   1728                 $fragment = substrb($field, $fieldPos);
   1729                 if ($encode && !is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?=";
   1730                 if ($bytesAvailable<strlenb($fragment)) {
   1731                     $bytesHandled = $bytesAvailable;
   1732                     if (!$encode) {
   1733                         for ($pos=$bytesHandled;$pos>0;--$pos) {
   1734                             if ($field[$fieldPos+$pos]==" ") {
   1735                                 $fragment = substrb($field, $fieldPos, $pos);
   1736                                 $bytesHandled = $pos+1;
   1737                                 break;
   1738                             }
   1739                         }
   1740                         if ($pos==0) $encode = true;
   1741                     }
   1742                     if ($encode) {
   1743                         while (true) {
   1744                             $fragment = substrb($field, $fieldPos, $bytesHandled);
   1745                             if (!is_string_empty($fragment)) $fragment = "=?UTF-8?B?".base64_encode($fragment)."?=";
   1746                             if ($bytesAvailable>=strlenb($fragment) || $bytesHandled==0) break;
   1747                             --$bytesHandled;
   1748                         }
   1749                     }
   1750                     $text .= $fragment."\r\n ";
   1751                     $fieldPos += $bytesHandled;
   1752                 } else {
   1753                     $text .= $fragment;
   1754                     break;
   1755                 }
   1756             }
   1757         } else {
   1758             $textPos = strlenb($text)-(($pos = strrposb($text, "\n")) ? $pos+1 : 0);
   1759             $bytesAvailable = max(0, 78-$textPos);
   1760             if ($bytesAvailable<strlenb($field)) {
   1761                 $text .= "\r\n ".ltrim($field);
   1762             } else {
   1763                 $text .= $field;
   1764             }
   1765         }
   1766         return $text;
   1767     }
   1768     
   1769     // Return directory location
   1770     public function getDirectoryLocation($location) {
   1771         return ($pos = strrposu($location, "/")) ? substru($location, 0, $pos+1) : "/";
   1772     }
   1773     
   1774     // Return redirect location
   1775     public function getRedirectLocation($location) {
   1776         if ($this->isFileLocation($location)) {
   1777             $location = "$location/";
   1778         } else {
   1779             $languageDefault = $this->yellow->system->get("language");
   1780             $language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->content->getLanguages(), $languageDefault);
   1781             $location = "/$language/";
   1782         }
   1783         return $location;
   1784     }
   1785     
   1786     // Check if clean URL is requested
   1787     public function isRequestCleanUrl($location) {
   1788         return isset($_REQUEST["clean-url"]) && substru($location, -1, 1)=="/";
   1789     }
   1790     
   1791     // Check if location is specifying root
   1792     public function isRootLocation($location) {
   1793         return substru($location, 0, 1)!="/";
   1794     }
   1795     
   1796     // Check if location is specifying file or directory
   1797     public function isFileLocation($location) {
   1798         return substru($location, -1, 1)!="/";
   1799     }
   1800     
   1801     // Check if location can be redirected into directory
   1802     public function isRedirectLocation($location) {
   1803         $redirect = false;
   1804         if ($this->isFileLocation($location)) {
   1805             $redirect = is_dir($this->findFileFromContentLocation("$location/", true));
   1806         } elseif ($location=="/") {
   1807             $redirect = $this->yellow->system->get("coreMultiLanguageMode");
   1808         }
   1809         return $redirect;
   1810     }
   1811     
   1812     // Check if location contains nested directories
   1813     public function isNestedLocation($location, $fileName, $checkHomeLocation = false) {
   1814         $nested = false;
   1815         if (!$checkHomeLocation || $location==$this->yellow->content->getHomeLocation($location)) {
   1816             $path = dirname($fileName);
   1817             if (!is_array_empty($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true;
   1818         }
   1819         return $nested;
   1820     }
   1821     
   1822     // Check if location is within shared directory
   1823     public function isSharedLocation($location) {
   1824         $sharedLocation = $this->yellow->content->getHomeLocation($location)."shared/";
   1825         return substru($location, 0, strlenu($sharedLocation))==$sharedLocation;
   1826     }
   1827     
   1828     // Check if location is within current HTTP request
   1829     public function isActiveLocation($location, $currentLocation) {
   1830         if ($this->isFileLocation($location)) {
   1831             $active = $currentLocation==$location;
   1832         } else {
   1833             if ($location==$this->yellow->content->getHomeLocation($location)) {
   1834                 $active = $this->getDirectoryLocation($currentLocation)==$location;
   1835             } else {
   1836                 $active = substru($currentLocation, 0, strlenu($location))==$location;
   1837             }
   1838         }
   1839         return $active;
   1840     }
   1841     
   1842     // Check if URL is a well-known URL scheme
   1843     public function isSafeUrl($url) {
   1844         return preg_match("/^(http|https|ftp|mailto|tel):/", $url);
   1845     }
   1846     
   1847     // Check if file is a well-known file type
   1848     public function isSafeFile($fileName) {
   1849         return preg_match("/\.(css|gif|ico|js|jpeg|jpg|json|map|png|scss|svg|webmanifest|woff|woff2)$/", $fileName);
   1850     }
   1851     
   1852     // Check if file is valid
   1853     public function isValidFile($fileName) {
   1854         $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory"));
   1855         $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory"));
   1856         $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory"));
   1857         return strposu($fileName, "/")===false ||
   1858             substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory") ||
   1859             substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory") ||
   1860             substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory");
   1861     }
   1862     
   1863     // Check if content file
   1864     public function isContentFile($fileName) {
   1865         $contentDirectoryLength = strlenu($this->yellow->system->get("coreContentDirectory"));
   1866         return substru($fileName, 0, $contentDirectoryLength)==$this->yellow->system->get("coreContentDirectory");
   1867     }
   1868     
   1869     // Check if media file
   1870     public function isMediaFile($fileName) {
   1871         $mediaDirectoryLength = strlenu($this->yellow->system->get("coreMediaDirectory"));
   1872         return substru($fileName, 0, $mediaDirectoryLength)==$this->yellow->system->get("coreMediaDirectory");
   1873     }
   1874     
   1875     // Check if system file
   1876     public function isSystemFile($fileName) {
   1877         $systemDirectoryLength = strlenu($this->yellow->system->get("coreSystemDirectory"));
   1878         return substru($fileName, 0, $systemDirectoryLength)==$this->yellow->system->get("coreSystemDirectory");
   1879     }
   1880     
   1881     // Check if running at command line
   1882     public function isCommandLine() {
   1883         return isset($this->commandHandler);
   1884     }
   1885 }
   1886 
   1887 class YellowToolbox {
   1888     public $yellow;             // access to API
   1889     
   1890     public function __construct($yellow) {
   1891         $this->yellow = $yellow;
   1892     }
   1893     
   1894     // Return browser cookie from from current HTTP request
   1895     public function getCookie($key) {
   1896         return isset($_COOKIE[$key]) ? $_COOKIE[$key] : "";
   1897     }
   1898     
   1899     // Return server argument from current HTTP request
   1900     public function getServer($key) {
   1901         return isset($_SERVER[$key]) ? $_SERVER[$key] : "";
   1902     }
   1903     
   1904     // Return location arguments from current HTTP request
   1905     public function getLocationArguments() {
   1906         return $this->getServer("LOCATION_ARGUMENTS");
   1907     }
   1908     
   1909     // Return location arguments from current HTTP request, modify existing arguments
   1910     public function getLocationArgumentsNew($key, $value) {
   1911         $locationArguments = "";
   1912         $found = false;
   1913         $separator = $this->getLocationArgumentsSeparator();
   1914         foreach (explode("/", $this->getServer("LOCATION_ARGUMENTS")) as $token) {
   1915             if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) {
   1916                 if ($matches[1]==$key) {
   1917                     $matches[2] = $value;
   1918                     $found = true;
   1919                 }
   1920                 if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
   1921                     if (!is_string_empty($locationArguments)) $locationArguments .= "/";
   1922                     $locationArguments .= "$matches[1]:$matches[2]";
   1923                 }
   1924             }
   1925         }
   1926         if (!$found && !is_string_empty($key) && !is_string_empty($value)) {
   1927             if (!is_string_empty($locationArguments)) $locationArguments .= "/";
   1928             $locationArguments .= "$key:$value";
   1929         }
   1930         if (!is_string_empty($locationArguments)) {
   1931             $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false);
   1932         }
   1933         return $locationArguments;
   1934     }
   1935     
   1936     // Return location arguments from current HTTP request, convert form parameters
   1937     public function getLocationArgumentsCleanUrl() {
   1938         $locationArguments = "";
   1939         foreach (array_merge($_GET, $_POST) as $key=>$value) {
   1940             if (!is_string_empty($key) && !is_string_empty($value)) {
   1941                 if (!is_string_empty($locationArguments)) $locationArguments .= "/";
   1942                 $key = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $key);
   1943                 $value = str_replace(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $value);
   1944                 $locationArguments .= "$key:$value";
   1945             }
   1946         }
   1947         if (!is_string_empty($locationArguments)) {
   1948             $locationArguments = $this->yellow->lookup->normaliseArguments($locationArguments, false);
   1949         }
   1950         return $locationArguments;
   1951     }
   1952 
   1953     // Return location arguments separator
   1954     public function getLocationArgumentsSeparator() {
   1955         return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ":" : "=";
   1956     }
   1957     
   1958     // Return human readable HTTP date
   1959     public function getHttpDateFormatted($timestamp) {
   1960         return gmdate("D, d M Y H:i:s", $timestamp)." GMT";
   1961     }
   1962     
   1963     // Return human readable HTTP server status
   1964     public function getHttpStatusFormatted($statusCode, $shortFormat = false) {
   1965         switch ($statusCode) {
   1966             case 0:     $text = "No data"; break;
   1967             case 200:   $text = "OK"; break;
   1968             case 301:   $text = "Moved permanently"; break;
   1969             case 302:   $text = "Moved temporarily"; break;
   1970             case 303:   $text = "Reload please"; break;
   1971             case 304:   $text = "Not modified"; break;
   1972             case 400:   $text = "Bad request"; break;
   1973             case 403:   $text = "Forbidden"; break;
   1974             case 404:   $text = "Not found"; break;
   1975             case 420:   $text = "Not public"; break;
   1976             case 430:   $text = "Login failed"; break;
   1977             case 434:   $text = "Can create"; break;
   1978             case 435:   $text = "Can restore"; break;
   1979             case 450:   $text = "Update error"; break;
   1980             case 500:   $text = "Server error"; break;
   1981             case 503:   $text = "Service unavailable"; break;
   1982             default:    $text = "Error $statusCode";
   1983         }
   1984         $serverProtocol = $this->getServer("SERVER_PROTOCOL");
   1985         if (!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1";
   1986         return $shortFormat ? $text : "$serverProtocol $statusCode $text";
   1987     }
   1988     
   1989     // Return MIME content type
   1990     public function getMimeContentType($fileName) {
   1991         $contentType = "";
   1992         $contentTypes = array(
   1993             "css" => "text/css",
   1994             "gif" => "image/gif",
   1995             "html" => "text/html; charset=utf-8",
   1996             "ico" => "image/x-icon",
   1997             "js" => "application/javascript",
   1998             "json" => "application/json",
   1999             "jpeg" => "image/jpeg",
   2000             "jpg" => "image/jpeg",
   2001             "md" => "text/markdown",
   2002             "png" => "image/png",
   2003             "scss" => "text/x-scss",
   2004             "svg" => "image/svg+xml",
   2005             "txt" => "text/plain",
   2006             "webmanifest" => "application/manifest+json",
   2007             "woff" => "application/font-woff",
   2008             "woff2" => "application/font-woff2",
   2009             "xml" => "text/xml; charset=utf-8");
   2010         $fileType = $this->getFileType($fileName);
   2011         if (is_string_empty($fileType)) {
   2012             $contentType = $contentTypes["html"];
   2013         } elseif (array_key_exists($fileType, $contentTypes)) {
   2014             $contentType = $contentTypes[$fileType];
   2015         }
   2016         return $contentType;
   2017     }
   2018     
   2019     // Send HTTP header
   2020     public function sendHttpHeader($text) {
   2021         if (!headers_sent()) header($text);
   2022     }
   2023     
   2024     // Return files and directories
   2025     public function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true) {
   2026         return $this->getDirectoryEntriesRecursive($path, $regex, $sort, $directories, $includePath, 1);
   2027     }
   2028     
   2029     // Return files and directories recursively
   2030     public function getDirectoryEntriesRecursive($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true, $levelMax = 0) {
   2031         --$levelMax;
   2032         $entries = array();
   2033         $directoryHandle = @opendir($path);
   2034         if ($directoryHandle) {
   2035             $path = rtrim($path, "/");
   2036             $directoryEntries = array();
   2037             while (($entry = readdir($directoryHandle))!==false) {
   2038                 if (substru($entry, 0, 1)==".") continue;
   2039                 $entry = $this->yellow->lookup->normaliseUnicode($entry);
   2040                 if (preg_match($regex, $entry)) {
   2041                     if ($directories) {
   2042                         if (is_dir("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry);
   2043                     } else {
   2044                         if (is_file("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry);
   2045                     }
   2046                 }
   2047                 if (is_dir("$path/$entry") && $levelMax!=0) array_push($directoryEntries, "$path/$entry");
   2048             }
   2049             if ($sort) {
   2050                 natcasesort($entries);
   2051                 natcasesort($directoryEntries);
   2052             }
   2053             closedir($directoryHandle);
   2054             foreach ($directoryEntries as $directoryEntry) {
   2055                 $entries = array_merge($entries, $this->getDirectoryEntriesRecursive($directoryEntry, $regex, $sort, $directories, $includePath, $levelMax));
   2056             }
   2057         }
   2058         return $entries;
   2059     }
   2060     
   2061     // Return directory information, modification date and file count
   2062     public function getDirectoryInformation($path) {
   2063         return $this->getDirectoryInformationRecursive($path, 1);
   2064     }
   2065     
   2066     // Return directory information recursively, modification date and file count
   2067     public function getDirectoryInformationRecursive($path, $levelMax = 0) {
   2068         --$levelMax;
   2069         $modified = $fileCount = 0;
   2070         $directoryHandle = @opendir($path);
   2071         if ($directoryHandle) {
   2072             $path = rtrim($path, "/");
   2073             $directoryEntries = array();
   2074             while (($entry = readdir($directoryHandle))!==false) {
   2075                 if (substru($entry, 0, 1)==".") continue;
   2076                 $modified = max($modified, $this->getFileModified("$path/$entry"));
   2077                 if (is_file("$path/$entry")) ++$fileCount;
   2078                 if (is_dir("$path/$entry") && $levelMax!=0) array_push($directoryEntries, "$path/$entry");
   2079             }
   2080             closedir($directoryHandle);
   2081             foreach ($directoryEntries as $directoryEntry) {
   2082                 list($modifiedBelow, $fileCountBelow) = $this->getDirectoryInformationRecursive($directoryEntry, $levelMax);
   2083                 $modified = max($modified, $modifiedBelow);
   2084                 $fileCount += $fileCountBelow;
   2085             }
   2086         }
   2087         return array($modified, $fileCount);
   2088     }
   2089     
   2090     // Read file, empty string if not found
   2091     public function readFile($fileName, $sizeMax = 0) {
   2092         $fileData = "";
   2093         $fileHandle = @fopen($fileName, "rb");
   2094         if ($fileHandle) {
   2095             clearstatcache(true, $fileName);
   2096             if (flock($fileHandle, LOCK_SH)) {
   2097                 $fileSize = $sizeMax ? $sizeMax : filesize($fileName);
   2098                 if ($fileSize) $fileData = fread($fileHandle, $fileSize);
   2099                 flock($fileHandle, LOCK_UN);
   2100             }
   2101             fclose($fileHandle);
   2102         }
   2103         return $fileData;
   2104     }
   2105     
   2106     // Write file
   2107     public function writeFile($fileName, $fileData, $mkdir = false) {
   2108         $ok = false;
   2109         if ($mkdir) {
   2110             $path = dirname($fileName);
   2111             if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
   2112         }
   2113         $fileHandle = @fopen($fileName, "cb");
   2114         if ($fileHandle) {
   2115             clearstatcache(true, $fileName);
   2116             if (flock($fileHandle, LOCK_EX)) {
   2117                 ftruncate($fileHandle, 0);
   2118                 fwrite($fileHandle, $fileData);
   2119                 fflush($fileHandle);
   2120                 flock($fileHandle, LOCK_UN);
   2121             }
   2122             fclose($fileHandle);
   2123             $ok = true;
   2124         }
   2125         return $ok;
   2126     }
   2127     
   2128     // Append file
   2129     public function appendFile($fileName, $fileData, $mkdir = false) {
   2130         $ok = false;
   2131         if ($mkdir) {
   2132             $path = dirname($fileName);
   2133             if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
   2134         }
   2135         $fileHandle = @fopen($fileName, "ab");
   2136         if ($fileHandle) {
   2137             clearstatcache(true, $fileName);
   2138             if (flock($fileHandle, LOCK_EX)) {
   2139                 fwrite($fileHandle, $fileData);
   2140                 fflush($fileHandle);
   2141                 flock($fileHandle, LOCK_UN);
   2142             }
   2143             fclose($fileHandle);
   2144             $ok = true;
   2145         }
   2146         return $ok;
   2147     }
   2148     
   2149     // Copy file
   2150     public function copyFile($fileNameSource, $fileNameDestination, $mkdir = false) {
   2151         clearstatcache();
   2152         if ($mkdir) {
   2153             $path = dirname($fileNameDestination);
   2154             if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
   2155         }
   2156         return @copy($fileNameSource, $fileNameDestination);
   2157     }
   2158     
   2159     // Rename file
   2160     public function renameFile($fileNameSource, $fileNameDestination, $mkdir = false) {
   2161         clearstatcache();
   2162         if ($mkdir) {
   2163             $path = dirname($fileNameDestination);
   2164             if (!is_string_empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
   2165         }
   2166         return @rename($fileNameSource, $fileNameDestination);
   2167     }
   2168     
   2169     // Rename directory
   2170     public function renameDirectory($pathSource, $pathDestination, $mkdir = false) {
   2171         return $pathSource==$pathDestination || $this->renameFile($pathSource, $pathDestination, $mkdir);
   2172     }
   2173 
   2174     // Delete file
   2175     public function deleteFile($fileName, $pathTrash = "") {
   2176         clearstatcache();
   2177         if (is_string_empty($pathTrash)) {
   2178             $ok = @unlink($fileName);
   2179         } else {
   2180             if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true);
   2181             $fileNameDestination = $pathTrash;
   2182             $fileNameDestination .= pathinfo($fileName, PATHINFO_FILENAME);
   2183             $fileNameDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s"));
   2184             $fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION);
   2185             $ok = @rename($fileName, $fileNameDestination);
   2186         }
   2187         return $ok;
   2188     }
   2189     
   2190     // Delete directory
   2191     public function deleteDirectory($path, $pathTrash = "") {
   2192         clearstatcache();
   2193         if (is_string_empty($pathTrash)) {
   2194             $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
   2195             $files = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST);
   2196             foreach ($files as $file) {
   2197                 if ($file->getType()=="dir") {
   2198                     @rmdir($file->getPathname());
   2199                 } else {
   2200                     @unlink($file->getPathname());
   2201                 }
   2202             }
   2203             $ok = @rmdir($path);
   2204         } else {
   2205             if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true);
   2206             $pathDestination = $pathTrash;
   2207             $pathDestination .= basename($path);
   2208             $pathDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s"));
   2209             $ok = @rename($path, $pathDestination);
   2210         }
   2211         return $ok;
   2212     }
   2213     
   2214     // Set file/directory modification date, Unix time
   2215     public function modifyFile($fileName, $modified) {
   2216         clearstatcache(true, $fileName);
   2217         return @touch($fileName, $modified);
   2218     }
   2219     
   2220     // Return file/directory modification date, Unix time
   2221     public function getFileModified($fileName) {
   2222         return (is_file($fileName) || is_dir($fileName)) ? filemtime($fileName) : 0;
   2223     }
   2224     
   2225     // Return file/directory deletion date, Unix time
   2226     public function getFileDeleted($fileName) {
   2227         $deleted = 0;
   2228         $text = basename($fileName);
   2229         $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text;
   2230         if (preg_match("#^(.+)-(\d\d\d\d-\d\d-\d\d)-(\d\d)-(\d\d)-(\d\d)$#", $text, $matches)) {
   2231             $deleted = strtotime("$matches[2] $matches[3]:$matches[4]:$matches[5]");
   2232         }
   2233         return $deleted;
   2234     }
   2235     
   2236     // Return file size
   2237     public function getFileSize($fileName) {
   2238         return is_file($fileName) ? filesize($fileName) : 0;
   2239     }
   2240     
   2241     // Return file type
   2242     public function getFileType($fileName) {
   2243         return strtoloweru(($pos = strrposu($fileName, ".")) ? substru($fileName, $pos+1) : "");
   2244     }
   2245     
   2246     // Return file group
   2247     public function getFileGroup($fileName, $path) {
   2248         $group = "none";
   2249         if (preg_match("#^$path(.+?)\/#", $fileName, $matches)) $group = strtoloweru($matches[1]);
   2250         return $group;
   2251     }
   2252     
   2253     // Return number of bytes
   2254     public function getNumberBytes($text) {
   2255         $bytes = intval($text);
   2256         switch (strtoupperu(substru($text, -1))) {
   2257             case "G": $bytes *= 1024*1024*1024; break;
   2258             case "M": $bytes *= 1024*1024; break;
   2259             case "K": $bytes *= 1024; break;
   2260         }
   2261         return $bytes;
   2262     }
   2263     
   2264     // Return lines from text, including newline
   2265     public function getTextLines($text) {
   2266         $lines = preg_split("/\n/", $text);
   2267         foreach ($lines as &$line) {
   2268             $line = $line."\n";
   2269         }
   2270         if (is_string_empty($text) || substru($text, -1, 1)=="\n") array_pop($lines);
   2271         return $lines;
   2272     }
   2273     
   2274     // Return settings from text
   2275     function getTextSettings($text, $blockStart) {
   2276         $settings = new YellowArray();
   2277         if (is_string_empty($blockStart)) {
   2278             foreach ($this->getTextLines($text) as $line) {
   2279                 if (preg_match("/^\#/", $line)) continue;
   2280                 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
   2281                     if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
   2282                         $settings[$matches[1]] = $matches[2];
   2283                     }
   2284                 }
   2285             }
   2286         } else {
   2287             $blockKey = "";
   2288             foreach ($this->getTextLines($text) as $line) {
   2289                 if (preg_match("/^\#/", $line)) continue;
   2290                 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
   2291                     if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) {
   2292                         $blockKey = $matches[2];
   2293                         $settings[$blockKey] = new YellowArray();
   2294                     }
   2295                     if (!is_string_empty($blockKey) && !is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
   2296                         $settings[$blockKey][$matches[1]] = $matches[2];
   2297                     }
   2298                 }
   2299             }
   2300         }
   2301         return $settings;
   2302     }
   2303     
   2304     // Set settings in text
   2305     function setTextSettings($text, $blockStart, $blockKey, $settings) {
   2306         $textNew = "";
   2307         if (is_string_empty($blockStart)) {
   2308             foreach ($this->getTextLines($text) as $line) {
   2309                 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
   2310                     if (!is_string_empty($matches[1]) && isset($settings[$matches[1]])) {
   2311                         $textNew .= "$matches[1]: ".$settings[$matches[1]]."\n";
   2312                         unset($settings[$matches[1]]);
   2313                         continue;
   2314                     }
   2315                 }
   2316                 $textNew .= $line;
   2317             }
   2318             foreach ($settings as $key=>$value) {
   2319                 $textNew .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n";
   2320             }
   2321         } else {
   2322             $scan = false;
   2323             $textStart = $textMiddle = $textEnd = "";
   2324             foreach ($this->getTextLines($text) as $line) {
   2325                 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
   2326                     if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) {
   2327                         $scan = lcfirst($matches[2])==lcfirst($blockKey);
   2328                     }
   2329                 }
   2330                 if (!$scan && is_string_empty($textMiddle)) {
   2331                     $textStart .= $line;
   2332                 } elseif ($scan) {
   2333                     $textMiddle .= $line;
   2334                 } else {
   2335                     $textEnd .= $line;
   2336                 }
   2337             }
   2338             $textSettings = "";
   2339             foreach ($this->getTextLines($textMiddle) as $line) {
   2340                 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
   2341                     if (!is_string_empty($matches[1]) && isset($settings[$matches[1]])) {
   2342                         $textSettings .= "$matches[1]: ".$settings[$matches[1]]."\n";
   2343                         unset($settings[$matches[1]]);
   2344                         continue;
   2345                     }
   2346                     $textSettings .= $line;
   2347                 }
   2348             }
   2349             foreach ($settings as $key=>$value) {
   2350                 $textSettings .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n";
   2351             }
   2352             if (!is_string_empty($textMiddle)) {
   2353                 $textMiddle = $textSettings;
   2354                 if (!is_string_empty($textEnd)) $textMiddle .= "\n";
   2355             } else {
   2356                 if (!is_string_empty($textStart)) $textEnd .= "\n";
   2357                 $textEnd .= $textSettings;
   2358             }
   2359             $textNew = $textStart.$textMiddle.$textEnd;
   2360         }
   2361         return $textNew;
   2362     }
   2363 
   2364     // Remove settings from text
   2365     function unsetTextSettings($text, $blockStart, $blockKey) {
   2366         $textNew = "";
   2367         if (!is_string_empty($blockStart)) {
   2368             $scan = false;
   2369             $textStart = $textMiddle = $textEnd = "";
   2370             foreach ($this->getTextLines($text) as $line) {
   2371                 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
   2372                     if (lcfirst($matches[1])==$blockStart && !is_string_empty($matches[2])) {
   2373                         $scan = lcfirst($matches[2])==lcfirst($blockKey);
   2374                     }
   2375                 }
   2376                 if (!$scan && is_string_empty($textMiddle)) {
   2377                     $textStart .= $line;
   2378                 } elseif ($scan) {
   2379                     $textMiddle .= $line;
   2380                 } else {
   2381                     $textEnd .= $line;
   2382                 }
   2383             }
   2384             $textNew = rtrim($textStart.$textEnd)."\n";
   2385         }
   2386         return $textNew;
   2387     }
   2388     
   2389     // Return array of specific size from text
   2390     public function getTextList($text, $separator, $size) {
   2391         $tokens = explode($separator, $text, $size);
   2392         return array_pad($tokens, $size, "");
   2393     }
   2394     
   2395     // Return array of variable size from text, space separated
   2396     public function getTextArguments($text, $optional = "-", $sizeMin = 9) {
   2397         $text = preg_replace("/\s+/s", " ", trim($text));
   2398         $tokens = str_getcsv($text, " ", "\"", "");
   2399         foreach ($tokens as $key=>$value) {
   2400             if (is_null($value) || $value==$optional) $tokens[$key] = "";
   2401         }
   2402         return array_pad($tokens, $sizeMin, "");
   2403     }
   2404     
   2405     // Return text from array, space separated
   2406     public function getTextString($tokens, $optional = "-") {
   2407         $text = "";
   2408         foreach ($tokens as $token) {
   2409             if (preg_match("/\s/", $token)) $token = "\"$token\"";
   2410             if (is_string_empty($token)) $token = $optional;
   2411             if (!is_string_empty($text)) $text .= " ";
   2412             $text .= $token;
   2413         }
   2414         return $text;
   2415     }
   2416 
   2417     // Return number of words in text
   2418     public function getTextWords($text) {
   2419         $text = preg_replace("/([\p{Han}\p{Hiragana}\p{Katakana}]{3})/u", "$1 ", $text);
   2420         $text = preg_replace("/(\pL|\p{N})/u", "x", $text);
   2421         return str_word_count($text);
   2422     }
   2423     
   2424     // Return text truncated at word boundary
   2425     public function getTextTruncated($text, $lengthMax) {
   2426         if (strlenu($text)>$lengthMax-1) {
   2427             $text = substru($text, 0, $lengthMax);
   2428             $pos = strrposu($text, " ");
   2429             $text = substru($text, 0, $pos ? $pos : $lengthMax-1)."…";
   2430         }
   2431         return $text;
   2432     }
   2433     
   2434     // Create text description, with or without HTML
   2435     public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") {
   2436         $output = "";
   2437         $elementsBlock = array("blockquote", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "li", "ol", "p", "pre", "ul");
   2438         $elementsVoid = array("area", "br", "col", "embed", "hr", "img", "input", "param", "source", "wbr");
   2439         if ($lengthMax==0) $lengthMax = strlenu($text);
   2440         if ($removeHtml) {
   2441             $hiddenLevel = 0;
   2442             $offsetBytes = 0;
   2443             while (true) {
   2444                 $elementFound = preg_match("/<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes);
   2445                 $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes);
   2446                 $elementRawData = isset($matches[0][0]) ? $matches[0][0] : "";
   2447                 $elementStart = isset($matches[1][0]) ? $matches[1][0] : "";
   2448                 $elementName = isset($matches[2][0]) ? $matches[2][0] : "";
   2449                 $elementAttributes = isset($matches[3][0]) ? $matches[3][0] : "";
   2450                 $elementEnd = isset($matches[4][0]) ? $matches[4][0] : "";
   2451                 if (!is_string_empty($elementBefore) && !$hiddenLevel) {
   2452                     $rawText = preg_replace("/\s+/s", " ", html_entity_decode($elementBefore, ENT_QUOTES, "UTF-8"));
   2453                     if (is_string_empty($elementStart) && in_array(strtolower($elementName), $elementsBlock)) $rawText = rtrim($rawText)." ";
   2454                     if (substru($rawText, 0, 1)==" " && (is_string_empty($output) || substru($output, -1)==" ")) $rawText = ltrim($rawText);
   2455                     $output .= $this->getTextTruncated($rawText, $lengthMax);
   2456                     $lengthMax -= strlenu($rawText);
   2457                 }
   2458                 if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) {
   2459                     $output .= $endMarkerText;
   2460                     $lengthMax = 0;
   2461                 }
   2462                 if ($lengthMax<=0 || !$elementFound) break;
   2463                 if ($hiddenLevel>0 ||
   2464                     preg_match("/aria-hidden=\"true\"/i", $elementAttributes) ||
   2465                     preg_match("/role=\"doc-noteref\"/i", $elementAttributes)) {
   2466                     if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) {
   2467                         if (is_string_empty($elementStart)) {
   2468                             ++$hiddenLevel;
   2469                         } else {
   2470                             --$hiddenLevel;
   2471                         }
   2472                     }
   2473                 }
   2474                 $offsetBytes = $matches[0][1] + strlenb($matches[0][0]);
   2475             }
   2476             $output = preg_replace("/\s+\…$/s", "…", $output);
   2477         } else {
   2478             $elementsOpen = array();
   2479             $offsetBytes = 0;
   2480             while (true) {
   2481                 $elementFound = preg_match("/&.*?\;|<(\/?)([\!\?\w]+)(.*?)(\/?)>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes);
   2482                 $elementBefore = $elementFound ? substrb($text, $offsetBytes, $matches[0][1] - $offsetBytes) : substrb($text, $offsetBytes);
   2483                 $elementRawData = isset($matches[0][0]) ? $matches[0][0] : "";
   2484                 $elementStart = isset($matches[1][0]) ? $matches[1][0] : "";
   2485                 $elementName = isset($matches[2][0]) ? $matches[2][0] : "";
   2486                 $elementEnd = isset($matches[4][0]) ? $matches[4][0] : "";
   2487                 if (!is_string_empty($elementBefore)) {
   2488                     $output .= $this->getTextTruncated($elementBefore, $lengthMax);
   2489                     $lengthMax -= strlenu($elementBefore);
   2490                 }
   2491                 if (!is_string_empty($elementRawData) && $elementRawData==$endMarker) {
   2492                     $output .= $endMarkerText;
   2493                     $lengthMax = 0;
   2494                 }
   2495                 if ($lengthMax<=0 || !$elementFound) break;
   2496                 if (!is_string_empty($elementName) && is_string_empty($elementEnd) && !in_array(strtolower($elementName), $elementsVoid)) {
   2497                     if (is_string_empty($elementStart)) {
   2498                         array_push($elementsOpen, $elementName);
   2499                     } else {
   2500                         array_pop($elementsOpen);
   2501                     }
   2502                 }
   2503                 $output .= $elementRawData;
   2504                 if ($elementRawData[0]=="&") --$lengthMax;
   2505                 $offsetBytes = $matches[0][1] + strlenb($matches[0][0]);
   2506             }
   2507             $output = preg_replace("/\s+\…$/s", "…", $output);
   2508             for ($i=count($elementsOpen)-1; $i>=0; --$i) {
   2509                 $output .= "</".$elementsOpen[$i].">";
   2510             }
   2511         }
   2512         return trim($output);
   2513     }
   2514     
   2515     // Create title from text
   2516     public function createTextTitle($text) {
   2517         if (preg_match("/^.*\/([\pL\d\-\_]+)/u", $text, $matches)) $text = str_replace("-", " ", ucfirst($matches[1]));
   2518         return $text;
   2519     }
   2520 
   2521     // Create random text for cryptography
   2522     public function createSalt($length, $bcryptFormat = false) {
   2523         $dataBuffer = $salt = "";
   2524         $dataBufferSize = $bcryptFormat ? intval(ceil($length/4) * 3) : intval(ceil($length/2));
   2525         if (is_string_empty($dataBuffer) && function_exists("random_bytes")) {
   2526             $dataBuffer = @random_bytes($dataBufferSize);
   2527         }
   2528         if (is_string_empty($dataBuffer) && function_exists("openssl_random_pseudo_bytes")) {
   2529             $dataBuffer = @openssl_random_pseudo_bytes($dataBufferSize);
   2530         }
   2531         if (strlenb($dataBuffer)==$dataBufferSize) {
   2532             if ($bcryptFormat) {
   2533                 $salt = substrb(base64_encode($dataBuffer), 0, $length);
   2534                 $base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
   2535                 $bcrypt64Chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
   2536                 $salt = strtr($salt, $base64Chars, $bcrypt64Chars);
   2537             } else {
   2538                 $salt = substrb(bin2hex($dataBuffer), 0, $length);
   2539             }
   2540         }
   2541         return $salt;
   2542     }
   2543     
   2544     // Create hash with random salt, bcrypt or sha256
   2545     public function createHash($text, $algorithm, $cost = 0) {
   2546         $hash = "";
   2547         switch ($algorithm) {
   2548             case "bcrypt":  $prefix = sprintf("$2y$%02d$", $cost);
   2549                             $salt = $this->createSalt(22, true);
   2550                             $hash = crypt($text, $prefix.$salt);
   2551                             if (is_string_empty($salt) || strlenb($hash)!=60) $hash = "";
   2552                             break;
   2553             case "sha256":  $prefix = "$5y$";
   2554                             $salt = $this->createSalt(32);
   2555                             $hash = "$prefix$salt".hash("sha256", $salt.$text);
   2556                             if (is_string_empty($salt) || strlenb($hash)!=100) $hash = "";
   2557                             break;
   2558         }
   2559         return $hash;
   2560     }
   2561     
   2562     // Verify that text matches hash
   2563     public function verifyHash($text, $algorithm, $hash) {
   2564         $hashCalculated = "";
   2565         switch ($algorithm) {
   2566             case "bcrypt":  if (substrb($hash, 0, 4)=="$2y$" || substrb($hash, 0, 4)=="$2a$") {
   2567                                 $hashCalculated = crypt($text, $hash);
   2568                             }
   2569                             break;
   2570             case "sha256":  if (substrb($hash, 0, 4)=="$5y$") {
   2571                                 $prefix = "$5y$";
   2572                                 $salt = substrb($hash, 4, 32);
   2573                                 $hashCalculated = "$prefix$salt".hash("sha256", $salt.$text);
   2574                             }
   2575                             break;
   2576         }
   2577         return $this->verifyToken($hashCalculated, $hash);
   2578     }
   2579     
   2580     // Verify that token is not empty and identical, timing attack safe string comparison
   2581     public function verifyToken($tokenExpected, $tokenReceived) {
   2582         $ok = false;
   2583         $lengthExpected = strlenb($tokenExpected);
   2584         $lengthReceived = strlenb($tokenReceived);
   2585         if ($lengthExpected!=0 && $lengthReceived!=0) {
   2586             $ok = $lengthExpected==$lengthReceived;
   2587             for ($i=0; $i<$lengthReceived; ++$i) {
   2588                 $ok &= $tokenExpected[$i<$lengthExpected ? $i : 0]==$tokenReceived[$i];
   2589             }
   2590         }
   2591         return $ok;
   2592     }
   2593     
   2594     // Return meta data from raw data
   2595     public function getMetaData($rawData, $key) {
   2596         $value = "";
   2597         if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) {
   2598             $key = lcfirst($key);
   2599             foreach ($this->getTextLines($parts[2]) as $line) {
   2600                 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
   2601                     if (lcfirst($matches[1])==$key && !is_string_empty($matches[2])) {
   2602                         $value = $matches[2];
   2603                         break;
   2604                     }
   2605                 }
   2606             }
   2607         }
   2608         return $value;
   2609     }
   2610     
   2611     // Set meta data in raw data
   2612     public function setMetaData($rawData, $key, $value) {
   2613         if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) {
   2614             $found = false;
   2615             $key = lcfirst($key);
   2616             $rawDataMiddle = "";
   2617             foreach ($this->getTextLines($parts[2]) as $line) {
   2618                 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
   2619                     if (lcfirst($matches[1])==$key) {
   2620                         $rawDataMiddle .= "$matches[1]: $value\n";
   2621                         $found = true;
   2622                         continue;
   2623                     }
   2624                 }
   2625                 $rawDataMiddle .= $line;
   2626             }
   2627             if (!$found) $rawDataMiddle .= (strposu($key, "/") ? $key : ucfirst($key)).": $value\n";
   2628             $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3];
   2629         } else {
   2630             $rawDataNew = $rawData;
   2631         }
   2632         return $rawDataNew;
   2633     }
   2634     
   2635     // Remove meta data in raw data
   2636     public function unsetMetaData($rawData, $key) {
   2637         if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) {
   2638             $key = lcfirst($key);
   2639             $rawDataMiddle = "";
   2640             foreach ($this->getTextLines($parts[2]) as $line) {
   2641                 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
   2642                     if (lcfirst($matches[1])==$key) continue;
   2643                 }
   2644                 $rawDataMiddle .= $line;
   2645             }
   2646             $rawDataNew = $parts[1]."---\n".$rawDataMiddle."---\n".$parts[3];
   2647         } else {
   2648             $rawDataNew = $rawData;
   2649         }
   2650         return $rawDataNew;
   2651     }
   2652     
   2653     // Return troubleshooting URL
   2654     public function getTroubleshootingUrl() {
   2655         return "https://datenstrom.se/yellow/help/troubleshooting";
   2656     }
   2657 
   2658     // Detect server URL
   2659     public function detectServerUrl() {
   2660         $scheme = "http";
   2661         if ($this->getServer("REQUEST_SCHEME")=="https" || $this->getServer("HTTPS")=="on") $scheme = "https";
   2662         if ($this->getServer("HTTP_X_FORWARDED_PROTO")=="https") $scheme = "https";
   2663         $address = $this->getServer("SERVER_NAME");
   2664         $port = $this->getServer("SERVER_PORT");
   2665         if ($port!=80 && $port!=443) $address .= ":$port";
   2666         $base = "";
   2667         if (preg_match("/^(.*)\/.*\.php$/", $this->getServer("SCRIPT_NAME"), $matches)) $base = $matches[1];
   2668         return "$scheme://$address$base/";
   2669     }
   2670     
   2671     // Detect server location
   2672     public function detectServerLocation() {
   2673         if (isset($_SERVER["REQUEST_URI"])) {
   2674             $location = $_SERVER["REQUEST_URI"];
   2675             $location = rawurldecode(($pos = strposu($location, "?")) ? substru($location, 0, $pos) : $location);
   2676             $location = $this->yellow->lookup->normalisePath($location);
   2677             if (substru($location, 0, 1)!="/") $location = "/".$location;
   2678             $separator = $this->getLocationArgumentsSeparator();
   2679             if (preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches)) {
   2680                 $_SERVER["LOCATION"] = $location = $matches[1];
   2681                 $_SERVER["LOCATION_ARGUMENTS"] = $matches[2];
   2682                 foreach (explode("/", $matches[2]) as $token) {
   2683                     if (preg_match("/^(.*?)$separator(.*)$/", $token, $matches)) {
   2684                         if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) {
   2685                             $matches[1] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[1]);
   2686                             $matches[2] = str_replace(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[2]);
   2687                             $_REQUEST[$matches[1]] = $matches[2];
   2688                         }
   2689                     }
   2690                 }
   2691             } else {
   2692                 $_SERVER["LOCATION"] = $location;
   2693                 $_SERVER["LOCATION_ARGUMENTS"] = "";
   2694             }
   2695         }
   2696         return $this->getServer("LOCATION");
   2697     }
   2698     
   2699     // Detect server sitename
   2700     public function detectServerSitename() {
   2701         $sitename = "Localhost";
   2702         if (preg_match("#^(www\.)?([\w\-]+)#", $this->getServer("SERVER_NAME"), $matches)) {
   2703             $sitename = ucfirst($matches[2]);
   2704         }
   2705         return $sitename;
   2706     }
   2707     
   2708     // Detect server timezone
   2709     public function detectServerTimezone() {
   2710         $timezone = ini_get("date.timezone");
   2711         if (is_string_empty($timezone)) {
   2712             if (PHP_OS=="Darwin") {
   2713                 if (preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1];
   2714             } else {
   2715                 if (preg_match("/^(\S+)\/(\S+)/", $this->readFile("/etc/timezone"), $matches)) $timezone = $matches[1];
   2716             }
   2717         }
   2718         if (!in_array($timezone, timezone_identifiers_list())) $timezone = "UTC";
   2719         return $timezone;
   2720     }
   2721     
   2722     // Detect server name, version and operating system
   2723     public function detectServerInformation() {
   2724         $name = "Unknown";
   2725         $version = "x.x.x";
   2726         $os = PHP_OS;
   2727         if (preg_match("/^(\S+)\/(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) {
   2728             $name = $matches[1];
   2729             $version = $matches[2];
   2730         } elseif (preg_match("/^(\S+)/", $this->getServer("SERVER_SOFTWARE"), $matches)) {
   2731             $name = $matches[1];
   2732         }
   2733         if (PHP_SAPI=="cli" || PHP_SAPI=="cli-server") {
   2734             $name = "Built-in";
   2735             $version = PHP_VERSION;
   2736         }
   2737         if (PHP_OS=="Darwin") {
   2738             $os = "Mac";
   2739         } elseif (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") {
   2740             $os = "Windows";
   2741         }
   2742         return array($name, $version, $os);
   2743     }
   2744     
   2745     // Detect browser language
   2746     public function detectBrowserLanguage($languages, $languageDefault) {
   2747         $languageFound = $languageDefault;
   2748         foreach (preg_split("/\s*,\s*/", $this->getServer("HTTP_ACCEPT_LANGUAGE")) as $text) {
   2749             list($language, $dummy) = $this->getTextList($text, ";", 2);
   2750             if (!is_string_empty($language) && in_array($language, $languages)) {
   2751                 $languageFound = $language;
   2752                 break;
   2753             }
   2754         }
   2755         return $languageFound;
   2756     }
   2757     
   2758     // Detect terminal width and height
   2759     public function detectTerminalInformation() {
   2760         $width = $height = 0;
   2761         if (strtoupperu(substru(PHP_OS, 0, 3))=="WIN") {
   2762             exec("powershell \$Host.UI.RawUI.WindowSize.Width", $outputLines, $returnStatus);
   2763             if ($returnStatus==0 && !is_array_empty($outputLines)) {
   2764                 $width = intval(end($outputLines));
   2765             }
   2766             exec("powershell \$Host.UI.RawUI.WindowSize.Height", $outputLines, $returnStatus);
   2767             if ($returnStatus==0 && !is_array_empty($outputLines)) {
   2768                 $height = intval(end($outputLines));
   2769             }
   2770         } else {
   2771             exec("stty size", $outputLines, $returnStatus);
   2772             if ($returnStatus==0 && preg_match("/^(\d+)\s+(\d+)/", implode("\n", $outputLines), $matches)) {
   2773                 $width = intval($matches[2]);
   2774                 $height = intval($matches[1]);
   2775             }
   2776         }
   2777         return array($width, $height);
   2778     }
   2779     
   2780     // Detect image width, height, orientation and type for GIF/JPEG/PNG/SVG
   2781     public function detectImageInformation($fileName, $fileType = "") {
   2782         $width = $height = $orientation = 0;
   2783         $type = "";
   2784         $fileHandle = @fopen($fileName, "rb");
   2785         if ($fileHandle) {
   2786             if (is_string_empty($fileType)) $fileType = $this->getFileType($fileName);
   2787             if ($fileType=="gif") {
   2788                 $dataSignature = fread($fileHandle, 6);
   2789                 $dataHeader = fread($fileHandle, 7);
   2790                 if (!feof($fileHandle) && ($dataSignature=="GIF87a" || $dataSignature=="GIF89a")) {
   2791                     $width = (ord($dataHeader[1])<<8) + ord($dataHeader[0]);
   2792                     $height = (ord($dataHeader[3])<<8) + ord($dataHeader[2]);
   2793                     $type = $fileType;
   2794                 }
   2795             } elseif ($fileType=="jpeg" || $fileType=="jpg") {
   2796                 $dataBufferSizeMax = filesize($fileName);
   2797                 $dataBufferSize = min($dataBufferSizeMax, 4096);
   2798                 if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize);
   2799                 $dataSignature = substrb($dataBuffer, 0, 2);
   2800                 if (!feof($fileHandle) && $dataSignature=="\xff\xd8") {
   2801                     for ($pos=2; $pos+8<$dataBufferSize; $pos+=$length) {
   2802                         if ($dataBuffer[$pos]!="\xff") break;
   2803                         $dataMarker = $dataBuffer[$pos+1];
   2804                         if ($dataMarker=="\xe1") {
   2805                             $orientation = $this->getImageOrientationFromBuffer($dataBuffer, $pos+4, $dataBufferSize);
   2806                         }
   2807                         if (($dataMarker>="\xc0" && $dataMarker<="\xc3") ||
   2808                             ($dataMarker>="\xc5" && $dataMarker<="\xc7") ||
   2809                             ($dataMarker>="\xc9" && $dataMarker<="\xcb") ||
   2810                             ($dataMarker>="\xcd" && $dataMarker<="\xcf")) {
   2811                             $width = (ord($dataBuffer[$pos+7])<<8) + ord($dataBuffer[$pos+8]);
   2812                             $height = (ord($dataBuffer[$pos+5])<<8) + ord($dataBuffer[$pos+6]);
   2813                             $type = "jpeg";
   2814                             break;
   2815                         }
   2816                         $length = (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]) + 2;
   2817                         while ($pos+$length+8>=$dataBufferSize) {
   2818                             if ($dataBufferSize==$dataBufferSizeMax) break;
   2819                             $dataBufferDiff = min($dataBufferSizeMax, $dataBufferSize*2) - $dataBufferSize;
   2820                             $dataBufferSize += $dataBufferDiff;
   2821                             $dataBufferChunk = fread($fileHandle, $dataBufferDiff);
   2822                             if (feof($fileHandle) || $dataBufferChunk===false) {
   2823                                 $dataBufferSize = 0;
   2824                                 break;
   2825                             }
   2826                             $dataBuffer .= $dataBufferChunk;
   2827                         }
   2828                     }
   2829                 }
   2830             } elseif ($fileType=="png") {
   2831                 $dataSignature = fread($fileHandle, 8);
   2832                 $dataHeader = fread($fileHandle, 16);
   2833                 if (!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n") {
   2834                     $width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]);
   2835                     $height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]);
   2836                     $type = $fileType;
   2837                 }
   2838             } elseif ($fileType=="svg") {
   2839                 $dataBufferSizeMax = filesize($fileName);
   2840                 $dataBufferSize = min($dataBufferSizeMax, 4096);
   2841                 if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize);
   2842                 if (!feof($fileHandle) && preg_match("/<svg(\s.*?)>/s", $dataBuffer, $matches)) {
   2843                     if (preg_match("/\swidth=\"(\d+)\"/s", $matches[1], $tokens)) $width = $tokens[1];
   2844                     if (preg_match("/\sheight=\"(\d+)\"/s", $matches[1], $tokens)) $height = $tokens[1];
   2845                     $type = $fileType;
   2846                 }
   2847             }
   2848             fclose($fileHandle);
   2849         }
   2850         return array($width, $height, $orientation, $type);
   2851     }
   2852     
   2853     // Return image orientation from Exif
   2854     public function getImageOrientationFromBuffer($dataBuffer, $pos, $size) {
   2855         $orientation = 0;
   2856         $dataSignature = substrb($dataBuffer, $pos, 6);
   2857         if ($dataSignature=="\x45\x78\x69\x66\x00\x00" && $pos+14<=$size) {
   2858             $startPos = $pos+6;
   2859             $bigEndian = $dataBuffer[$startPos]=="M";
   2860             $ifdOffset = $this->getLongFromBuffer($dataBuffer, $startPos+4, $bigEndian);
   2861             $ifdStartPos = $startPos+$ifdOffset;
   2862             $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0;
   2863             $pos = $ifdStartPos+2;
   2864             while ($ifdCount && $pos+12<=$size) {
   2865                 $ifdTag = $this->getShortFromBuffer($dataBuffer, $pos, $bigEndian);
   2866                 $ifdFormat = $this->getShortFromBuffer($dataBuffer, $pos+2, $bigEndian);
   2867                 if ($ifdTag==0x8769 && $ifdFormat==4) {
   2868                     $ifdOffset = $this->getLongFromBuffer($dataBuffer, $pos+8, $bigEndian);
   2869                     $ifdStartPos = $startPos+$ifdOffset;
   2870                     $ifdCount = $ifdStartPos+2<=$size ? $this->getShortFromBuffer($dataBuffer, $ifdStartPos, $bigEndian) : 0;
   2871                     $pos = $ifdStartPos+2;
   2872                     continue;
   2873                 }
   2874                 if ($ifdTag==0x0112 && $ifdFormat==3) {
   2875                     $orientation = $this->getShortFromBuffer($dataBuffer, $pos+8, $bigEndian);
   2876                     break;
   2877                 }
   2878                 --$ifdCount;
   2879                 $pos += 12;
   2880             }
   2881         }
   2882         return $orientation;
   2883     }
   2884     
   2885     // Return unsigned short value from buffer
   2886     public  function getShortFromBuffer($dataBuffer, $pos, $bigEndian) {
   2887         if ($bigEndian) {
   2888             $value = (ord($dataBuffer[$pos])<<8) + ord($dataBuffer[$pos+1]);
   2889         } else {
   2890             $value = (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]);
   2891         }
   2892         return $value;
   2893     }
   2894     
   2895     // Return unsigned long value from buffer
   2896     public function getLongFromBuffer($dataBuffer, $pos, $bigEndian) {
   2897         if ($bigEndian) {
   2898             $value = (ord($dataBuffer[$pos])<<24) + (ord($dataBuffer[$pos+1])<<16) +
   2899                 (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]);
   2900         } else {
   2901             $value = (ord($dataBuffer[$pos+3])<<24) + (ord($dataBuffer[$pos+2])<<16) +
   2902                 (ord($dataBuffer[$pos+1])<<8) + ord($dataBuffer[$pos]);
   2903         }
   2904         return $value;
   2905     }
   2906     
   2907     // Return possible values
   2908     public function enumerate($action, $context = "") {
   2909         $values = array();
   2910         foreach ($this->yellow->extension->data as $key=>$value) {
   2911             if (method_exists($value["object"], "onEnumerate")) {
   2912                 $output = $value["object"]->onEnumerate($action, $context);
   2913                 if (!is_null($output))  {
   2914                     $values = array_merge($values, is_array($output) ? $output : array($output));
   2915                 }
   2916             }
   2917         }
   2918         if ($action=="email") {
   2919             foreach ($this->yellow->user->settings as $userKey=>$userValue) {
   2920                 array_push($values, $userKey);
   2921             }
   2922         } elseif ($action=="language") {
   2923             foreach ($this->yellow->language->settings as $languageKey=>$languageValue) {
   2924                 array_push($values, $languageKey);
   2925             }
   2926         } elseif ($action=="theme") {
   2927             $path = $this->yellow->system->get("coreThemeDirectory");
   2928             foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false, false) as $entry) {
   2929                 array_push($values, substru($entry, 0, -4));
   2930             }
   2931         }
   2932         usort($values, "strnatcasecmp");
   2933         return $values;
   2934     }
   2935 
   2936     // Send email message
   2937     public function mail($action, $headers, $message) {
   2938         $statusCode = 0;
   2939         foreach ($this->yellow->extension->data as $key=>$value) {
   2940             if (method_exists($value["object"], "onMail")) {
   2941                 $statusCode = $value["object"]->onMail($action, $headers, $message);
   2942                 if ($statusCode!=0) break;
   2943             }
   2944         }
   2945         if ($statusCode==0) {
   2946             $text = $this->yellow->lookup->normaliseHeaders($headers, "mime");
   2947             $to = $subject = $remaining = $key = "";
   2948             foreach (preg_split("/\r\n/", $text) as $line) {
   2949                 if (preg_match("/^(.*?):\s*(.*?)$/", $line, $matches) && !is_string_empty($matches[1])) {
   2950                     $key = $matches[1];
   2951                     $fragment = $matches[2];
   2952                 } else {
   2953                     $fragment = $line;
   2954                 }
   2955                 if ($key=="To") { $to .= $fragment; continue; }
   2956                 if ($key=="Subject") { $subject .= $fragment; continue; }
   2957                 $remaining .= $line."\r\n";
   2958             }
   2959             $statusCode = mail($to, $subject, $message, $remaining) ? 200 : 500;
   2960         }
   2961         return $statusCode==200;
   2962     }
   2963 
   2964     // Write information to log file
   2965     public function log($action, $message) {
   2966         $statusCode = 0;
   2967         foreach ($this->yellow->extension->data as $key=>$value) {
   2968             if (method_exists($value["object"], "onLog")) {
   2969                 $statusCode = $value["object"]->onLog($action, $message);
   2970                 if ($statusCode!=0) break;
   2971             }
   2972         }
   2973         if ($statusCode==0 && $this->yellow->system->get("coreWebsiteFile")!="none") {
   2974             $line = date("Y-m-d H:i:s")." ".trim($action)." ".trim($message)."\n";
   2975             $this->appendFile($this->yellow->system->get("coreServerInstallDirectory").
   2976                 $this->yellow->system->get("coreExtensionDirectory").
   2977                 $this->yellow->system->get("coreWebsiteFile"), $line);
   2978         }
   2979     }
   2980 
   2981     // Start timer
   2982     public function timerStart(&$time) {
   2983         $time = microtime(true);
   2984     }
   2985     
   2986     // Stop timer and calculate elapsed time in milliseconds
   2987     public function timerStop(&$time) {
   2988         $time = intval((microtime(true)-$time) * 1000);
   2989     }
   2990     
   2991     // Check if there are location arguments in current HTTP request
   2992     public function isLocationArguments($location = "") {
   2993         if (is_string_empty($location)) $location = $this->getServer("LOCATION").$this->getServer("LOCATION_ARGUMENTS");
   2994         $separator = $this->getLocationArgumentsSeparator();
   2995         return preg_match("/[^\/]+$separator.*$/", $location);
   2996     }
   2997     
   2998     // Check if there are pagination arguments in current HTTP request
   2999     public function isLocationArgumentsPagination($location) {
   3000         $separator = $this->getLocationArgumentsSeparator();
   3001         return preg_match("/^(.*\/)?page$separator\d+$/", $location);
   3002     }
   3003 
   3004     // Check if unmodified since last HTTP request
   3005     public function isNotModified($lastModifiedFormatted) {
   3006         return $this->getServer("HTTP_IF_MODIFIED_SINCE")==$lastModifiedFormatted;
   3007     }
   3008 }
   3009 
   3010 class YellowPage {
   3011     public $yellow;                 // access to API
   3012     public $scheme;                 // server scheme
   3013     public $address;                // server address
   3014     public $base;                   // base location
   3015     public $location;               // page location
   3016     public $fileName;               // content file name
   3017     public $rawData;                // raw data of page
   3018     public $metaDataOffsetBytes;    // meta data offset
   3019     public $metaData;               // meta data
   3020     public $pageCollections;        // additional pages
   3021     public $sharedPages;            // shared pages
   3022     public $headerData;             // response header
   3023     public $outputData;             // response output
   3024     public $parser;                 // content parser
   3025     public $parserData;             // content data of page
   3026     public $statusCode;             // status code
   3027     public $errorMessage;           // error message
   3028     public $lastModified;           // last modification date
   3029     public $available;              // page is available? (boolean)
   3030     public $visible;                // page is visible location? (boolean)
   3031     public $cacheable;              // page is cacheable? (boolean)
   3032 
   3033     public function __construct($yellow) {
   3034         $this->yellow = $yellow;
   3035         $this->scheme = "";
   3036         $this->address = "";
   3037         $this->base = "";
   3038         $this->location = "";
   3039         $this->fileName = "";
   3040         $this->metaData = new YellowArray();
   3041         $this->pageCollections = array();
   3042         $this->sharedPages = array();
   3043         $this->headerData = array();
   3044     }
   3045 
   3046     // Set request information
   3047     public function setRequestInformation($scheme, $address, $base, $location, $fileName, $cacheable) {
   3048         $this->scheme = $scheme;
   3049         $this->address = $address;
   3050         $this->base = $base;
   3051         $this->location = $location;
   3052         $this->fileName = $fileName;
   3053         $this->cacheable = $cacheable;
   3054     }
   3055     
   3056     // Parse page meta
   3057     public function parseMeta($rawData, $statusCode = 0, $errorMessage = "") {
   3058         $this->rawData = $rawData;
   3059         $this->parser = null;
   3060         $this->parserData = "";
   3061         $this->statusCode = $statusCode;
   3062         $this->errorMessage = $errorMessage;
   3063         $this->lastModified = 0;
   3064         $this->available = true;
   3065         $this->visible = true;
   3066         $this->parseMetaData();
   3067     }
   3068     
   3069     // Parse page meta update
   3070     public function parseMetaUpdate() {
   3071         if ($this->statusCode==0) {
   3072             $this->rawData = $this->yellow->toolbox->readFile($this->fileName);
   3073             $this->statusCode = 200;
   3074             $this->parseMetaData();
   3075         }
   3076     }
   3077     
   3078     // Parse page meta data
   3079     public function parseMetaData() {
   3080         $this->metaData = new YellowArray();
   3081         $this->metaDataOffsetBytes = 0;
   3082         if (!is_null($this->rawData)) {
   3083             $this->set("title", $this->yellow->toolbox->createTextTitle($this->location));
   3084             $this->set("language", $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language")));
   3085             $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName)));
   3086             $this->parseMetaDataRaw(array("sitename", "author", "layout", "theme", "parser", "status"));
   3087             $this->parseMetaDataShared();
   3088             $titleHeader = ($this->location==$this->yellow->content->getHomeLocation($this->location)) ?
   3089                 $this->get("sitename") : $this->get("title")." - ".$this->get("sitename");
   3090             if (!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title"));
   3091             if (!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title"));
   3092             if (!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader);
   3093             if ($this->yellow->lookup->isRootLocation($this->location) || !is_readable($this->fileName)) $this->available = false;
   3094             if ($this->get("status")=="shared") $this->available = false;
   3095             if ($this->get("status")=="unlisted") $this->visible = false;
   3096         } else {
   3097             $this->set("size", $this->yellow->toolbox->getFileSize($this->fileName));
   3098             $this->set("type", $this->yellow->toolbox->getFileType($this->fileName));
   3099             $this->set("group", $this->yellow->toolbox->getFileGroup($this->fileName, $this->yellow->system->get("coreMediaDirectory")));
   3100             $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName)));
   3101             if (!$this->yellow->lookup->isFileLocation($this->location)) $this->available = false;
   3102         }
   3103         foreach ($this->yellow->extension->data as $key=>$value) {
   3104             if (method_exists($value["object"], "onParseMetaData")) $value["object"]->onParseMetaData($this);
   3105         }
   3106     }
   3107     
   3108     // Parse page meta data from raw data
   3109     public function parseMetaDataRaw($defaultKeys) {
   3110         foreach ($defaultKeys as $key) {
   3111             $value = $this->yellow->system->get($key);
   3112             if (!is_string_empty($key) && !is_string_empty($value)) $this->set($key, $value);
   3113         }
   3114         if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+/s", $this->rawData, $parts)) {
   3115             $this->metaDataOffsetBytes = strlenb($parts[0]);
   3116             foreach (preg_split("/[\r\n]+/", $parts[2]) as $line) {
   3117                 if (preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches)) {
   3118                     if (!is_string_empty($matches[1]) && !is_string_empty($matches[2])) $this->set($matches[1], $matches[2]);
   3119                 }
   3120             }
   3121         } elseif (preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parts)) {
   3122             $this->metaDataOffsetBytes = strlenb($parts[0]);
   3123             $this->set("title", $parts[2]);
   3124         }
   3125     }
   3126     
   3127     // Parse page meta data for shared pages
   3128     public function parseMetaDataShared() {
   3129         $this->sharedPages["main"] = $this;
   3130         if (!$this->yellow->lookup->isSharedLocation($this->location) && $this->statusCode!=0) {
   3131             foreach ($this->yellow->content->getShared($this->location) as $page) {
   3132                 $this->sharedPages[basename($page->location)] = $page;
   3133                 $page->sharedPages["main"] = $this;
   3134             }
   3135         }
   3136         if ($this->yellow->lookup->isSharedLocation($this->location)) {
   3137             $this->set("status", "shared");
   3138         }
   3139     }
   3140     
   3141     // Parse page content on demand
   3142     public function parseContent() {
   3143         if (!is_null($this->rawData) && !is_object($this->parser)) {
   3144             if ($this->yellow->extension->isExisting($this->get("parser"))) {
   3145                 $value = $this->yellow->extension->data[$this->get("parser")];
   3146                 if (method_exists($value["object"], "onParseContentRaw")) {
   3147                     $this->parser = $value["object"];
   3148                     $this->parserData = $this->getContentRaw();
   3149                     $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData);
   3150                     foreach ($this->yellow->extension->data as $key=>$value) {
   3151                         if (method_exists($value["object"], "onParseContentHtml")) {
   3152                             $output = $value["object"]->onParseContentHtml($this, $this->parserData);
   3153                             if (!is_null($output)) $this->parserData = $output;
   3154                         }
   3155                     }
   3156                 }
   3157             } else {
   3158                 $this->parserData = $this->getContentRaw();
   3159                 $this->parserData = preg_replace("/\[yellow error\]/i", $this->errorMessage, $this->parserData);
   3160             }
   3161             if (!$this->isExisting("description")) {
   3162                 $description = $this->yellow->toolbox->createTextDescription($this->parserData, 150);
   3163                 $this->set("description", !is_string_empty($description) ? $description : $this->get("title"));
   3164             }
   3165             if ($this->yellow->system->get("coreDebugMode")>=2) {
   3166                 echo "YellowPage::parseContent file:".$this->fileName." parser:".$this->get("parser")."<br />\n";
   3167             }
   3168         }
   3169     }
   3170     
   3171     // Parse page content element, experimental
   3172     public function parseContentElement($name, $text, $attributes, $type) {
   3173         $output = null;
   3174         foreach ($this->yellow->extension->data as $key=>$value) {
   3175             if (method_exists($value["object"], "onParseContentElement")) {
   3176                 $output = $value["object"]->onParseContentElement($this, $name, $text, $attributes, $type);
   3177                 if (!is_null($output)) break;
   3178             }
   3179         }
   3180         if (is_null($output)) {
   3181             if ($name=="yellow" && $type=="inline" && $text=="error") {
   3182                 $output = $this->errorMessage;
   3183             }
   3184         }
   3185         if ($this->yellow->system->get("coreDebugMode")>=3 && !is_string_empty($name)) {
   3186             echo "YellowPage::parseContentElement name:$name type:$type<br />\n";
   3187         }
   3188         return $output;
   3189     }
   3190     
   3191     // Parse page
   3192     public function parsePage() {
   3193         $this->parsePageLayout($this->get("layout"));
   3194         if (!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, no-store");
   3195         if (!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8");
   3196         if (!$this->isHeader("Last-Modified")) $this->setHeader("Last-Modified", $this->getLastModified(true));
   3197         $theme = $this->yellow->lookup->normaliseName($this->get("theme"));
   3198         if (!is_file($this->yellow->system->get("coreThemeDirectory").$theme.".css") &&
   3199             !in_array($theme, $this->yellow->toolbox->enumerate("theme"))) {
   3200             $this->error(500, "Theme '".$this->get("theme")."' does not exist!");
   3201         }
   3202         if (!$this->yellow->language->isExisting($this->get("language"))) {
   3203             $this->error(500, "Language '".$this->get("language")."' does not exist!");
   3204         }
   3205         if (!is_object($this->parser)) {
   3206             $this->error(500, "Parser '".$this->get("parser")."' does not exist!");
   3207         }
   3208         if ($this->yellow->lookup->isNestedLocation($this->location, $this->fileName, true)) {
   3209             $this->error(500, "Folder '".dirname($this->fileName)."' may not contain subfolders!");
   3210         }
   3211         if ($this->yellow->lookup->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200) {
   3212             $location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->location);
   3213             $location = $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, "", $location);
   3214             $this->status(301, $location);
   3215         }
   3216         if ($this->yellow->lookup->getRequestHandler()=="core" && !$this->isAvailable() && $this->statusCode==200) {
   3217             $this->error(404);
   3218         }
   3219         if ($this->isExisting("pageClean")) $this->outputData = null;
   3220         foreach ($this->yellow->extension->data as $key=>$value) {
   3221             if (method_exists($value["object"], "onParsePageOutput")) {
   3222                 $output = $value["object"]->onParsePageOutput($this, $this->outputData);
   3223                 if (!is_null($output)) $this->outputData = $output;
   3224             }
   3225         }
   3226     }
   3227     
   3228     // Parse page layout
   3229     public function parsePageLayout($name) {
   3230         $this->outputData = null;
   3231         foreach ($this->yellow->extension->data as $key=>$value) {
   3232             if (method_exists($value["object"], "onParsePageLayout")) {
   3233                 $value["object"]->onParsePageLayout($this, $name);
   3234             }
   3235         }
   3236         if (is_null($this->outputData)) {
   3237             ob_start();
   3238             $this->includeLayout($name);
   3239             $this->outputData = ob_get_contents();
   3240             ob_end_clean();
   3241         }
   3242     }
   3243     
   3244     // Include page layout
   3245     public function includeLayout($name) {
   3246         $fileNameLayoutNormal = $this->yellow->system->get("coreLayoutDirectory").$this->yellow->lookup->normaliseName($name).".html";
   3247         $fileNameLayoutTheme = $this->yellow->system->get("coreLayoutDirectory").
   3248             $this->yellow->lookup->normaliseName($this->get("theme"))."-".$this->yellow->lookup->normaliseName($name).".html";
   3249         if (is_file($fileNameLayoutTheme)) {
   3250             if ($this->yellow->system->get("coreDebugMode")>=2) {
   3251                 echo "YellowPage::includeLayout file:$fileNameLayoutTheme<br />\n";
   3252             }
   3253             $this->setLastModified(filemtime($fileNameLayoutTheme));
   3254             require($fileNameLayoutTheme);
   3255         } elseif (is_file($fileNameLayoutNormal)) {
   3256             if ($this->yellow->system->get("coreDebugMode")>=2) {
   3257                 echo "YellowPage::includeLayout file:$fileNameLayoutNormal<br />\n";
   3258             }
   3259             $this->setLastModified(filemtime($fileNameLayoutNormal));
   3260             require($fileNameLayoutNormal);
   3261         } else {
   3262             $this->error(500, "Layout '$name' does not exist!");
   3263             echo "Layout error<br />\n";
   3264         }
   3265     }
   3266     
   3267     // Set page setting
   3268     public function set($key, $value) {
   3269         $this->metaData[$key] = $value;
   3270     }
   3271     
   3272     // Return page setting
   3273     public function get($key) {
   3274         return $this->isExisting($key) ? $this->metaData[$key] : "";
   3275     }
   3276 
   3277     // Return page setting, HTML encoded
   3278     public function getHtml($key) {
   3279         return htmlspecialchars($this->get($key));
   3280     }
   3281     
   3282     // Return page setting as language specific date
   3283     public function getDate($key, $format = "") {
   3284         if (!is_string_empty($format)) {
   3285             $format = $this->yellow->language->getText($format);
   3286         } else {
   3287             $format = $this->yellow->language->getText("coreDateFormatMedium");
   3288         }
   3289         return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format);
   3290     }
   3291 
   3292     // Return page setting as language specific date, HTML encoded
   3293     public function getDateHtml($key, $format = "") {
   3294         return htmlspecialchars($this->getDate($key, $format));
   3295     }
   3296 
   3297     // Return page setting as language specific date, relative to today
   3298     public function getDateRelative($key, $format = "", $daysLimit = 30) {
   3299         if (!is_string_empty($format)) {
   3300             $format = $this->yellow->language->getText($format);
   3301         } else {
   3302             $format = $this->yellow->language->getText("coreDateFormatMedium");
   3303         }
   3304         return $this->yellow->language->getDateRelative(strtotime($this->get($key)), $format, $daysLimit);
   3305     }
   3306     
   3307     // Return page setting as language specific date, relative to today, HTML encoded
   3308     public function getDateRelativeHtml($key, $format = "", $daysLimit = 30) {
   3309         return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit));
   3310     }
   3311     
   3312     // Return page setting as date
   3313     public function getDateFormatted($key, $format) {
   3314         return $this->yellow->language->getDateFormatted(strtotime($this->get($key)), $format);
   3315     }
   3316     
   3317     // Return page setting as date, HTML encoded
   3318     public function getDateFormattedHtml($key, $format) {
   3319         return htmlspecialchars($this->getDateFormatted($key, $format));
   3320     }
   3321     
   3322     // Return page content data, raw format
   3323     public function getContentRaw() {
   3324         $this->parseMetaUpdate();
   3325         return substrb($this->rawData, $this->metaDataOffsetBytes);
   3326     }
   3327     
   3328     // Return page content data, HTML encoded or raw format
   3329     public function getContentHtml() {
   3330         $this->parseContent();
   3331         return $this->parserData;
   3332     }
   3333     
   3334     // Return page extra data, HTML encoded
   3335     public function getExtraHtml($name) {
   3336         $output = "";
   3337         foreach ($this->yellow->extension->data as $key=>$value) {
   3338             if (method_exists($value["object"], "onParsePageExtra")) {
   3339                 $outputExtension = $value["object"]->onParsePageExtra($this, $name);
   3340                 if (!is_null($outputExtension)) $output .= $outputExtension;
   3341             }
   3342         }
   3343         if ($name=="header") {
   3344             $assetLocation = $this->yellow->system->get("coreServerBase").$this->yellow->system->get("coreAssetLocation");
   3345             $fileNameTheme = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".css";
   3346             if (is_file($fileNameTheme)) {
   3347                 $fileLocation = $assetLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".css";
   3348                 $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"$fileLocation\" />\n";
   3349             }
   3350             $fileNameScript = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".js";
   3351             if (is_file($fileNameScript)) {
   3352                 $fileLocation = $assetLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".js";
   3353                 $output .= "<script type=\"text/javascript\" src=\"$fileLocation\"></script>\n";
   3354             }
   3355             $fileNameFavicon = $this->yellow->system->get("coreThemeDirectory").$this->yellow->lookup->normaliseName($this->get("theme")).".png";
   3356             if (is_file($fileNameFavicon)) {
   3357                 $fileLocation = $assetLocation.$this->yellow->lookup->normaliseName($this->get("theme")).".png";
   3358                 $output .= "<link rel=\"icon\" type=\"image/png\" href=\"$fileLocation\" />\n";
   3359             }
   3360         }
   3361         return $output;
   3362     }
   3363     
   3364     // Return parent page, null if none
   3365     public function getParent() {
   3366         $parentLocation = $this->yellow->content->getParentLocation($this->location);
   3367         return $this->yellow->content->find($parentLocation);
   3368     }
   3369     
   3370     // Return top-level parent page, null if none
   3371     public function getParentTop($homeFallback = false) {
   3372         $parentTopLocation = $this->yellow->content->getParentTopLocation($this->location);
   3373         if (!$this->yellow->content->find($parentTopLocation) && $homeFallback) {
   3374             $parentTopLocation = $this->yellow->content->getHomeLocation($this->location);
   3375         }
   3376         return $this->yellow->content->find($parentTopLocation);
   3377     }
   3378     
   3379     // Return page collection with pages on the same level
   3380     public function getSiblings($showInvisible = false) {
   3381         $parentLocation = $this->yellow->content->getParentLocation($this->location);
   3382         return $this->yellow->content->getChildren($parentLocation, $showInvisible);
   3383     }
   3384     
   3385     // Return page collection with child pages
   3386     public function getChildren($showInvisible = false) {
   3387         return $this->yellow->content->getChildren($this->location, $showInvisible);
   3388     }
   3389 
   3390     // Return page collection with child pages recursively
   3391     public function getChildrenRecursive($showInvisible = false, $levelMax = 0) {
   3392         return $this->yellow->content->getChildrenRecursive($this->location, $showInvisible, $levelMax);
   3393     }
   3394     
   3395     // Set page collection with additional pages
   3396     public function setPages($key, $pages) {
   3397         $this->pageCollections[$key] = $pages;
   3398     }
   3399 
   3400     // Return page collection with additional pages
   3401     public function getPages($key) {
   3402         return isset($this->pageCollections[$key]) ? $this->pageCollections[$key] : new YellowPageCollection($this->yellow);
   3403     }
   3404     
   3405     // Set shared page
   3406     public function setPage($key, $page) {
   3407         $this->sharedPages[$key] = $page;
   3408     }
   3409     
   3410     // Return shared page
   3411     public function getPage($key) {
   3412         return isset($this->sharedPages[$key]) ? $this->sharedPages[$key] : new YellowPage($this->yellow);
   3413     }
   3414     
   3415     // Return page URL
   3416     public function getUrl($canonicalUrl = false) {
   3417         if ($canonicalUrl) {
   3418             $scheme = $this->yellow->system->get("coreServerScheme");
   3419             $address = $this->yellow->system->get("coreServerAddress");
   3420             $location = $this->yellow->system->get("coreServerBase").$this->location;
   3421         } else {
   3422             $scheme = $this->scheme;
   3423             $address = $this->address;
   3424             $location = $this->base.$this->location;
   3425         }
   3426         return "$scheme://$address$location";
   3427     }
   3428     
   3429     // Return page base
   3430     public function getBase($multiLanguage = false) {
   3431         return $multiLanguage ? rtrim($this->base.$this->yellow->content->getHomeLocation($this->location), "/") :  $this->base;
   3432     }
   3433     
   3434     // Return page location
   3435     public function getLocation($absoluteLocation = false) {
   3436         return $absoluteLocation ? $this->base.$this->location : $this->location;
   3437     }
   3438     
   3439     // Set page request argument
   3440     public function setRequest($key, $value) {
   3441         $_REQUEST[$key] = $value;
   3442     }
   3443     
   3444     // Return page request argument
   3445     public function getRequest($key) {
   3446         return isset($_REQUEST[$key]) ? $_REQUEST[$key] : "";
   3447     }
   3448     
   3449     // Return page request argument, HTML encoded
   3450     public function getRequestHtml($key) {
   3451         return htmlspecialchars($this->getRequest($key));
   3452     }
   3453     
   3454     // Set page response header
   3455     public function setHeader($key, $value) {
   3456         $this->headerData[$key] = $value;
   3457     }
   3458     
   3459     // Return page response header
   3460     public function getHeader($key) {
   3461         return $this->isHeader($key) ? $this->headerData[$key] : "";
   3462     }
   3463     
   3464     // Set page response output
   3465     public function setOutput($output) {
   3466         $this->outputData = $output;
   3467     }
   3468     
   3469     // Return page modification date, Unix time or HTTP format
   3470     public function getModified($httpFormat = false) {
   3471         $modified = strtotime($this->get("modified"));
   3472         return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified;
   3473     }
   3474     
   3475     // Set last modification date, Unix time
   3476     public function setLastModified($modified) {
   3477         $this->lastModified = max($this->lastModified, $modified);
   3478     }
   3479     
   3480     // Return last modification date, Unix time or HTTP format
   3481     public function getLastModified($httpFormat = false) {
   3482         $lastModified = max($this->lastModified, $this->getModified(), $this->yellow->system->getModified(),
   3483             $this->yellow->language->getModified(), $this->yellow->extension->getModified());
   3484         foreach ($this->pageCollections as $pages) $lastModified = max($lastModified, $pages->getModified());
   3485         foreach ($this->sharedPages as $page) $lastModified = max($lastModified, $page->getModified());
   3486         return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($lastModified) : $lastModified;
   3487     }
   3488     
   3489     // Return raw data for error page
   3490     public function getRawDataError() {
   3491         $statusCode = $this->statusCode;
   3492         $fileNameError = $this->getFileNameError();
   3493         $languageError = $this->yellow->lookup->findContentLanguage($this->fileName, $this->yellow->system->get("language"));
   3494         if (is_file($fileNameError)) {
   3495             $rawData = $this->yellow->toolbox->readFile($fileNameError);
   3496         } elseif ($this->yellow->language->isText("coreError{$statusCode}Title", $languageError)) {
   3497             $rawData = "---\nTitle: ".$this->yellow->language->getText("coreError{$statusCode}Title", $languageError)."\n";
   3498             $rawData .= "Layout: error\n---\n".$this->yellow->language->getText("coreError{$statusCode}Text", $languageError);
   3499         } else {
   3500             $rawData = "---\nTitle:".$this->yellow->toolbox->getHttpStatusFormatted($statusCode, true)."\n";
   3501             $rawData .= "Layout:error\n---\n".$this->errorMessage;
   3502         }
   3503         return $rawData;
   3504     }
   3505     
   3506     // Return file name for error page
   3507     public function getFileNameError() {
   3508         $sharedLocation = $this->yellow->content->getHomeLocation($this->location)."shared/";
   3509         $fileNameError = $this->yellow->lookup->findFileFromContentLocation($sharedLocation, true).$this->yellow->system->get("coreContentErrorFile");
   3510         return str_replace("(.*)", $this->statusCode, $fileNameError);
   3511     }
   3512     
   3513     // Return page status code, number or HTTP format
   3514     public function getStatusCode($httpFormat = false) {
   3515         $statusCode = $this->statusCode;
   3516         if ($httpFormat) {
   3517             $statusCode = $this->yellow->toolbox->getHttpStatusFormatted($statusCode);
   3518             if (!is_string_empty($this->errorMessage)) $statusCode .= ": ".$this->errorMessage;
   3519         }
   3520         return $statusCode;
   3521     }
   3522     
   3523     // Respond with status code, no page content
   3524     public function status($statusCode, $location = "") {
   3525         if ($statusCode>0 && !$this->isExisting("pageClean")) {
   3526             $this->statusCode = $statusCode;
   3527             $this->lastModified = 0;
   3528             $this->headerData = array();
   3529             if (!is_string_empty($location)) {
   3530                 $this->setHeader("Location", $location);
   3531                 $this->setHeader("Cache-Control", "no-cache, no-store");
   3532             }
   3533             $this->set("pageClean", (string)$statusCode);
   3534         }
   3535     }
   3536     
   3537     // Respond with error page
   3538     public function error($statusCode, $errorMessage = "") {
   3539         if ($statusCode>=400 && is_string_empty($this->errorMessage)) {
   3540             $this->statusCode = $statusCode;
   3541             $this->errorMessage = is_string_empty($errorMessage) ? "Page error!" : $errorMessage;
   3542         }
   3543     }
   3544     
   3545     // Check if page is available
   3546     public function isAvailable() {
   3547         return $this->available;
   3548     }
   3549     
   3550     // Check if page is visible
   3551     public function isVisible() {
   3552         return $this->visible;
   3553     }
   3554 
   3555     // Check if page is within current HTTP request
   3556     public function isActive() {
   3557         return $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location);
   3558     }
   3559     
   3560     // Check if page is cacheable
   3561     public function isCacheable() {
   3562         return $this->cacheable;
   3563     }
   3564 
   3565     // Check if page with error
   3566     public function isError() {
   3567         return $this->statusCode>=400;
   3568     }
   3569     
   3570     // Check if page setting exists
   3571     public function isExisting($key) {
   3572         return isset($this->metaData[$key]);
   3573     }
   3574     
   3575     // Check if request argument exists
   3576     public function isRequest($key) {
   3577         return isset($_REQUEST[$key]);
   3578     }
   3579     
   3580     // Check if response header exists
   3581     public function isHeader($key) {
   3582         return isset($this->headerData[$key]);
   3583     }
   3584     
   3585     // Check if shared page exists
   3586     public function isPage($key) {
   3587         return isset($this->sharedPages[$key]);
   3588     }
   3589 }
   3590 
   3591 class YellowPageCollection extends ArrayObject {
   3592     public $yellow;                 // access to API
   3593     public $filterValue;            // current page filter value
   3594     public $paginationNumber;       // current page number in pagination
   3595     public $paginationCount;        // highest page number in pagination
   3596     
   3597     public function __construct($yellow) {
   3598         parent::__construct(array());
   3599         $this->yellow = $yellow;
   3600     }
   3601     
   3602     // Append page to end of page collection
   3603     #[\ReturnTypeWillChange]
   3604     public function append($page) {
   3605         parent::append($page);
   3606     }
   3607     
   3608     // Prepend page to start of page collection
   3609     #[\ReturnTypeWillChange]
   3610     public function prepend($page) {
   3611         $array = $this->getArrayCopy();
   3612         array_unshift($array, $page);
   3613         $this->exchangeArray($array);
   3614     }
   3615     
   3616     // Remove page from page collection
   3617     public function remove($page): YellowPageCollection {
   3618         $array = array();
   3619         $location = $page->location;
   3620         foreach ($this->getArrayCopy() as $page) {
   3621             if ($page->location!=$location) array_push($array, $page);
   3622         }
   3623         $this->exchangeArray($array);
   3624         return $this;
   3625     }
   3626     
   3627     // Filter page collection by page setting
   3628     public function filter($key, $value, $exactMatch = true): YellowPageCollection {
   3629         $array = array();
   3630         $value = str_replace(" ", "-", strtoloweru($value));
   3631         $valueLength = strlenu($value);
   3632         $this->filterValue = "";
   3633         foreach ($this->getArrayCopy() as $page) {
   3634             if ($page->isExisting($key)) {
   3635                 foreach (preg_split("/\s*,\s*/", $page->get($key)) as $pageValue) {
   3636                     $pageValueLength = $exactMatch ? strlenu($pageValue) : $valueLength;
   3637                     if ($value==substru(str_replace(" ", "-", strtoloweru($pageValue)), 0, $pageValueLength)) {
   3638                         if (is_string_empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength);
   3639                         array_push($array, $page);
   3640                         break;
   3641                     }
   3642                 }
   3643             }
   3644         }
   3645         $this->exchangeArray($array);
   3646         return $this;
   3647     }
   3648     
   3649     // Filter page collection by location or file
   3650     public function match($regex = "/.*/", $filterByLocation = true): YellowPageCollection {
   3651         $array = array();
   3652         $this->filterValue = $regex;
   3653         foreach ($this->getArrayCopy() as $page) {
   3654             if (preg_match($regex, $filterByLocation ? $page->location : $page->fileName)) array_push($array, $page);
   3655         }
   3656         $this->exchangeArray($array);
   3657         return $this;
   3658     }
   3659     
   3660     // Sort page collection by settings similarity
   3661     public function similar($page): YellowPageCollection {
   3662         $location = $page->location;
   3663         $keywords = strtoloweru($page->get("title").",".$page->get("tag").",".$page->get("author"));
   3664         $tokens = array_unique(array_filter(preg_split("/[,\s\(\)\+\-]/", $keywords), "strlen"));
   3665         if (!is_array_empty($tokens)) {
   3666             $array = array();
   3667             foreach ($this->getArrayCopy() as $page) {
   3668                 $sortScore = 0;
   3669                 foreach ($tokens as $token) {
   3670                     if (stristr($page->get("title"), $token)) $sortScore += 50;
   3671                     if (stristr($page->get("tag"), $token)) $sortScore += 5;
   3672                     if (stristr($page->get("author"), $token)) $sortScore += 2;
   3673                 }
   3674                 if ($page->location!=$location) {
   3675                     $page->set("sortScore", $sortScore);
   3676                     array_push($array, $page);
   3677                 }
   3678             }
   3679             $this->exchangeArray($array);
   3680             $this->sort("modified", false)->sort("sortScore", false);
   3681         }
   3682         return $this;
   3683     }
   3684     
   3685     // Sort page collection by page setting
   3686     public function sort($key, $ascendingOrder = true): YellowPageCollection {
   3687         $array = $this->getArrayCopy();
   3688         $sortIndex = 0;
   3689         $sortKeys = array();
   3690         foreach ($array as $page) {
   3691             $sortKeys[$page->location] = $page->get($key)." ".++$sortIndex;
   3692         }
   3693         $callback = function ($a, $b) use ($sortKeys, $ascendingOrder) {
   3694             return $ascendingOrder ?
   3695                 strnatcasecmp($sortKeys[$a->location], $sortKeys[$b->location]) :
   3696                 strnatcasecmp($sortKeys[$b->location], $sortKeys[$a->location]);
   3697         };
   3698         usort($array, $callback);
   3699         $this->exchangeArray($array);
   3700         return $this;
   3701     }
   3702     
   3703     // Group page collection by page setting, return array with multiple collections
   3704     public function group($key, $ascendingOrder = true, $format = ""): array {
   3705         $array = array();
   3706         $groupByInitial = $format=="initial";
   3707         $groupByDate = !is_string_empty($format) && $format!="count" && $format!="initial";
   3708         foreach ($this->getIterator() as $page) {
   3709             if ($page->isExisting($key)) {
   3710                 foreach (preg_split("/\s*,\s*/", $page->get($key)) as $group) {
   3711                     if ($groupByInitial) {
   3712                         $group = strtoupperu(substru($group, 0, 1));
   3713                     } elseif ($groupByDate) {
   3714                         $group = $this->yellow->language->getDateFormatted(strtotime($group), $format);
   3715                     }
   3716                     if (!is_string_empty($group)) {
   3717                         if (!isset($array[$group])) {
   3718                             $groupSearch = strtoloweru($group);
   3719                             foreach (array_keys($array) as $groupFound) {
   3720                                 if (strtoloweru($groupFound)==$groupSearch) {
   3721                                     $group = $groupFound;
   3722                                     break;
   3723                                 }
   3724                             }
   3725                             if (!isset($array[$group])) $array[$group] = new YellowPageCollection($this->yellow);
   3726                         }
   3727                         $array[$group]->append($page);
   3728                     }
   3729                 }
   3730             }
   3731         }
   3732         $callbackString = function ($a, $b) use ($ascendingOrder) {
   3733             return $ascendingOrder ? strnatcasecmp($a, $b) : strnatcasecmp($b, $a);
   3734         };
   3735         $callbackCollection = function ($a, $b) use ($ascendingOrder) {
   3736             return $ascendingOrder ? count($a)-count($b) : count($b)-count($a);
   3737         };
   3738         if ($format!="count") {
   3739             uksort($array, $callbackString);
   3740         } else {
   3741             uasort($array, $callbackCollection);
   3742         }
   3743         return $array;
   3744     }
   3745 
   3746     // Calculate union, merge page collection
   3747     public function merge($input): YellowPageCollection {
   3748         $this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input));
   3749         return $this;
   3750     }
   3751     
   3752     // Calculate intersection, remove pages that are not present in another page collection
   3753     public function intersect($input): YellowPageCollection {
   3754         $callback = function ($a, $b) {
   3755             return strcmp($a->location, $b->location);
   3756         };
   3757         $this->exchangeArray(array_uintersect($this->getArrayCopy(), (array)$input, $callback));
   3758         return $this;
   3759     }
   3760 
   3761     // Calculate difference, remove pages that are present in another page collection
   3762     public function diff($input): YellowPageCollection {
   3763         $callback = function ($a, $b) {
   3764             return strcmp($a->location, $b->location);
   3765         };
   3766         $this->exchangeArray(array_udiff($this->getArrayCopy(), (array)$input, $callback));
   3767         return $this;
   3768     }
   3769     
   3770     // Limit the number of pages in page collection
   3771     public function limit($pagesMax): YellowPageCollection {
   3772         $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax));
   3773         return $this;
   3774     }
   3775     
   3776     // Reverse page collection
   3777     public function reverse(): YellowPageCollection {
   3778         $this->exchangeArray(array_reverse($this->getArrayCopy()));
   3779         return $this;
   3780     }
   3781     
   3782     // Randomize page collection
   3783     public function shuffle(): YellowPageCollection {
   3784         $array = $this->getArrayCopy();
   3785         shuffle($array);
   3786         $this->exchangeArray($array);
   3787         return $this;
   3788     }
   3789 
   3790     // Paginate page collection
   3791     public function paginate($limit): YellowPageCollection {
   3792         if (!$this->isPagination() && $limit!=0) {
   3793             $this->paginationNumber = 1;
   3794             $this->paginationCount = ceil($this->count() / $limit);
   3795             if ($this->yellow->page->isRequest("page")) {
   3796                 $this->paginationNumber = intval($this->yellow->page->getRequest("page"));
   3797             }
   3798             if ($this->paginationNumber<0 || $this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0;
   3799             if ($this->paginationNumber) {
   3800                 $this->exchangeArray(array_slice($this->getArrayCopy(), ($this->paginationNumber - 1) * $limit, $limit));
   3801             } else {
   3802                 $this->yellow->page->error(404);
   3803             }
   3804         }
   3805         return $this;
   3806     }
   3807     
   3808     // Return current page number in pagination
   3809     public function getPaginationNumber() {
   3810         return $this->paginationNumber;
   3811     }
   3812     
   3813     // Return highest page number in pagination
   3814     public function getPaginationCount() {
   3815         return $this->paginationCount;
   3816     }
   3817     
   3818     // Return location for a page in pagination
   3819     public function getPaginationLocation($absoluteLocation = true, $pageNumber = 1) {
   3820         $location = $locationArguments = "";
   3821         if ($pageNumber>=1 && $pageNumber<=$this->paginationCount) {
   3822             $location = $this->yellow->page->getLocation($absoluteLocation);
   3823             $locationArguments = $this->yellow->toolbox->getLocationArgumentsNew("page", $pageNumber>1 ? "$pageNumber" : "");
   3824         }
   3825         return $location.$locationArguments;
   3826     }
   3827     
   3828     // Return location for previous page in pagination
   3829     public function getPaginationPrevious($absoluteLocation = true) {
   3830         $pageNumber = $this->paginationNumber-1;
   3831         return $this->getPaginationLocation($absoluteLocation, $pageNumber);
   3832     }
   3833     
   3834     // Return location for next page in pagination
   3835     public function getPaginationNext($absoluteLocation = true) {
   3836         $pageNumber = $this->paginationNumber+1;
   3837         return $this->getPaginationLocation($absoluteLocation, $pageNumber);
   3838     }
   3839     
   3840     // Return current page number in collection
   3841     public function getPageNumber($page) {
   3842         $pageNumber = 0;
   3843         foreach ($this->getIterator() as $key=>$value) {
   3844             if ($page->getLocation()==$value->getLocation()) {
   3845                 $pageNumber = $key+1;
   3846                 break;
   3847             }
   3848         }
   3849         return $pageNumber;
   3850     }
   3851     
   3852     // Return page in collection, null if none
   3853     public function getPage($pageNumber = 1) {
   3854         return ($pageNumber>=1 && $pageNumber<=$this->count()) ? $this->offsetGet($pageNumber-1) : null;
   3855     }
   3856     
   3857     // Return previous page in collection, null if none
   3858     public function getPagePrevious($page) {
   3859         $pageNumber = $this->getPageNumber($page)-1;
   3860         return $this->getPage($pageNumber);
   3861     }
   3862     
   3863     // Return next page in collection, null if none
   3864     public function getPageNext($page) {
   3865         $pageNumber = $this->getPageNumber($page)+1;
   3866         return $this->getPage($pageNumber);
   3867     }
   3868     
   3869     // Return current page filter
   3870     public function getFilter() {
   3871         return $this->filterValue;
   3872     }
   3873     
   3874     // Return page collection modification date, Unix time or HTTP format
   3875     public function getModified($httpFormat = false) {
   3876         $modified = 0;
   3877         foreach ($this->getIterator() as $page) {
   3878             $modified = max($modified, $page->getModified());
   3879         }
   3880         return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified;
   3881     }
   3882     
   3883     // Check if there is a pagination
   3884     public function isPagination() {
   3885         return $this->paginationCount>1;
   3886     }
   3887     
   3888     // Check if page collection is empty
   3889     public function isEmpty() {
   3890         return empty($this->getArrayCopy());
   3891     }
   3892 }
   3893     
   3894 class YellowArray extends ArrayObject {
   3895     public function __construct($array = []) {
   3896         parent::__construct($array);
   3897     }
   3898     
   3899     // Set array element
   3900     public function set($key, $value) {
   3901         $this->offsetSet($key, $value);
   3902     }
   3903     
   3904     // Return array element
   3905     public function get($key) {
   3906         return $this->offsetExists($key) ? $this->offsetGet($key) : "";
   3907     }
   3908     
   3909     // Check if array element exists
   3910     public function isExisting($key) {
   3911         return $this->offsetExists($key);
   3912     }
   3913     
   3914     // Return array element
   3915     #[\ReturnTypeWillChange]
   3916     public function offsetGet($key) {
   3917         if (is_string($key)) $key = lcfirst($key);
   3918         return parent::offsetGet($key);
   3919     }
   3920     
   3921     // Set array element
   3922     #[\ReturnTypeWillChange]
   3923     public function offsetSet($key, $value) {
   3924         if (is_string($key)) $key = lcfirst($key);
   3925         parent::offsetSet($key, $value);
   3926     }
   3927     
   3928     // Remove array element
   3929     #[\ReturnTypeWillChange]
   3930     public function offsetUnset($key) {
   3931         if (is_string($key)) $key = lcfirst($key);
   3932         parent::offsetUnset($key);
   3933     }
   3934     
   3935     // Check if array element exists
   3936     #[\ReturnTypeWillChange]
   3937     public function offsetExists($key) {
   3938         if (is_string($key)) $key = lcfirst($key);
   3939         return parent::offsetExists($key);
   3940     }
   3941 
   3942     // Check if array is empty
   3943     public function isEmpty() {
   3944         return empty($this->getArrayCopy());
   3945     }
   3946 }
   3947 
   3948 // Make string lowercase, UTF-8 compatible
   3949 function strtoloweru() {
   3950     return call_user_func_array("mb_strtolower", func_get_args());
   3951 }
   3952 
   3953 // Make string uppercase, UTF-8 compatible
   3954 function strtoupperu() {
   3955     return call_user_func_array("mb_strtoupper", func_get_args());
   3956 }
   3957 
   3958 // Return string length, UTF-8 characters
   3959 function strlenu() {
   3960     return call_user_func_array("mb_strlen", func_get_args());
   3961 }
   3962 
   3963 // Return string length, bytes
   3964 function strlenb() {
   3965     return call_user_func_array("strlen", func_get_args());
   3966 }
   3967 
   3968 // Return string position of first match, UTF-8 characters
   3969 function strposu() {
   3970     return call_user_func_array("mb_strpos", func_get_args());
   3971 }
   3972 
   3973 // Return string position of first match, bytes
   3974 function strposb() {
   3975     return call_user_func_array("strpos", func_get_args());
   3976 }
   3977 
   3978 // Return string position of last match, UTF-8 characters
   3979 function strrposu() {
   3980     return call_user_func_array("mb_strrpos", func_get_args());
   3981 }
   3982 
   3983 // Return string position of last match, bytes
   3984 function strrposb() {
   3985     return call_user_func_array("strrpos", func_get_args());
   3986 }
   3987 
   3988 // Return part of a string, UTF-8 characters
   3989 function substru() {
   3990     return call_user_func_array("mb_substr", func_get_args());
   3991 }
   3992 
   3993 // Return part of a string, bytes
   3994 function substrb() {
   3995     return call_user_func_array("substr", func_get_args());
   3996 }
   3997 
   3998 // Check if string is empty
   3999 function is_string_empty($string) {
   4000     return is_null($string) || $string==="";
   4001 }
   4002 
   4003 // Check if array is empty
   4004 function is_array_empty($array) {
   4005     return is_null($array) || (is_array($array) ? empty($array) : empty($array->getArrayCopy()));
   4006 }