Parcourir la source

Updated coding standard, there is no spoon

markseu il y a 7 ans
Parent
commit
2f213b10bd

+ 558 - 647
system/plugins/command.php

@@ -3,657 +3,568 @@
 // Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
-class YellowCommand
-{
-	const VERSION = "0.7.8";
-	var $yellow;					//access to API
-	var $files;						//number of files
-	var $links;						//number of links
-	var $errors;					//number of errors
-	var $locationsArgs;				//locations with location arguments detected
-	var $locationsArgsPagination;	//locations with pagination arguments detected
-	
-	// Handle initialisation
-	function onLoad($yellow)
-	{
-		$this->yellow = $yellow;
-	}
-	
-	// Handle command
-	function onCommand($args)
-	{
-		list($command) = $args;
-		switch($command)
-		{
-			case "":		$statusCode = $this->helpCommand(); break;
-			case "build":	$statusCode = $this->buildCommand($args); break;
-			case "check":	$statusCode = $this->checkCommand($args); break;
-			case "clean":	$statusCode = $this->cleanCommand($args); break;
-			case "version":	$statusCode = $this->versionCommand($args); break;
-			default:		$statusCode = 0;
-		}
-		return $statusCode;
-	}
-	
-	// Handle command help
-	function onCommandHelp()
-	{
-		$help .= "build [directory location]\n";
-		$help .= "check [directory location]\n";
-		$help .= "clean [directory location]\n";
-		$help .= "version\n";
-		return $help;
-	}
-	
-	// Show available commands
-	function helpCommand()
-	{
-		echo "Datenstrom Yellow ".YellowCore::VERSION."\n";
-		$lineCounter = 0;
-		foreach($this->getCommandHelp() as $line) echo (++$lineCounter>1 ? "        " : "Syntax: ")."yellow.php $line\n";
-		return 200;
-	}
-	
-	// Build static website
-	function buildCommand($args)
-	{
-		$statusCode = 0;
-		list($command, $path, $location) = $args;
-		if(empty($location) || $location[0]=='/')
-		{
-			if($this->checkStaticConfig())
-			{
-				$statusCode = $this->buildStaticFiles($path, $location);
-			} else {
-				$statusCode = 500;
-				$this->files = 0; $this->errors = 1;
-				$fileName = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
-				echo "ERROR building files: Please configure StaticUrl in file '$fileName'!\n";
-			}
-			echo "Yellow $command: $this->files file".($this->files!=1 ? 's' : '');
-			echo ", $this->errors error".($this->errors!=1 ? 's' : '')."\n";
-		} else {
-			$statusCode = 400;
-			echo "Yellow $command: Invalid arguments\n";
-		}
-		return $statusCode;
-	}
-	
-	// Build static files
-	function buildStaticFiles($path, $locationFilter)
-	{
-		$path = rtrim(empty($path) ? $this->yellow->config->get("staticDir") : $path, '/');
-		$this->files = $this->errors = 0;
-		$this->locationsArgs = $this->locationsArgsPagination = array();
-		$statusCode = empty($locationFilter) ? $this->cleanStaticFiles($path, $locationFilter) : 200;
-		$staticUrl = $this->yellow->config->get("staticUrl");
-		list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
-		foreach($this->getContentLocations() as $location)
-		{
-			if(!preg_match("#^$base$locationFilter#", "$base$location")) continue;
-			$statusCode = max($statusCode, $this->buildStaticFile($path, $location, true));
-		}
-		foreach($this->locationsArgs as $location)
-		{
-			if(!preg_match("#^$base$locationFilter#", "$base$location")) continue;
-			$statusCode = max($statusCode, $this->buildStaticFile($path, $location, true));
-		}
-		foreach($this->locationsArgsPagination as $location)
-		{
-			if(!preg_match("#^$base$locationFilter#", "$base$location")) continue;
-			if(substru($location, -1)!=$this->yellow->toolbox->getLocationArgsSeparator())
-			{
-				$statusCode = max($statusCode, $this->buildStaticFile($path, $location, false, true));
-			}
-			for($pageNumber=2; $pageNumber<=999; ++$pageNumber)
-			{
-				$statusCodeLocation = $this->buildStaticFile($path, $location.$pageNumber, false, true);
-				$statusCode = max($statusCode, $statusCodeLocation);
-				if($statusCodeLocation==100) break;
-			}
-		}
-		if(empty($locationFilter))
-		{
-			foreach($this->getMediaLocations() as $location)
-			{
-				$statusCode = max($statusCode, $this->buildStaticFile($path, $location));
-			}
-			foreach($this->getSystemLocations() as $location)
-			{
-				$statusCode = max($statusCode, $this->buildStaticFile($path, $location));
-			}
-			$statusCode = max($statusCode, $this->buildStaticFile($path, "/error/", false, false, true));
-		}
-		return $statusCode;
-	}
-	
-	// Build static file
-	function buildStaticFile($path, $location, $analyse = false, $probe = false, $error = false)
-	{
-		$this->yellow->pages = new YellowPages($this->yellow);
-		$this->yellow->page = new YellowPage($this->yellow);
-		$this->yellow->page->fileName = substru($location, 1);
-		if(!is_readable($this->yellow->page->fileName))
-		{
-			ob_start();
-			$staticUrl = $this->yellow->config->get("staticUrl");
-			list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
-			$statusCode = $this->requestStaticFile($scheme, $address, $base, $location);
-			if($statusCode<400 || $error)
-			{
-				$fileData = ob_get_contents();
-				$modified = strtotime($this->yellow->page->getHeader("Last-Modified"));
-				if($modified==0) $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName);
-				if($statusCode>=301 && $statusCode<=303)
-				{
-					$fileData = $this->getStaticRedirect($this->yellow->page->getHeader("Location"));
-					$modified = time();
-				}
-				$fileName = $this->getStaticFile($path, $location, $statusCode);
-				if(!$this->yellow->toolbox->createFile($fileName, $fileData, true) ||
-				   !$this->yellow->toolbox->modifyFile($fileName, $modified))
-				{
-					$statusCode = 500;
-					$this->yellow->page->statusCode = $statusCode;
-					$this->yellow->page->set("pageError", "Can't write file '$fileName'!");
-				}
-			}
-			ob_end_clean();
-		} else {
-			$statusCode = 200;
-			$modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName);
-			$fileName = $this->getStaticFile($path, $location, $statusCode);
-			if(!$this->yellow->toolbox->copyFile($this->yellow->page->fileName, $fileName, true) ||
-			   !$this->yellow->toolbox->modifyFile($fileName, $modified))
-			{
-				$statusCode = 500;
-				$this->yellow->page->statusCode = $statusCode;
-				$this->yellow->page->set("pageError", "Can't write file '$fileName'!");
-			}
-		}
-		if($statusCode==200 && $analyse) $this->analyseStaticFile($scheme, $address, $base, $fileData);
-		if($statusCode==404 && $probe) $statusCode = 100;
-		if($statusCode==404 && $error) $statusCode = 200;
-		if($statusCode>=200) ++$this->files;
-		if($statusCode>=400)
-		{
-			++$this->errors;
-			echo "ERROR building location '$location', ".$this->yellow->page->getStatusCode(true)."\n";
-		}
-		if(defined("DEBUG") && DEBUG>=1) echo "YellowCommand::buildStaticFile status:$statusCode location:$location<br/>\n";
-		return $statusCode;
-	}
-	
-	// Request static file
-	function requestStaticFile($scheme, $address, $base, $location)
-	{
-		list($serverName, $serverPort) = explode(':', $address);
-		if(is_null($serverPort)) $serverPort = $scheme=="https" ? 443 : 80;
-		$_SERVER["HTTPS"] = $scheme=="https" ? "on" : "off";
-		$_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1";
-		$_SERVER["SERVER_NAME"] = $serverName;
-		$_SERVER["SERVER_PORT"] = $serverPort;
-		$_SERVER["REQUEST_METHOD"] = "GET";
-		$_SERVER["REQUEST_URI"] = $base.$location;
-		$_SERVER["SCRIPT_NAME"] = $base."/yellow.php";
-		$_SERVER["REMOTE_ADDR"] = "127.0.0.1";
-		$_REQUEST = array();
-		return $this->yellow->request();
-	}
-	
-	// Analyse static file, detect locations with arguments
-	function analyseStaticFile($scheme, $address, $base, $rawData)
-	{
-		$pagination = $this->yellow->config->get("contentPagination");
-		preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $rawData, $matches);
-		foreach($matches[2] as $match)
-		{
-			$location = rawurldecode($match);
-			if(preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1];
-			if(preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $tokens))
-			{
-				if($tokens[1]!=$scheme) continue;
-				if($tokens[2]!=$address) continue;
-				$location = $tokens[3];
-			}
-			if(substru($location, 0, strlenu($base))!=$base) continue;
-			$location = substru($location, strlenu($base));
-			if(!$this->yellow->toolbox->isLocationArgs($location)) continue;
-			if(!$this->yellow->toolbox->isLocationArgsPagination($location, $pagination))
-			{
-				$location = rtrim($location, '/').'/';
-				if(is_null($this->locationsArgs[$location]))
-				{
-					$this->locationsArgs[$location] = $location;
-					if(defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseStaticFile detected location:$location<br/>\n";
-				}
-			} else {
-				$location = rtrim($location, "0..9");
-				if(is_null($this->locationsArgsPagination[$location]))
-				{
-					$this->locationsArgsPagination[$location] = $location;
-					if(defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseStaticFile detected location:$location<br/>\n";
-				}
-			}
-		}
-	}
+class YellowCommand {
+    const VERSION = "0.7.8";
+    public $yellow;                     //access to API
+    public $files;                      //number of files
+    public $links;                      //number of links
+    public $errors;                     //number of errors
+    public $locationsArgs;              //locations with location arguments detected
+    public $locationsArgsPagination;    //locations with pagination arguments detected
+    
+    // Handle initialisation
+    public function onLoad($yellow) {
+        $this->yellow = $yellow;
+    }
+    
+    // Handle command
+    public function onCommand($args) {
+        list($command) = $args;
+        switch ($command) {
+            case "":        $statusCode = $this->helpCommand(); break;
+            case "build":   $statusCode = $this->buildCommand($args); break;
+            case "check":   $statusCode = $this->checkCommand($args); break;
+            case "clean":   $statusCode = $this->cleanCommand($args); break;
+            case "version": $statusCode = $this->versionCommand($args); break;
+            default:        $statusCode = 0;
+        }
+        return $statusCode;
+    }
+    
+    // Handle command help
+    public function onCommandHelp() {
+        $help .= "build [directory location]\n";
+        $help .= "check [directory location]\n";
+        $help .= "clean [directory location]\n";
+        $help .= "version\n";
+        return $help;
+    }
+    
+    // Show available commands
+    public function helpCommand() {
+        echo "Datenstrom Yellow ".YellowCore::VERSION."\n";
+        $lineCounter = 0;
+        foreach ($this->getCommandHelp() as $line) {
+            echo(++$lineCounter>1 ? "        " : "Syntax: ")."yellow.php $line\n";
+        }
+        return 200;
+    }
+    
+    // Build static website
+    public function buildCommand($args) {
+        $statusCode = 0;
+        list($command, $path, $location) = $args;
+        if (empty($location) || $location[0]=="/") {
+            if ($this->checkStaticConfig()) {
+                $statusCode = $this->buildStaticFiles($path, $location);
+            } else {
+                $statusCode = 500;
+                $this->files = 0;
+                $this->errors = 1;
+                $fileName = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
+                echo "ERROR building files: Please configure StaticUrl in file '$fileName'!\n";
+            }
+            echo "Yellow $command: $this->files file".($this->files!=1 ? "s" : "");
+            echo ", $this->errors error".($this->errors!=1 ? "s" : "")."\n";
+        } else {
+            $statusCode = 400;
+            echo "Yellow $command: Invalid arguments\n";
+        }
+        return $statusCode;
+    }
+    
+    // Build static files
+    public function buildStaticFiles($path, $locationFilter) {
+        $path = rtrim(empty($path) ? $this->yellow->config->get("staticDir") : $path, "/");
+        $this->files = $this->errors = 0;
+        $this->locationsArgs = $this->locationsArgsPagination = array();
+        $statusCode = empty($locationFilter) ? $this->cleanStaticFiles($path, $locationFilter) : 200;
+        $staticUrl = $this->yellow->config->get("staticUrl");
+        list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
+        foreach ($this->getContentLocations() as $location) {
+            if (!preg_match("#^$base$locationFilter#", "$base$location")) continue;
+            $statusCode = max($statusCode, $this->buildStaticFile($path, $location, true));
+        }
+        foreach ($this->locationsArgs as $location) {
+            if (!preg_match("#^$base$locationFilter#", "$base$location")) continue;
+            $statusCode = max($statusCode, $this->buildStaticFile($path, $location, true));
+        }
+        foreach ($this->locationsArgsPagination as $location) {
+            if (!preg_match("#^$base$locationFilter#", "$base$location")) continue;
+            if (substru($location, -1)!=$this->yellow->toolbox->getLocationArgsSeparator()) {
+                $statusCode = max($statusCode, $this->buildStaticFile($path, $location, false, true));
+            }
+            for ($pageNumber=2; $pageNumber<=999; ++$pageNumber) {
+                $statusCodeLocation = $this->buildStaticFile($path, $location.$pageNumber, false, true);
+                $statusCode = max($statusCode, $statusCodeLocation);
+                if ($statusCodeLocation==100) break;
+            }
+        }
+        if (empty($locationFilter)) {
+            foreach ($this->getMediaLocations() as $location) {
+                $statusCode = max($statusCode, $this->buildStaticFile($path, $location));
+            }
+            foreach ($this->getSystemLocations() as $location) {
+                $statusCode = max($statusCode, $this->buildStaticFile($path, $location));
+            }
+            $statusCode = max($statusCode, $this->buildStaticFile($path, "/error/", false, false, true));
+        }
+        return $statusCode;
+    }
+    
+    // Build static file
+    public function buildStaticFile($path, $location, $analyse = false, $probe = false, $error = false) {
+        $this->yellow->pages = new YellowPages($this->yellow);
+        $this->yellow->page = new YellowPage($this->yellow);
+        $this->yellow->page->fileName = substru($location, 1);
+        if (!is_readable($this->yellow->page->fileName)) {
+            ob_start();
+            $staticUrl = $this->yellow->config->get("staticUrl");
+            list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
+            $statusCode = $this->requestStaticFile($scheme, $address, $base, $location);
+            if ($statusCode<400 || $error) {
+                $fileData = ob_get_contents();
+                $modified = strtotime($this->yellow->page->getHeader("Last-Modified"));
+                if ($modified==0) $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName);
+                if ($statusCode>=301 && $statusCode<=303) {
+                    $fileData = $this->getStaticRedirect($this->yellow->page->getHeader("Location"));
+                    $modified = time();
+                }
+                $fileName = $this->getStaticFile($path, $location, $statusCode);
+                if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) ||
+                    !$this->yellow->toolbox->modifyFile($fileName, $modified)) {
+                    $statusCode = 500;
+                    $this->yellow->page->statusCode = $statusCode;
+                    $this->yellow->page->set("pageError", "Can't write file '$fileName'!");
+                }
+            }
+            ob_end_clean();
+        } else {
+            $statusCode = 200;
+            $modified = $this->yellow->toolbox->getFileModified($this->yellow->page->fileName);
+            $fileName = $this->getStaticFile($path, $location, $statusCode);
+            if (!$this->yellow->toolbox->copyFile($this->yellow->page->fileName, $fileName, true) ||
+                !$this->yellow->toolbox->modifyFile($fileName, $modified)) {
+                $statusCode = 500;
+                $this->yellow->page->statusCode = $statusCode;
+                $this->yellow->page->set("pageError", "Can't write file '$fileName'!");
+            }
+        }
+        if ($statusCode==200 && $analyse) $this->analyseStaticFile($scheme, $address, $base, $fileData);
+        if ($statusCode==404 && $probe) $statusCode = 100;
+        if ($statusCode==404 && $error) $statusCode = 200;
+        if ($statusCode>=200) ++$this->files;
+        if ($statusCode>=400) {
+            ++$this->errors;
+            echo "ERROR building location '$location', ".$this->yellow->page->getStatusCode(true)."\n";
+        }
+        if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::buildStaticFile status:$statusCode location:$location<br/>\n";
+        return $statusCode;
+    }
+    
+    // Request static file
+    public function requestStaticFile($scheme, $address, $base, $location) {
+        list($serverName, $serverPort) = explode(":", $address);
+        if (is_null($serverPort)) $serverPort = $scheme=="https" ? 443 : 80;
+        $_SERVER["HTTPS"] = $scheme=="https" ? "on" : "off";
+        $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1";
+        $_SERVER["SERVER_NAME"] = $serverName;
+        $_SERVER["SERVER_PORT"] = $serverPort;
+        $_SERVER["REQUEST_METHOD"] = "GET";
+        $_SERVER["REQUEST_URI"] = $base.$location;
+        $_SERVER["SCRIPT_NAME"] = $base."/yellow.php";
+        $_SERVER["REMOTE_ADDR"] = "127.0.0.1";
+        $_REQUEST = array();
+        return $this->yellow->request();
+    }
+    
+    // Analyse static file, detect locations with arguments
+    public function analyseStaticFile($scheme, $address, $base, $rawData) {
+        $pagination = $this->yellow->config->get("contentPagination");
+        preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $rawData, $matches);
+        foreach ($matches[2] as $match) {
+            $location = rawurldecode($match);
+            if (preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1];
+            if (preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $tokens)) {
+                if ($tokens[1]!=$scheme) continue;
+                if ($tokens[2]!=$address) continue;
+                $location = $tokens[3];
+            }
+            if (substru($location, 0, strlenu($base))!=$base) continue;
+            $location = substru($location, strlenu($base));
+            if (!$this->yellow->toolbox->isLocationArgs($location)) continue;
+            if (!$this->yellow->toolbox->isLocationArgsPagination($location, $pagination)) {
+                $location = rtrim($location, "/")."/";
+                if (is_null($this->locationsArgs[$location])) {
+                    $this->locationsArgs[$location] = $location;
+                    if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseStaticFile detected location:$location<br/>\n";
+                }
+            } else {
+                $location = rtrim($location, "0..9");
+                if (is_null($this->locationsArgsPagination[$location])) {
+                    $this->locationsArgsPagination[$location] = $location;
+                    if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseStaticFile detected location:$location<br/>\n";
+                }
+            }
+        }
+    }
 
-	// Check static files for broken links
-	function checkCommand($args)
-	{
-		$statusCode = 0;
-		list($command, $path, $location) = $args;
-		if(empty($location) || $location[0]=='/')
-		{
-			if($this->checkStaticConfig())
-			{
-				$statusCode = $this->checkStaticFiles($path, $location);
-			} else {
-				$statusCode = 500;
-				$this->files = $this->links = 0;
-				$fileName = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
-				echo "ERROR checking files: Please configure StaticUrl in file '$fileName'!\n";
-			}
-			echo "Yellow $command: $this->files file".($this->files!=1 ? 's' : '');
-			echo ", $this->links link".($this->links!=1 ? 's' : '')."\n";
-		} else {
-			$statusCode = 400;
-			echo "Yellow $command: Invalid arguments\n";
-		}
-		return $statusCode;
-	}
-	
-	// Check static files
-	function checkStaticFiles($path, $locationFilter)
-	{
-		$path = rtrim(empty($path) ? $this->yellow->config->get("staticDir") : $path, '/');
-		$this->files = $this->links = 0;
-		$regex = "/^[^.]+$|".$this->yellow->config->get("staticDefaultFile")."$/";
-		$fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($path, $regex, false, false);
-		list($statusCodeFiles, $links) = $this->analyseStaticFiles($path, $locationFilter, $fileNames);
-		list($statusCodeLinks, $broken, $redirect) = $this->analyseLinks($path, $links);
-		if($statusCodeLinks!=200)
-		{
-			$this->showLinks($broken, "Broken links");
-			$this->showLinks($redirect, "Redirect links");
-		}
-		return max($statusCodeFiles, $statusCodeLinks);
-	}
-	
-	// Analyse static files, detect links
-	function analyseStaticFiles($path, $locationFilter, $fileNames) 
-	{
-		$statusCode = 200;
-		$links = array();
-		if(!empty($fileNames))
-		{
-			$staticUrl = $this->yellow->config->get("staticUrl");
-			list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
-			foreach($fileNames as $fileName)
-			{
-				if(is_readable($fileName))
-				{
-					$locationSource = $this->getStaticLocation($path, $fileName);
-					if(!preg_match("#^$base$locationFilter#", "$base$locationSource")) continue;
-					$fileData = $this->yellow->toolbox->readFile($fileName);
-					preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $fileData, $matches);
-					foreach($matches[2] as $match)
-					{
-						$location = rawurldecode($match);
-						if(preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1];
-						if(preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $matches))
-						{
-							$url = $location.(empty($matches[3]) ? "/" : "");
-							if(!is_null($links[$url])) $links[$url] .= ",";
-							$links[$url] .= $locationSource;
-							if(defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseStaticFiles detected url:$url<br/>\n";
-						} else if($location[0]=='/') {
-							$url = "$scheme://$address$location";
-							if(!is_null($links[$url])) $links[$url] .= ",";
-							$links[$url] .= $locationSource;
-							if(defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseStaticFiles detected url:$url<br/>\n";
-						}
-					}
-					++$this->files;
-				} else {
-					$statusCode = 500;
-					echo "ERROR reading files: Can't read file '$fileName'!\n";
-				}
-			}
-			$this->links = count($links);
-		} else {
-			$statusCode = 500;
-			echo "ERROR reading files: Can't find files in directory '$path'!\n";
-		}
-		return array($statusCode, $links);
-	}
-	
-	// Analyse links, detect status
-	function analyseLinks($path, $links)
-	{
-		$statusCode = 200;
-		$broken = $redirect = $data = array();
-		$staticUrl = $this->yellow->config->get("staticUrl");
-		$staticUrlLength = strlenu(rtrim($staticUrl, '/'));
-		list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
-		$staticLocations = $this->getContentLocations(true);
-		uksort($links, "strnatcasecmp");
-		foreach($links as $url=>$value)
-		{
-			if(defined("DEBUG") && DEBUG>=1) echo "YellowCommand::analyseLinks url:$url\n";
-			if(preg_match("#^$staticUrl#", $url))
-			{
-				$location = substru($url, $staticUrlLength);
-				$fileName = $path.substru($url, $staticUrlLength);
-				if(is_readable($fileName)) continue;
-				if(in_array($location, $staticLocations)) continue;
-			}
-			if(preg_match("/^(http|https):/", $url))
-			{
-				$referer = "$scheme://$address$base".(($pos = strposu($value, ',')) ? substru($value, 0, $pos) : $value);
-				$statusCodeUrl = $this->getLinkStatus($url, $referer);
-				if($statusCodeUrl!=200)
-				{
-					$statusCode = max($statusCode, $statusCodeUrl);
-					$data[$url] = "$statusCodeUrl,$value";
-				}
-			}
-		}
-		foreach($data as $url=>$value)
-		{
-			$locations = preg_split("/\s*,\s*/", $value);
-			$statusCodeUrl = array_shift($locations);
-			foreach($locations as $location)
-			{
-				if($statusCodeUrl==302) continue;
-				if($statusCodeUrl>=300 && $statusCodeUrl<=399) {
-					$redirect["$scheme://$address$base$location -> $url - ".$this->getStatusFormatted($statusCodeUrl)] = $statusCodeUrl;
-				} else {
-					$broken["$scheme://$address$base$location -> $url - ".$this->getStatusFormatted($statusCodeUrl)] = $statusCodeUrl;
-				}
-			}
-		}
-		return array($statusCode, $broken, $redirect);
-	}
+    // Check static files for broken links
+    public function checkCommand($args) {
+        $statusCode = 0;
+        list($command, $path, $location) = $args;
+        if (empty($location) || $location[0]=="/") {
+            if ($this->checkStaticConfig()) {
+                $statusCode = $this->checkStaticFiles($path, $location);
+            } else {
+                $statusCode = 500;
+                $this->files = $this->links = 0;
+                $fileName = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
+                echo "ERROR checking files: Please configure StaticUrl in file '$fileName'!\n";
+            }
+            echo "Yellow $command: $this->files file".($this->files!=1 ? "s" : "");
+            echo ", $this->links link".($this->links!=1 ? "s" : "")."\n";
+        } else {
+            $statusCode = 400;
+            echo "Yellow $command: Invalid arguments\n";
+        }
+        return $statusCode;
+    }
+    
+    // Check static files
+    public function checkStaticFiles($path, $locationFilter) {
+        $path = rtrim(empty($path) ? $this->yellow->config->get("staticDir") : $path, "/");
+        $this->files = $this->links = 0;
+        $regex = "/^[^.]+$|".$this->yellow->config->get("staticDefaultFile")."$/";
+        $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($path, $regex, false, false);
+        list($statusCodeFiles, $links) = $this->analyseStaticFiles($path, $locationFilter, $fileNames);
+        list($statusCodeLinks, $broken, $redirect) = $this->analyseLinks($path, $links);
+        if ($statusCodeLinks!=200) {
+            $this->showLinks($broken, "Broken links");
+            $this->showLinks($redirect, "Redirect links");
+        }
+        return max($statusCodeFiles, $statusCodeLinks);
+    }
+    
+    // Analyse static files, detect links
+    public function analyseStaticFiles($path, $locationFilter, $fileNames) {
+        $statusCode = 200;
+        $links = array();
+        if (!empty($fileNames)) {
+            $staticUrl = $this->yellow->config->get("staticUrl");
+            list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
+            foreach ($fileNames as $fileName) {
+                if (is_readable($fileName)) {
+                    $locationSource = $this->getStaticLocation($path, $fileName);
+                    if (!preg_match("#^$base$locationFilter#", "$base$locationSource")) continue;
+                    $fileData = $this->yellow->toolbox->readFile($fileName);
+                    preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $fileData, $matches);
+                    foreach ($matches[2] as $match) {
+                        $location = rawurldecode($match);
+                        if (preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1];
+                        if (preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $matches)) {
+                            $url = $location.(empty($matches[3]) ? "/" : "");
+                            if (!is_null($links[$url])) $links[$url] .= ",";
+                            $links[$url] .= $locationSource;
+                            if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseStaticFiles detected url:$url<br/>\n";
+                        } elseif ($location[0]=="/") {
+                            $url = "$scheme://$address$location";
+                            if (!is_null($links[$url])) $links[$url] .= ",";
+                            $links[$url] .= $locationSource;
+                            if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::analyseStaticFiles detected url:$url<br/>\n";
+                        }
+                    }
+                    ++$this->files;
+                } else {
+                    $statusCode = 500;
+                    echo "ERROR reading files: Can't read file '$fileName'!\n";
+                }
+            }
+            $this->links = count($links);
+        } else {
+            $statusCode = 500;
+            echo "ERROR reading files: Can't find files in directory '$path'!\n";
+        }
+        return array($statusCode, $links);
+    }
+    
+    // Analyse links, detect status
+    public function analyseLinks($path, $links) {
+        $statusCode = 200;
+        $broken = $redirect = $data = array();
+        $staticUrl = $this->yellow->config->get("staticUrl");
+        $staticUrlLength = strlenu(rtrim($staticUrl, "/"));
+        list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
+        $staticLocations = $this->getContentLocations(true);
+        uksort($links, "strnatcasecmp");
+        foreach ($links as $url=>$value) {
+            if (defined("DEBUG") && DEBUG>=1) echo "YellowCommand::analyseLinks url:$url\n";
+            if (preg_match("#^$staticUrl#", $url)) {
+                $location = substru($url, $staticUrlLength);
+                $fileName = $path.substru($url, $staticUrlLength);
+                if (is_readable($fileName)) continue;
+                if (in_array($location, $staticLocations)) continue;
+            }
+            if (preg_match("/^(http|https):/", $url)) {
+                $referer = "$scheme://$address$base".(($pos = strposu($value, ",")) ? substru($value, 0, $pos) : $value);
+                $statusCodeUrl = $this->getLinkStatus($url, $referer);
+                if ($statusCodeUrl!=200) {
+                    $statusCode = max($statusCode, $statusCodeUrl);
+                    $data[$url] = "$statusCodeUrl,$value";
+                }
+            }
+        }
+        foreach ($data as $url=>$value) {
+            $locations = preg_split("/\s*,\s*/", $value);
+            $statusCodeUrl = array_shift($locations);
+            foreach ($locations as $location) {
+                if ($statusCodeUrl==302) continue;
+                if ($statusCodeUrl>=300 && $statusCodeUrl<=399) {
+                    $redirect["$scheme://$address$base$location -> $url - ".$this->getStatusFormatted($statusCodeUrl)] = $statusCodeUrl;
+                } else {
+                    $broken["$scheme://$address$base$location -> $url - ".$this->getStatusFormatted($statusCodeUrl)] = $statusCodeUrl;
+                }
+            }
+        }
+        return array($statusCode, $broken, $redirect);
+    }
 
-	// Show links
-	function showLinks($data, $text)
-	{
-		if(!empty($data))
-		{
-			echo "$text\n\n";
-			uksort($data, "strnatcasecmp");
-			$data = array_slice($data, 0, 99);
-			foreach($data as $key=>$value) echo "- $key\n";
-			echo "\n";
-		}
-	}
-	
-	// Clean static files
-	function cleanCommand($args)
-	{
-		$statusCode = 0;
-		list($command, $path, $location) = $args;
-		if(empty($location) || $location[0]=='/')
-		{
-			$statusCode = $this->cleanStaticFiles($path, $location);
-			echo "Yellow $command: Static file".(empty($location) ? "s" : "")." ".($statusCode!=200 ? "not " : "")."cleaned\n";
-		} else {
-			$statusCode = 400;
-			echo "Yellow $command: Invalid arguments\n";
-		}
-		return $statusCode;
-	}
-	
-	// Clean static files and directories
-	function cleanStaticFiles($path, $location)
-	{
-		$statusCode = 200;
-		$path = rtrim(empty($path) ? $this->yellow->config->get("staticDir") : $path, '/');
-		if(empty($location))
-		{
-			$statusCode = max($statusCode, $this->commandBroadcast("clean", "all"));
-			$statusCode = max($statusCode, $this->cleanStaticDirectory($path));
-		} else {
-			if($this->yellow->lookup->isFileLocation($location))
-			{
-				$fileName = $this->getStaticFile($path, $location, $statusCode);
-				$statusCode = $this->cleanStaticFile($fileName);
-			} else {
-				$statusCode = $this->cleanStaticDirectory($path.$location);
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Clean static directory
-	function cleanStaticDirectory($path)
-	{
-		$statusCode = 200;
-		if(is_dir($path) && $this->checkStaticDirectory($path))
-		{
-			if(!$this->yellow->toolbox->deleteDirectory($path))
-			{
-				$statusCode = 500;
-				echo "ERROR cleaning files: Can't delete directory '$path'!\n";
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Clean static file
-	function cleanStaticFile($fileName)
-	{
-		$statusCode = 200;
-		if(is_file($fileName))
-		{
-			if(!$this->yellow->toolbox->deleteFile($fileName))
-			{
-				$statusCode = 500;
-				echo "ERROR cleaning files: Can't delete file '$fileName'!\n";
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Broadcast command to other plugins
-	function commandBroadcast($args)
-	{
-		$statusCode = 0;
-		foreach($this->yellow->plugins->plugins as $key=>$value)
-		{
-			if($key=="command") continue;
-			if(method_exists($value["obj"], "onCommand"))
-			{
-				$statusCode = $value["obj"]->onCommand(func_get_args());
-				if($statusCode!=0) break;
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Show software version and updates
-	function versionCommand($args)
-	{
-		$serverVersion = $this->yellow->toolbox->getServerVersion();
-		echo "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion\n";
-		list($statusCode, $dataCurrent) = $this->getSoftwareVersion();
-		list($statusCode, $dataLatest) = $this->getSoftwareVersion(true);
-		foreach($dataCurrent as $key=>$value)
-		{
-			if(strnatcasecmp($dataCurrent[$key], $dataLatest[$key])>=0)
-			{
-				echo "$key $value\n";
-			} else {
-				echo "$key $dataLatest[$key] - Update available\n";
-			}
-		}
-		if($statusCode!=200) echo "ERROR checking updates: ".$this->yellow->page->get("pageError")."\n";
-		return $statusCode;
-	}
-	
-	// Check static configuration
-	function checkStaticConfig()
-	{
-		$staticUrl = $this->yellow->config->get("staticUrl");
-		return !empty($staticUrl);
-	}
-	
-	// Check static directory
-	function checkStaticDirectory($path)
-	{
-		$ok = false;
-		if(!empty($path))
-		{
-			if($path==rtrim($this->yellow->config->get("staticDir"), '/')) $ok = true;
-			if($path==rtrim($this->yellow->config->get("trashDir"), '/')) $ok = true;
-			if(is_file("$path/".$this->yellow->config->get("staticDefaultFile"))) $ok = true;
-			if(is_file("$path/yellow.php")) $ok = false;
-		}
-		return $ok;
-	}
-	
-	// Return static file
-	function getStaticFile($path, $location, $statusCode)
-	{
-		if($statusCode<400)
-		{
-			$fileName = $path.$location;
-			if(!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->config->get("staticDefaultFile");
-		} else if($statusCode==404) {
-			$fileName = $path."/".$this->yellow->config->get("staticErrorFile");
-		}
-		return $fileName;
-	}
-	
-	// Return static location
-	function getStaticLocation($path, $fileName)
-	{
-		$location = substru($fileName, strlenu($path));
-		if(basename($location)==$this->yellow->config->get("staticDefaultFile"))
-		{
-			$defaultFileLength = strlenu($this->yellow->config->get("staticDefaultFile"));
-			$location = substru($location, 0, -$defaultFileLength);
-		}
-		return $location;
-	}
-	
-	// Return static redirect
-	function getStaticRedirect($location)
-	{
-		$output = "<!DOCTYPE html><html>\n<head>\n";
-		$output .= "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n";
-		$output .= "<meta http-equiv=\"refresh\" content=\"0;url=".htmlspecialchars($location)."\" />\n";
-		$output .= "</head>\n</html>";
-		return $output;
-	}
+    // Show links
+    public function showLinks($data, $text) {
+        if (!empty($data)) {
+            echo "$text\n\n";
+            uksort($data, "strnatcasecmp");
+            $data = array_slice($data, 0, 99);
+            foreach ($data as $key=>$value) {
+                echo "- $key\n";
+            }
+            echo "\n";
+        }
+    }
+    
+    // Clean static files
+    public function cleanCommand($args) {
+        $statusCode = 0;
+        list($command, $path, $location) = $args;
+        if (empty($location) || $location[0]=="/") {
+            $statusCode = $this->cleanStaticFiles($path, $location);
+            echo "Yellow $command: Static file".(empty($location) ? "s" : "")." ".($statusCode!=200 ? "not " : "")."cleaned\n";
+        } else {
+            $statusCode = 400;
+            echo "Yellow $command: Invalid arguments\n";
+        }
+        return $statusCode;
+    }
+    
+    // Clean static files and directories
+    public function cleanStaticFiles($path, $location) {
+        $statusCode = 200;
+        $path = rtrim(empty($path) ? $this->yellow->config->get("staticDir") : $path, "/");
+        if (empty($location)) {
+            $statusCode = max($statusCode, $this->commandBroadcast("clean", "all"));
+            $statusCode = max($statusCode, $this->cleanStaticDirectory($path));
+        } else {
+            if ($this->yellow->lookup->isFileLocation($location)) {
+                $fileName = $this->getStaticFile($path, $location, $statusCode);
+                $statusCode = $this->cleanStaticFile($fileName);
+            } else {
+                $statusCode = $this->cleanStaticDirectory($path.$location);
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Clean static directory
+    public function cleanStaticDirectory($path) {
+        $statusCode = 200;
+        if (is_dir($path) && $this->checkStaticDirectory($path)) {
+            if (!$this->yellow->toolbox->deleteDirectory($path)) {
+                $statusCode = 500;
+                echo "ERROR cleaning files: Can't delete directory '$path'!\n";
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Clean static file
+    public function cleanStaticFile($fileName) {
+        $statusCode = 200;
+        if (is_file($fileName)) {
+            if (!$this->yellow->toolbox->deleteFile($fileName)) {
+                $statusCode = 500;
+                echo "ERROR cleaning files: Can't delete file '$fileName'!\n";
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Broadcast command to other plugins
+    public function commandBroadcast($args) {
+        $statusCode = 0;
+        foreach ($this->yellow->plugins->plugins as $key=>$value) {
+            if ($key=="command") continue;
+            if (method_exists($value["obj"], "onCommand")) {
+                $statusCode = $value["obj"]->onCommand(func_get_args());
+                if ($statusCode!=0) break;
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Show software version and updates
+    public function versionCommand($args) {
+        $serverVersion = $this->yellow->toolbox->getServerVersion();
+        echo "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion\n";
+        list($statusCode, $dataCurrent) = $this->getSoftwareVersion();
+        list($statusCode, $dataLatest) = $this->getSoftwareVersion(true);
+        foreach ($dataCurrent as $key=>$value) {
+            if (strnatcasecmp($dataCurrent[$key], $dataLatest[$key])>=0) {
+                echo "$key $value\n";
+            } else {
+                echo "$key $dataLatest[$key] - Update available\n";
+            }
+        }
+        if ($statusCode!=200) echo "ERROR checking updates: ".$this->yellow->page->get("pageError")."\n";
+        return $statusCode;
+    }
+    
+    // Check static configuration
+    public function checkStaticConfig() {
+        $staticUrl = $this->yellow->config->get("staticUrl");
+        return !empty($staticUrl);
+    }
+    
+    // Check static directory
+    public function checkStaticDirectory($path) {
+        $ok = false;
+        if (!empty($path)) {
+            if ($path==rtrim($this->yellow->config->get("staticDir"), "/")) $ok = true;
+            if ($path==rtrim($this->yellow->config->get("trashDir"), "/")) $ok = true;
+            if (is_file("$path/".$this->yellow->config->get("staticDefaultFile"))) $ok = true;
+            if (is_file("$path/yellow.php")) $ok = false;
+        }
+        return $ok;
+    }
+    
+    // Return static file
+    public function getStaticFile($path, $location, $statusCode) {
+        if ($statusCode<400) {
+            $fileName = $path.$location;
+            if (!$this->yellow->lookup->isFileLocation($location)) $fileName .= $this->yellow->config->get("staticDefaultFile");
+        } elseif ($statusCode==404) {
+            $fileName = $path."/".$this->yellow->config->get("staticErrorFile");
+        }
+        return $fileName;
+    }
+    
+    // Return static location
+    public function getStaticLocation($path, $fileName) {
+        $location = substru($fileName, strlenu($path));
+        if (basename($location)==$this->yellow->config->get("staticDefaultFile")) {
+            $defaultFileLength = strlenu($this->yellow->config->get("staticDefaultFile"));
+            $location = substru($location, 0, -$defaultFileLength);
+        }
+        return $location;
+    }
+    
+    // Return static redirect
+    public function getStaticRedirect($location) {
+        $output = "<!DOCTYPE html><html>\n<head>\n";
+        $output .= "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n";
+        $output .= "<meta http-equiv=\"refresh\" content=\"0;url=".htmlspecialchars($location)."\" />\n";
+        $output .= "</head>\n</html>";
+        return $output;
+    }
 
-	// Return human readable status
-	function getStatusFormatted($statusCode)
-	{
-		return $this->yellow->toolbox->getHttpStatusFormatted($statusCode, true);
-	}
-	
-	// Return content locations
-	function getContentLocations($includeAll = false)
-	{
-		$locations = array();
-		$staticUrl = $this->yellow->config->get("staticUrl");
-		list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
-		$this->yellow->page->setRequestInformation($scheme, $address, $base, "", "");
-		foreach($this->yellow->pages->index(true, true) as $page)
-		{
-			if(($page->get("status")!="ignore" && $page->get("status")!="draft") || $includeAll)
-			{
-				array_push($locations, $page->location);
-			}
-		}
-		if(!$this->yellow->pages->find("/") && $this->yellow->config->get("multiLanguageMode")) array_unshift($locations, "/");
-		return $locations;
-	}
-	
-	// Return media locations
-	function getMediaLocations()
-	{
-		$locations = array();
-		$fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->config->get("mediaDir"), "/.*/", false, false);
-		foreach($fileNames as $fileName)
-		{
-			array_push($locations, "/".$fileName);
-		}
-		return $locations;
-	}
+    // Return human readable status
+    public function getStatusFormatted($statusCode) {
+        return $this->yellow->toolbox->getHttpStatusFormatted($statusCode, true);
+    }
+    
+    // Return content locations
+    public function getContentLocations($includeAll = false) {
+        $locations = array();
+        $staticUrl = $this->yellow->config->get("staticUrl");
+        list($scheme, $address, $base) = $this->yellow->lookup->getUrlInformation($staticUrl);
+        $this->yellow->page->setRequestInformation($scheme, $address, $base, "", "");
+        foreach ($this->yellow->pages->index(true, true) as $page) {
+            if (($page->get("status")!="ignore" && $page->get("status")!="draft") || $includeAll) {
+                array_push($locations, $page->location);
+            }
+        }
+        if (!$this->yellow->pages->find("/") && $this->yellow->config->get("multiLanguageMode")) array_unshift($locations, "/");
+        return $locations;
+    }
+    
+    // Return media locations
+    public function getMediaLocations() {
+        $locations = array();
+        $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->config->get("mediaDir"), "/.*/", false, false);
+        foreach ($fileNames as $fileName) {
+            array_push($locations, "/".$fileName);
+        }
+        return $locations;
+    }
 
-	// Return system locations
-	function getSystemLocations()
-	{
-		$locations = array();
-		$regex = "/\.(css|gif|ico|js|jpg|png|svg|txt|woff|woff2)$/";
-		$pluginDirLength = strlenu($this->yellow->config->get("pluginDir"));
-		$fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->config->get("pluginDir"), $regex, false, false);
-		foreach($fileNames as $fileName)
-		{
-			array_push($locations, $this->yellow->config->get("pluginLocation").substru($fileName, $pluginDirLength));
-		}
-		$themeDirLength = strlenu($this->yellow->config->get("themeDir"));
-		$fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->config->get("themeDir"), $regex, false, false);
-		foreach($fileNames as $fileName)
-		{
-			array_push($locations, $this->yellow->config->get("themeLocation").substru($fileName, $themeDirLength));
-		}
-		array_push($locations, "/".$this->yellow->config->get("robotsFile"));
-		return $locations;
-	}
-	
-	// Return command help
-	function getCommandHelp()
-	{
-		$data = array();
-		foreach($this->yellow->plugins->plugins as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onCommandHelp"))
-			{
-				foreach(preg_split("/[\r\n]+/", $value["obj"]->onCommandHelp()) as $line)
-				{
-					list($command) = explode(' ', $line);
-					if(!empty($command) && is_null($data[$command])) $data[$command] = $line;
-				}
-			}
-		}
-		uksort($data, "strnatcasecmp");
-		return $data;
-	}
+    // Return system locations
+    public function getSystemLocations() {
+        $locations = array();
+        $regex = "/\.(css|gif|ico|js|jpg|png|svg|txt|woff|woff2)$/";
+        $pluginDirLength = strlenu($this->yellow->config->get("pluginDir"));
+        $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->config->get("pluginDir"), $regex, false, false);
+        foreach ($fileNames as $fileName) {
+            array_push($locations, $this->yellow->config->get("pluginLocation").substru($fileName, $pluginDirLength));
+        }
+        $themeDirLength = strlenu($this->yellow->config->get("themeDir"));
+        $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($this->yellow->config->get("themeDir"), $regex, false, false);
+        foreach ($fileNames as $fileName) {
+            array_push($locations, $this->yellow->config->get("themeLocation").substru($fileName, $themeDirLength));
+        }
+        array_push($locations, "/".$this->yellow->config->get("robotsFile"));
+        return $locations;
+    }
+    
+    // Return command help
+    public function getCommandHelp() {
+        $data = array();
+        foreach ($this->yellow->plugins->plugins as $key=>$value) {
+            if (method_exists($value["obj"], "onCommandHelp")) {
+                foreach (preg_split("/[\r\n]+/", $value["obj"]->onCommandHelp()) as $line) {
+                    list($command) = explode(" ", $line);
+                    if (!empty($command) && is_null($data[$command])) $data[$command] = $line;
+                }
+            }
+        }
+        uksort($data, "strnatcasecmp");
+        return $data;
+    }
 
-	// Return software version
-	function getSoftwareVersion($latest = false)
-	{
-		$data = array();
-		if($this->yellow->plugins->isExisting("update"))
-		{
-			list($statusCode, $data) = $this->yellow->plugins->get("update")->getSoftwareVersion($latest);
-		} else {
-			$statusCode = 200;
-			$data = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData());
-		}
-		return array($statusCode, $data);
-	}
-	
-	// Return link status
-	function getLinkStatus($url, $referer)
-	{
-		$curlHandle = curl_init();
-		curl_setopt($curlHandle, CURLOPT_URL, $url);
-		curl_setopt($curlHandle, CURLOPT_REFERER, $referer);
-		curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; DatenstromYellow/".YellowCore::VERSION."; LinkChecker)");
-		curl_setopt($curlHandle, CURLOPT_NOBODY, 1);
-		curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30);
-		curl_exec($curlHandle);
-		$statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
-		curl_close($curlHandle);
-		if(defined("DEBUG") && DEBUG>=2) echo "YellowCommand::getLinkStatus status:$statusCode url:$url<br/>\n";
-		return $statusCode;
-	}
+    // Return software version
+    public function getSoftwareVersion($latest = false) {
+        $data = array();
+        if ($this->yellow->plugins->isExisting("update")) {
+            list($statusCode, $data) = $this->yellow->plugins->get("update")->getSoftwareVersion($latest);
+        } else {
+            $statusCode = 200;
+            $data = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData());
+        }
+        return array($statusCode, $data);
+    }
+    
+    // Return link status
+    public function getLinkStatus($url, $referer) {
+        $curlHandle = curl_init();
+        curl_setopt($curlHandle, CURLOPT_URL, $url);
+        curl_setopt($curlHandle, CURLOPT_REFERER, $referer);
+        curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; DatenstromYellow/".YellowCore::VERSION."; LinkChecker)");
+        curl_setopt($curlHandle, CURLOPT_NOBODY, 1);
+        curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30);
+        curl_exec($curlHandle);
+        $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
+        curl_close($curlHandle);
+        if (defined("DEBUG") && DEBUG>=2) echo "YellowCommand::getLinkStatus status:$statusCode url:$url<br/>\n";
+        return $statusCode;
+    }
 }
-	
+    
 $yellow->plugins->register("command", "YellowCommand", YellowCommand::VERSION);
-?>

+ 3053 - 3474
system/plugins/core.php

@@ -3,3536 +3,3115 @@
 // Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
-class YellowCore
-{
-	const VERSION = "0.7.5";
-	var $page;				//current page
-	var $pages;				//pages from file system
-	var $files;				//files from file system
-	var $plugins;			//plugins
-	var $themes;			//themes
-	var $config;			//configuration
-	var $text;				//text
-	var $lookup;			//location and file lookup
-	var $toolbox;			//toolbox with helpers
+class YellowCore {
+    const VERSION = "0.7.5";
+    public $page;           //current page
+    public $pages;          //pages from file system
+    public $files;          //files from file system
+    public $plugins;        //plugins
+    public $themes;         //themes
+    public $config;         //configuration
+    public $text;           //text
+    public $lookup;         //location and file lookup
+    public $toolbox;        //toolbox with helpers
 
-	function __construct()
-	{
-		$this->page = new YellowPage($this);
-		$this->pages = new YellowPages($this);
-		$this->files = new YellowFiles($this);
-		$this->plugins = new YellowPlugins($this);
-		$this->themes = new YellowThemes($this);
-		$this->config = new YellowConfig($this);
-		$this->text = new YellowText($this);
-		$this->lookup = new YellowLookup($this);
-		$this->toolbox = new YellowToolbox();
-		$this->config->setDefault("sitename", "Yellow");
-		$this->config->setDefault("author", "Yellow");
-		$this->config->setDefault("email", "webmaster");
-		$this->config->setDefault("language", "en");
-		$this->config->setDefault("timezone", "UTC");
-		$this->config->setDefault("theme", "default");
-		$this->config->setDefault("staticUrl", "");
-		$this->config->setDefault("staticDefaultFile", "index.html");
-		$this->config->setDefault("staticErrorFile", "404.html");
-		$this->config->setDefault("staticDir", "cache/");
-		$this->config->setDefault("mediaLocation", "/media/");
-		$this->config->setDefault("downloadLocation", "/media/downloads/");
-		$this->config->setDefault("imageLocation", "/media/images/");
-		$this->config->setDefault("pluginLocation", "/media/plugins/");
-		$this->config->setDefault("themeLocation", "/media/themes/");
-		$this->config->setDefault("assetLocation", "/media/themes/assets/");
-		$this->config->setDefault("mediaDir", "media/");
-		$this->config->setDefault("downloadDir", "media/downloads/");
-		$this->config->setDefault("imageDir", "media/images/");
-		$this->config->setDefault("systemDir", "system/");
-		$this->config->setDefault("configDir", "system/config/");
-		$this->config->setDefault("pluginDir", "system/plugins/");
-		$this->config->setDefault("themeDir", "system/themes/");
-		$this->config->setDefault("assetDir", "system/themes/assets/");
-		$this->config->setDefault("snippetDir", "system/themes/snippets/");
-		$this->config->setDefault("templateDir", "system/themes/templates/");
-		$this->config->setDefault("trashDir", "system/trash/");
-		$this->config->setDefault("contentDir", "content/");
-		$this->config->setDefault("contentRootDir", "default/");
-		$this->config->setDefault("contentHomeDir", "home/");
-		$this->config->setDefault("contentPagination", "page");
-		$this->config->setDefault("contentDefaultFile", "page.txt");
-		$this->config->setDefault("contentExtension", ".txt");
-		$this->config->setDefault("configExtension", ".ini");
-		$this->config->setDefault("downloadExtension", ".download");
-		$this->config->setDefault("configFile", "config.ini");
-		$this->config->setDefault("textFile", "text.ini");
-		$this->config->setDefault("languageFile", "language-(.*).txt");
-		$this->config->setDefault("errorFile", "page-error-(.*).txt");
-		$this->config->setDefault("newFile", "page-new-(.*).txt");
-		$this->config->setDefault("robotsFile", "robots.txt");
-		$this->config->setDefault("faviconFile", "favicon.ico");
-		$this->config->setDefault("serverUrl", "");
-		$this->config->setDefault("template", "default");
-		$this->config->setDefault("navigation", "navigation");
-		$this->config->setDefault("sidebar", "sidebar");
-		$this->config->setDefault("siteicon", "icon");
-		$this->config->setDefault("tagline", "");
-		$this->config->setDefault("parser", "markdown");
-		$this->config->setDefault("parserSafeMode", "0");
-		$this->config->setDefault("multiLanguageMode", "0");
-		$this->config->setDefault("installationMode", "0");
-		$this->config->setDefault("startupUpdate", "none");
-	}
-	
-	function __destruct()
-	{
-		$this->shutdown();
-	}
-	
-	// Handle initialisation
-	function load()
-	{
-		if(defined("DEBUG") && DEBUG>=2)
-		{
-			$serverVersion = $this->toolbox->getServerVersion();
-			echo "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion<br/>\n";
-		}
-		$this->toolbox->timerStart($time);
-		$this->config->load($this->config->get("configDir").$this->config->get("configFile"));
-		$this->lookup->load();
-		$this->themes->load();
-		$this->plugins->load();
-		$this->text->load($this->config->get("pluginDir").$this->config->get("languageFile"), "");
-		$this->text->load($this->config->get("configDir").$this->config->get("textFile"), $this->config->get("language"));
-		$this->toolbox->timerStop($time);
-		$this->startup();
-		if(defined("DEBUG") && DEBUG>=2)
-		{
-			$plugins = count($this->plugins->plugins);
-			$themes = count($this->themes->themes);
-			$languages = count($this->text->text);
-			echo "YellowCore::load plugins:$plugins themes:$themes languages:$languages time:$time ms<br/>\n";
-		}
-	}
-	
-	// Handle request
-	function request()
-	{
-		ob_start();
-		$statusCode = 0;
-		$this->toolbox->timerStart($time);
-		list($scheme, $address, $base, $location, $fileName) = $this->getRequestInformation();
-		$this->page->setRequestInformation($scheme, $address, $base, $location, $fileName);
-		foreach($this->plugins->plugins as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onRequest"))
-			{
-				$this->lookup->requestHandler = $key;
-				$statusCode = $value["obj"]->onRequest($scheme, $address, $base, $location, $fileName);
-				if($statusCode!=0) break;
-			}
-		}
-		if($statusCode==0)
-		{
-			$this->lookup->requestHandler = "core";
-			$statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName, true);
-		}
-		if($this->page->isExisting("pageError")) $statusCode = $this->processRequestError();
-		$this->toolbox->timerStop($time);
-		ob_end_flush();
-		if(defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName))
-		{
-			$handler = $this->getRequestHandler();
-			echo "YellowCore::request status:$statusCode handler:$handler time:$time ms<br/>\n";
-		}
-		return $statusCode;
-	}
-	
-	// Process request
-	function processRequest($scheme, $address, $base, $location, $fileName, $cacheable)
-	{
-		$statusCode = 0;
-		if(is_readable($fileName))
-		{
-			if($this->toolbox->isRequestCleanUrl($location))
-			{
-				$location = $location.$this->getRequestLocationArgsClean();
-				$location = $this->lookup->normaliseUrl($scheme, $address, $base, $location);
-				$statusCode = $this->sendStatus(303, $location);
-			}
-		} else {
-			if($this->lookup->isRedirectLocation($location))
-			{
-				$location = $this->lookup->isFileLocation($location) ? "$location/" : "/".$this->getRequestLanguage()."/";
-				$location = $this->lookup->normaliseUrl($scheme, $address, $base, $location);
-				$statusCode = $this->sendStatus(301, $location);
-			}
-		}
-		if($statusCode==0)
-		{
-			$fileName = $this->lookup->findFileStatic($location, $fileName, $cacheable && !$this->isCommandLine());
-			if($this->lookup->isContentFile($fileName) || !is_readable($fileName))
-			{
-				$fileName = $this->readPage($scheme, $address, $base, $location, $fileName, $cacheable,
-					max(is_readable($fileName) ? 200 : 404, $this->page->statusCode), $this->page->get("pageError"));
-				$statusCode = $this->sendPage();
-			} else {
-				$statusCode = $this->sendFile(200, $fileName, true);
-			}
-		}
-		if(defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName))
-		{
-			echo "YellowCore::processRequest file:$fileName<br/>\n";
-		}
-		return $statusCode;
-	}
-	
-	// Process request with error
-	function processRequestError()
-	{
-		ob_clean();
-		$fileName = $this->readPage($this->page->scheme, $this->page->address, $this->page->base,
-			$this->page->location, $this->page->fileName, $this->page->cacheable, $this->page->statusCode,
-			$this->page->get("pageError"));
-		$statusCode = $this->sendPage();
-		if(defined("DEBUG") && DEBUG>=1) echo "YellowCore::processRequestError file:$fileName<br/>\n";
-		return $statusCode;
-	}
-	
-	// Read page
-	function readPage($scheme, $address, $base, $location, $fileName, $cacheable, $statusCode, $pageError)
-	{
-		if($statusCode>=400)
-		{
-			$cacheable = false;
-			$fileName = $this->config->get("configDir").$this->config->get("errorFile");
-			$fileName = strreplaceu("(.*)", $statusCode, $fileName);
-			$rawData = $this->toolbox->readFile($fileName);
-			if(empty($rawData)) $rawData = "---\nTitle:".$this->toolbox->getHttpStatusFormatted($statusCode, true)."\n---\n";
-		} else {
-			$rawData = $this->toolbox->readFile($fileName);
-		}
-		$this->page = new YellowPage($this);
-		$this->page->setRequestInformation($scheme, $address, $base, $location, $fileName);
-		$this->page->parseData($rawData, $cacheable, $statusCode, $pageError);
-		$this->text->setLanguage($this->page->get("language"));
-		$this->page->parseContent();
-		return $fileName;
-	}
-	
-	// Send page response
-	function sendPage()
-	{
-		$this->page->parsePage();
-		$statusCode = $this->page->statusCode;
-		$lastModifiedFormatted = $this->page->getHeader("Last-Modified");
-		if($statusCode==200 && $this->page->isCacheable() && $this->toolbox->isRequestNotModified($lastModifiedFormatted))
-		{
-			$statusCode = 304;
-			@header($this->toolbox->getHttpStatusFormatted($statusCode));
-		} else {
-			@header($this->toolbox->getHttpStatusFormatted($statusCode));
-			foreach($this->page->headerData as $key=>$value) @header("$key: $value");
-			if(!is_null($this->page->outputData)) echo $this->page->outputData;
-		}
-		if(defined("DEBUG") && DEBUG>=1)
-		{
-			foreach($this->page->headerData as $key=>$value) echo "YellowCore::sendPage $key: $value<br/>\n";
-			$theme = $this->page->get("theme");
-			$template = $this->page->get("template");
-			$parser = $this->page->get("parser");
-			echo "YellowCore::sendPage theme:$theme template:$template parser:$parser<br/>\n";
-		}
-		return $statusCode;
-	}
-	
-	// Send file response
-	function sendFile($statusCode, $fileName, $cacheable)
-	{
-		$lastModifiedFormatted = $this->toolbox->getHttpDateFormatted($this->toolbox->getFileModified($fileName));
-		if($statusCode==200 && $cacheable && $this->toolbox->isRequestNotModified($lastModifiedFormatted))
-		{
-			$statusCode = 304;
-			@header($this->toolbox->getHttpStatusFormatted($statusCode));
-		} else {
-			@header($this->toolbox->getHttpStatusFormatted($statusCode));
-			if(!$cacheable) @header("Cache-Control: no-cache, must-revalidate");
-			@header("Content-Type: ".$this->toolbox->getMimeContentType($fileName));
-			@header("Last-Modified: ".$lastModifiedFormatted);
-			echo $this->toolbox->readFile($fileName);
-		}
-		return $statusCode;
-	}
-	
-	// Send data response
-	function sendData($statusCode, $rawData, $fileName, $cacheable)
-	{
-		@header($this->toolbox->getHttpStatusFormatted($statusCode));
-		if(!$cacheable) @header("Cache-Control: no-cache, must-revalidate");
-		@header("Content-Type: ".$this->toolbox->getMimeContentType($fileName));
-		@header("Last-Modified: ".$this->toolbox->getHttpDateFormatted(time()));
-		echo $rawData;
-		return $statusCode;
-	}
+    public function __construct() {
+        $this->page = new YellowPage($this);
+        $this->pages = new YellowPages($this);
+        $this->files = new YellowFiles($this);
+        $this->plugins = new YellowPlugins($this);
+        $this->themes = new YellowThemes($this);
+        $this->config = new YellowConfig($this);
+        $this->text = new YellowText($this);
+        $this->lookup = new YellowLookup($this);
+        $this->toolbox = new YellowToolbox();
+        $this->config->setDefault("sitename", "Yellow");
+        $this->config->setDefault("author", "Yellow");
+        $this->config->setDefault("email", "webmaster");
+        $this->config->setDefault("language", "en");
+        $this->config->setDefault("timezone", "UTC");
+        $this->config->setDefault("theme", "default");
+        $this->config->setDefault("staticUrl", "");
+        $this->config->setDefault("staticDefaultFile", "index.html");
+        $this->config->setDefault("staticErrorFile", "404.html");
+        $this->config->setDefault("staticDir", "cache/");
+        $this->config->setDefault("mediaLocation", "/media/");
+        $this->config->setDefault("downloadLocation", "/media/downloads/");
+        $this->config->setDefault("imageLocation", "/media/images/");
+        $this->config->setDefault("pluginLocation", "/media/plugins/");
+        $this->config->setDefault("themeLocation", "/media/themes/");
+        $this->config->setDefault("assetLocation", "/media/themes/assets/");
+        $this->config->setDefault("mediaDir", "media/");
+        $this->config->setDefault("downloadDir", "media/downloads/");
+        $this->config->setDefault("imageDir", "media/images/");
+        $this->config->setDefault("systemDir", "system/");
+        $this->config->setDefault("configDir", "system/config/");
+        $this->config->setDefault("pluginDir", "system/plugins/");
+        $this->config->setDefault("themeDir", "system/themes/");
+        $this->config->setDefault("assetDir", "system/themes/assets/");
+        $this->config->setDefault("snippetDir", "system/themes/snippets/");
+        $this->config->setDefault("templateDir", "system/themes/templates/");
+        $this->config->setDefault("trashDir", "system/trash/");
+        $this->config->setDefault("contentDir", "content/");
+        $this->config->setDefault("contentRootDir", "default/");
+        $this->config->setDefault("contentHomeDir", "home/");
+        $this->config->setDefault("contentPagination", "page");
+        $this->config->setDefault("contentDefaultFile", "page.txt");
+        $this->config->setDefault("contentExtension", ".txt");
+        $this->config->setDefault("configExtension", ".ini");
+        $this->config->setDefault("downloadExtension", ".download");
+        $this->config->setDefault("configFile", "config.ini");
+        $this->config->setDefault("textFile", "text.ini");
+        $this->config->setDefault("languageFile", "language-(.*).txt");
+        $this->config->setDefault("errorFile", "page-error-(.*).txt");
+        $this->config->setDefault("newFile", "page-new-(.*).txt");
+        $this->config->setDefault("robotsFile", "robots.txt");
+        $this->config->setDefault("faviconFile", "favicon.ico");
+        $this->config->setDefault("serverUrl", "");
+        $this->config->setDefault("template", "default");
+        $this->config->setDefault("navigation", "navigation");
+        $this->config->setDefault("sidebar", "sidebar");
+        $this->config->setDefault("siteicon", "icon");
+        $this->config->setDefault("tagline", "");
+        $this->config->setDefault("parser", "markdown");
+        $this->config->setDefault("parserSafeMode", "0");
+        $this->config->setDefault("multiLanguageMode", "0");
+        $this->config->setDefault("installationMode", "0");
+        $this->config->setDefault("startupUpdate", "none");
+    }
+    
+    public function __destruct() {
+        $this->shutdown();
+    }
+    
+    // Handle initialisation
+    public function load() {
+        if (defined("DEBUG") && DEBUG>=2) {
+            $serverVersion = $this->toolbox->getServerVersion();
+            echo "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion<br/>\n";
+        }
+        $this->toolbox->timerStart($time);
+        $this->config->load($this->config->get("configDir").$this->config->get("configFile"));
+        $this->lookup->load();
+        $this->themes->load();
+        $this->plugins->load();
+        $this->text->load($this->config->get("pluginDir").$this->config->get("languageFile"), "");
+        $this->text->load($this->config->get("configDir").$this->config->get("textFile"), $this->config->get("language"));
+        $this->toolbox->timerStop($time);
+        $this->startup();
+        if (defined("DEBUG") && DEBUG>=2) {
+            $plugins = count($this->plugins->plugins);
+            $themes = count($this->themes->themes);
+            $languages = count($this->text->text);
+            echo "YellowCore::load plugins:$plugins themes:$themes languages:$languages time:$time ms<br/>\n";
+        }
+    }
+    
+    // Handle request
+    public function request() {
+        ob_start();
+        $statusCode = 0;
+        $this->toolbox->timerStart($time);
+        list($scheme, $address, $base, $location, $fileName) = $this->getRequestInformation();
+        $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName);
+        foreach ($this->plugins->plugins as $key=>$value) {
+            if (method_exists($value["obj"], "onRequest")) {
+                $this->lookup->requestHandler = $key;
+                $statusCode = $value["obj"]->onRequest($scheme, $address, $base, $location, $fileName);
+                if ($statusCode!=0) break;
+            }
+        }
+        if ($statusCode==0) {
+            $this->lookup->requestHandler = "core";
+            $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName, true);
+        }
+        if ($this->page->isExisting("pageError")) $statusCode = $this->processRequestError();
+        $this->toolbox->timerStop($time);
+        ob_end_flush();
+        if (defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName)) {
+            $handler = $this->getRequestHandler();
+            echo "YellowCore::request status:$statusCode handler:$handler time:$time ms<br/>\n";
+        }
+        return $statusCode;
+    }
+    
+    // Process request
+    public function processRequest($scheme, $address, $base, $location, $fileName, $cacheable) {
+        $statusCode = 0;
+        if (is_readable($fileName)) {
+            if ($this->toolbox->isRequestCleanUrl($location)) {
+                $location = $location.$this->getRequestLocationArgsClean();
+                $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location);
+                $statusCode = $this->sendStatus(303, $location);
+            }
+        } else {
+            if ($this->lookup->isRedirectLocation($location)) {
+                $location = $this->lookup->isFileLocation($location) ? "$location/" : "/".$this->getRequestLanguage()."/";
+                $location = $this->lookup->normaliseUrl($scheme, $address, $base, $location);
+                $statusCode = $this->sendStatus(301, $location);
+            }
+        }
+        if ($statusCode==0) {
+            $fileName = $this->lookup->findFileStatic($location, $fileName, $cacheable && !$this->isCommandLine());
+            if ($this->lookup->isContentFile($fileName) || !is_readable($fileName)) {
+                $fileName = $this->readPage($scheme, $address, $base, $location, $fileName, $cacheable,
+                    max(is_readable($fileName) ? 200 : 404, $this->page->statusCode), $this->page->get("pageError"));
+                $statusCode = $this->sendPage();
+            } else {
+                $statusCode = $this->sendFile(200, $fileName, true);
+            }
+        }
+        if (defined("DEBUG") && DEBUG>=1 && $this->lookup->isContentFile($fileName)) {
+            echo "YellowCore::processRequest file:$fileName<br/>\n";
+        }
+        return $statusCode;
+    }
+    
+    // Process request with error
+    public function processRequestError() {
+        ob_clean();
+        $fileName = $this->readPage($this->page->scheme, $this->page->address, $this->page->base,
+            $this->page->location, $this->page->fileName, $this->page->cacheable, $this->page->statusCode,
+            $this->page->get("pageError"));
+        $statusCode = $this->sendPage();
+        if (defined("DEBUG") && DEBUG>=1) echo "YellowCore::processRequestError file:$fileName<br/>\n";
+        return $statusCode;
+    }
+    
+    // Read page
+    public function readPage($scheme, $address, $base, $location, $fileName, $cacheable, $statusCode, $pageError) {
+        if ($statusCode>=400) {
+            $cacheable = false;
+            $fileName = $this->config->get("configDir").$this->config->get("errorFile");
+            $fileName = strreplaceu("(.*)", $statusCode, $fileName);
+            $rawData = $this->toolbox->readFile($fileName);
+            if (empty($rawData)) $rawData = "---\nTitle:".$this->toolbox->getHttpStatusFormatted($statusCode, true)."\n---\n";
+        } else {
+            $rawData = $this->toolbox->readFile($fileName);
+        }
+        $this->page = new YellowPage($this);
+        $this->page->setRequestInformation($scheme, $address, $base, $location, $fileName);
+        $this->page->parseData($rawData, $cacheable, $statusCode, $pageError);
+        $this->text->setLanguage($this->page->get("language"));
+        $this->page->parseContent();
+        return $fileName;
+    }
+    
+    // Send page response
+    public function sendPage() {
+        $this->page->parsePage();
+        $statusCode = $this->page->statusCode;
+        $lastModifiedFormatted = $this->page->getHeader("Last-Modified");
+        if ($statusCode==200 && $this->page->isCacheable() && $this->toolbox->isRequestNotModified($lastModifiedFormatted)) {
+            $statusCode = 304;
+            @header($this->toolbox->getHttpStatusFormatted($statusCode));
+        } else {
+            @header($this->toolbox->getHttpStatusFormatted($statusCode));
+            foreach ($this->page->headerData as $key=>$value) {
+                @header("$key: $value");
+            }
+            if (!is_null($this->page->outputData)) echo $this->page->outputData;
+        }
+        if (defined("DEBUG") && DEBUG>=1) {
+            foreach ($this->page->headerData as $key=>$value) {
+                echo "YellowCore::sendPage $key: $value<br/>\n";
+            }
+            $theme = $this->page->get("theme");
+            $template = $this->page->get("template");
+            $parser = $this->page->get("parser");
+            echo "YellowCore::sendPage theme:$theme template:$template parser:$parser<br/>\n";
+        }
+        return $statusCode;
+    }
+    
+    // Send file response
+    public function sendFile($statusCode, $fileName, $cacheable) {
+        $lastModifiedFormatted = $this->toolbox->getHttpDateFormatted($this->toolbox->getFileModified($fileName));
+        if ($statusCode==200 && $cacheable && $this->toolbox->isRequestNotModified($lastModifiedFormatted)) {
+            $statusCode = 304;
+            @header($this->toolbox->getHttpStatusFormatted($statusCode));
+        } else {
+            @header($this->toolbox->getHttpStatusFormatted($statusCode));
+            if (!$cacheable) @header("Cache-Control: no-cache, must-revalidate");
+            @header("Content-Type: ".$this->toolbox->getMimeContentType($fileName));
+            @header("Last-Modified: ".$lastModifiedFormatted);
+            echo $this->toolbox->readFile($fileName);
+        }
+        return $statusCode;
+    }
+    
+    // Send data response
+    public function sendData($statusCode, $rawData, $fileName, $cacheable) {
+        @header($this->toolbox->getHttpStatusFormatted($statusCode));
+        if (!$cacheable) @header("Cache-Control: no-cache, must-revalidate");
+        @header("Content-Type: ".$this->toolbox->getMimeContentType($fileName));
+        @header("Last-Modified: ".$this->toolbox->getHttpDateFormatted(time()));
+        echo $rawData;
+        return $statusCode;
+    }
 
-	// Send status response
-	function sendStatus($statusCode, $location = "")
-	{
-		if(!empty($location)) $this->page->clean($statusCode, $location);
-		@header($this->toolbox->getHttpStatusFormatted($statusCode));
-		foreach($this->page->headerData as $key=>$value) @header("$key: $value");
-		if(defined("DEBUG") && DEBUG>=1)
-		{
-			foreach($this->page->headerData as $key=>$value) echo "YellowCore::sendStatus $key: $value<br/>\n";
-		}
-		return $statusCode;
-	}
-	
-	// Handle command
-	function command($args = null)
-	{
-		$statusCode = 0;
-		$this->toolbox->timerStart($time);
-		foreach($this->plugins->plugins as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onCommand"))
-			{
-				$this->lookup->commandHandler = $key;
-				$statusCode = $value["obj"]->onCommand(func_get_args());
-				if($statusCode!=0) break;
-			}
-		}
-		if($statusCode==0)
-		{
-			$this->lookup->commandHandler = "core";
-			$statusCode = 400;
-			list($command) = func_get_args();
-			echo "Yellow $command: Command not found\n";
-		}
-		$this->toolbox->timerStop($time);
-		if(defined("DEBUG") && DEBUG>=1)
-		{
-			$handler = $this->getCommandHandler();
-			echo "YellowCore::command status:$statusCode handler:$handler time:$time ms<br/>\n";
-		}
-		return $statusCode;
-	}
-	
-	// Handle startup
-	function startup()
-	{
-		$tokens = explode(',', $this->config->get("startupUpdate"));
-		foreach($this->plugins->plugins as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onStartup")) $value["obj"]->onStartup(in_array($value["plugin"], $tokens));
-		}
-		foreach($this->themes->themes as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onStartup")) $value["obj"]->onStartup(in_array($value["theme"], $tokens));
-		}
-		if($this->config->get("startupUpdate")!="none")
-		{
-			$fileNameConfig = $this->config->get("configDir").$this->config->get("configFile");
-			$this->config->save($fileNameConfig, array("startupUpdate" => "none"));
-		}
-	}
-	
-	// Handle shutdown
-	function shutdown()
-	{
-		foreach($this->plugins->plugins as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onShutdown")) $value["obj"]->onShutdown();
-		}
-		foreach($this->themes->themes as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onShutdown")) $value["obj"]->onShutdown();
-		}
-	}
-	
-	// Parse snippet
-	function snippet($name, $args = null)
-	{
-		$this->lookup->snippetArgs = func_get_args();
-		$this->page->parseSnippet($name);
-	}
-	
-	// Return snippet arguments
-	function getSnippetArgs()
-	{
-		return $this->lookup->snippetArgs;
-	}
-	
-	// Return request information
-	function getRequestInformation($scheme = "", $address = "", $base = "")
-	{
-		if(empty($scheme) && empty($address) && empty($base))
-		{
-			$url = $this->config->get("serverUrl");
-			if(empty($url) || $this->isCommandLine()) $url = $this->toolbox->getServerUrl();
-			list($scheme, $address, $base) = $this->lookup->getUrlInformation($url);
-			$this->config->set("serverScheme", $scheme);
-			$this->config->set("serverAddress", $address);
-			$this->config->set("serverBase", $base);
-			if(defined("DEBUG") && DEBUG>=3) echo "YellowCore::getRequestInformation $scheme://$address$base<br/>\n";
-		}
-		$location = substru($this->toolbox->getLocation(), strlenu($base));
-		if(empty($fileName)) $fileName = $this->lookup->findFileFromSystem($location);
-		if(empty($fileName)) $fileName = $this->lookup->findFileFromMedia($location);
-		if(empty($fileName)) $fileName = $this->lookup->findFileFromLocation($location);
-		return array($scheme, $address, $base, $location, $fileName);
-	}
-	
-	// Return request location
-	function getRequestLocationArgsClean()
-	{
-		return $this->toolbox->getLocationArgsClean($this->config->get("contentPagination"));
-	}
-	
-	// Return request language
-	function getRequestLanguage()
-	{
-		return $this->toolbox->detectBrowserLanguage($this->pages->getLanguages(), $this->config->get("language"));
-	}
-	
-	// Return request handler
-	function getRequestHandler()
-	{
-		return $this->lookup->requestHandler;
-	}
+    // Send status response
+    public function sendStatus($statusCode, $location = "") {
+        if (!empty($location)) $this->page->clean($statusCode, $location);
+        @header($this->toolbox->getHttpStatusFormatted($statusCode));
+        foreach ($this->page->headerData as $key=>$value) {
+            @header("$key: $value");
+        }
+        if (defined("DEBUG") && DEBUG>=1) {
+            foreach ($this->page->headerData as $key=>$value) {
+                echo "YellowCore::sendStatus $key: $value<br/>\n";
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Handle command
+    public function command($args = null) {
+        $statusCode = 0;
+        $this->toolbox->timerStart($time);
+        foreach ($this->plugins->plugins as $key=>$value) {
+            if (method_exists($value["obj"], "onCommand")) {
+                $this->lookup->commandHandler = $key;
+                $statusCode = $value["obj"]->onCommand(func_get_args());
+                if ($statusCode!=0) break;
+            }
+        }
+        if ($statusCode==0) {
+            $this->lookup->commandHandler = "core";
+            $statusCode = 400;
+            list($command) = func_get_args();
+            echo "Yellow $command: Command not found\n";
+        }
+        $this->toolbox->timerStop($time);
+        if (defined("DEBUG") && DEBUG>=1) {
+            $handler = $this->getCommandHandler();
+            echo "YellowCore::command status:$statusCode handler:$handler time:$time ms<br/>\n";
+        }
+        return $statusCode;
+    }
+    
+    // Handle startup
+    public function startup() {
+        $tokens = explode(",", $this->config->get("startupUpdate"));
+        foreach ($this->plugins->plugins as $key=>$value) {
+            if (method_exists($value["obj"], "onStartup")) $value["obj"]->onStartup(in_array($value["plugin"], $tokens));
+        }
+        foreach ($this->themes->themes as $key=>$value) {
+            if (method_exists($value["obj"], "onStartup")) $value["obj"]->onStartup(in_array($value["theme"], $tokens));
+        }
+        if ($this->config->get("startupUpdate")!="none") {
+            $fileNameConfig = $this->config->get("configDir").$this->config->get("configFile");
+            $this->config->save($fileNameConfig, array("startupUpdate" => "none"));
+        }
+    }
+    
+    // Handle shutdown
+    public function shutdown() {
+        foreach ($this->plugins->plugins as $key=>$value) {
+            if (method_exists($value["obj"], "onShutdown")) $value["obj"]->onShutdown();
+        }
+        foreach ($this->themes->themes as $key=>$value) {
+            if (method_exists($value["obj"], "onShutdown")) $value["obj"]->onShutdown();
+        }
+    }
+    
+    // Parse snippet
+    public function snippet($name, $args = null) {
+        $this->lookup->snippetArgs = func_get_args();
+        $this->page->parseSnippet($name);
+    }
+    
+    // Return snippet arguments
+    public function getSnippetArgs() {
+        return $this->lookup->snippetArgs;
+    }
+    
+    // Return request information
+    public function getRequestInformation($scheme = "", $address = "", $base = "") {
+        if (empty($scheme) && empty($address) && empty($base)) {
+            $url = $this->config->get("serverUrl");
+            if (empty($url) || $this->isCommandLine()) $url = $this->toolbox->getServerUrl();
+            list($scheme, $address, $base) = $this->lookup->getUrlInformation($url);
+            $this->config->set("serverScheme", $scheme);
+            $this->config->set("serverAddress", $address);
+            $this->config->set("serverBase", $base);
+            if (defined("DEBUG") && DEBUG>=3) echo "YellowCore::getRequestInformation $scheme://$address$base<br/>\n";
+        }
+        $location = substru($this->toolbox->getLocation(), strlenu($base));
+        if (empty($fileName)) $fileName = $this->lookup->findFileFromSystem($location);
+        if (empty($fileName)) $fileName = $this->lookup->findFileFromMedia($location);
+        if (empty($fileName)) $fileName = $this->lookup->findFileFromLocation($location);
+        return array($scheme, $address, $base, $location, $fileName);
+    }
+    
+    // Return request location
+    public function getRequestLocationArgsClean() {
+        return $this->toolbox->getLocationArgsClean($this->config->get("contentPagination"));
+    }
+    
+    // Return request language
+    public function getRequestLanguage() {
+        return $this->toolbox->detectBrowserLanguage($this->pages->getLanguages(), $this->config->get("language"));
+    }
+    
+    // Return request handler
+    public function getRequestHandler() {
+        return $this->lookup->requestHandler;
+    }
 
-	// Return command handler
-	function getCommandHandler()
-	{
-		return $this->lookup->commandHandler;
-	}
-	
-	// Check if running at command line
-	function isCommandLine()
-	{
-		return !empty($this->lookup->commandHandler);
-	}
+    // Return command handler
+    public function getCommandHandler() {
+        return $this->lookup->commandHandler;
+    }
+    
+    // Check if running at command line
+    public function isCommandLine() {
+        return !empty($this->lookup->commandHandler);
+    }
 }
-	
-class YellowPage
-{
-	var $yellow;				//access to API
-	var $scheme;				//server scheme
-	var $address;				//server address
-	var $base;					//base location
-	var $location;				//page location
-	var $fileName;				//content file name
-	var $rawData;				//raw data of page
-	var $metaDataOffsetBytes;	//meta data offset
-	var $metaData;				//meta data
-	var $pageCollection;		//page collection
-	var $pageRelations;			//page relations
-	var $headerData;			//response header
-	var $outputData;			//response output
-	var $parser;				//content parser
-	var $parserData;			//content data of page
-	var $parserSafeMode;		//page is parsed in safe mode? (boolean)
-	var $available;				//page is available? (boolean)
-	var $visible;				//page is visible location? (boolean)
-	var $active;				//page is active location? (boolean)
-	var $cacheable;				//page is cacheable? (boolean)
-	var $lastModified;			//last modification date
-	var $statusCode;			//status code
+    
+class YellowPage {
+    public $yellow;                 //access to API
+    public $scheme;                 //server scheme
+    public $address;                //server address
+    public $base;                   //base location
+    public $location;               //page location
+    public $fileName;               //content file name
+    public $rawData;                //raw data of page
+    public $metaDataOffsetBytes;    //meta data offset
+    public $metaData;               //meta data
+    public $pageCollection;         //page collection
+    public $pageRelations;          //page relations
+    public $headerData;             //response header
+    public $outputData;             //response output
+    public $parser;                 //content parser
+    public $parserData;             //content data of page
+    public $parserSafeMode;         //page is parsed in safe mode? (boolean)
+    public $available;              //page is available? (boolean)
+    public $visible;                //page is visible location? (boolean)
+    public $active;                 //page is active location? (boolean)
+    public $cacheable;              //page is cacheable? (boolean)
+    public $lastModified;           //last modification date
+    public $statusCode;             //status code
 
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->metaData = new YellowDataCollection();
-		$this->pageCollection = new YellowPageCollection($yellow);
-		$this->pageRelations = array();
-		$this->headerData = array();
-	}
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+        $this->metaData = new YellowDataCollection();
+        $this->pageCollection = new YellowPageCollection($yellow);
+        $this->pageRelations = array();
+        $this->headerData = array();
+    }
 
-	// Set request information
-	function setRequestInformation($scheme, $address, $base, $location, $fileName)
-	{
-		$this->scheme = $scheme;
-		$this->address = $address;
-		$this->base = $base;
-		$this->location = $location;
-		$this->fileName = $fileName;
-	}
-	
-	// Parse page data
-	function parseData($rawData, $cacheable, $statusCode, $pageError = "")
-	{
-		$this->rawData = $rawData;
-		$this->parser = null;
-		$this->parserData = "";
-		$this->parserSafeMode = intval($this->yellow->config->get("parserSafeMode"));
-		$this->available = true;
-		$this->visible = $this->yellow->lookup->isVisibleLocation($this->location, $this->fileName);
-		$this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location);
-		$this->cacheable = $cacheable;
-		$this->lastModified = 0;
-		$this->statusCode = $statusCode;
-		$this->parseMeta($pageError);
-	}
-	
-	// Parse page data update
-	function parseDataUpdate()
-	{
-		if($this->statusCode==0)
-		{
-			$this->rawData = $this->yellow->toolbox->readFile($this->fileName);
-			$this->statusCode = 200;
-			$this->parseMeta();
-		}
-	}
-	
-	// Parse page meta data
-	function parseMeta($pageError = "")
-	{
-		$this->metaData = new YellowDataCollection();
-		if(!is_null($this->rawData))
-		{
-			$this->set("title", $this->yellow->toolbox->createTextTitle($this->location));
-			$this->set("language", $this->yellow->lookup->findLanguageFromFile($this->fileName, $this->yellow->config->get("language")));
-			$this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName)));
-			$this->parseMetaRaw(array("theme", "template", "sitename", "siteicon", "tagline", "author", "navigation", "sidebar", "parser"));
-			$titleHeader = ($this->location==$this->yellow->pages->getHomeLocation($this->location)) ?
-				$this->get("sitename") : $this->get("title")." - ".$this->get("sitename");
-			if(!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title"));
-			if(!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title"));
-			if(!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader);
-			if($this->get("status")=="hidden") $this->available = false;
-			$this->set("pageRead", $this->yellow->lookup->normaliseUrl(
-				$this->yellow->config->get("serverScheme"),
-				$this->yellow->config->get("serverAddress"),
-				$this->yellow->config->get("serverBase"), $this->location));
-			$this->set("pageEdit", $this->yellow->lookup->normaliseUrl(
-				$this->yellow->config->get("serverScheme"),
-				$this->yellow->config->get("serverAddress"),
-				$this->yellow->config->get("serverBase"),
-				rtrim($this->yellow->config->get("editLocation"), '/').$this->location));
-		} else {
-			$this->set("type", $this->yellow->toolbox->getFileType($this->fileName));
-			$this->set("group", $this->yellow->toolbox->getFileGroup($this->fileName, $this->yellow->config->get("mediaDir")));
-			$this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName)));
-		}
-		if(!empty($pageError)) $this->set("pageError", $pageError);
-		foreach($this->yellow->plugins->plugins as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onParseMeta")) $value["obj"]->onParseMeta($this);
-		}
-	}
-	
-	// Parse page meta data from raw data
-	function parseMetaRaw($defaultKeys)
-	{
-		foreach($defaultKeys as $key)
-		{
-			$value = $this->yellow->config->get($key);
-			if(!empty($key) && !strempty($value)) $this->set($key, $value);
-		}
-		if(preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+/s", $this->rawData, $parts))
-		{
-			$this->metaDataOffsetBytes = strlenb($parts[0]);
-			foreach(preg_split("/[\r\n]+/", $parts[2]) as $line)
-			{
-				preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-				if(!empty($matches[1]) && !strempty($matches[2])) $this->set($matches[1], $matches[2]);
-			}
-		} else if(preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parts)) {
-			$this->metaDataOffsetBytes = strlenb($parts[0]);
-			$this->set("title", $parts[2]);
-		}
-	}
-	
-	// Parse page content on demand
-	function parseContent($sizeMax = 0)
-	{
-		if(!is_object($this->parser))
-		{
-			if($this->yellow->plugins->isExisting($this->get("parser")))
-			{
-				$plugin = $this->yellow->plugins->plugins[$this->get("parser")];
-				if(method_exists($plugin["obj"], "onParseContentRaw"))
-				{
-					$this->parser = $plugin["obj"];
-					$this->parserData = $this->getContent(true, $sizeMax);
-					$this->parserData = preg_replace("/@pageRead/i", $this->get("pageRead"), $this->parserData);
-					$this->parserData = preg_replace("/@pageEdit/i", $this->get("pageEdit"), $this->parserData);
-					$this->parserData = $this->parser->onParseContentRaw($this, $this->parserData);
-					foreach($this->yellow->plugins->plugins as $key=>$value)
-					{
-						if(method_exists($value["obj"], "onParseContentText"))
-						{
-							$output = $value["obj"]->onParseContentText($this, $this->parserData);
-							if(!is_null($output)) $this->parserData = $output;
-						}
-					}
-				}
-			} else {
-				$this->parserData = $this->getContent(true, $sizeMax);
-				$this->parserData = preg_replace("/\[yellow error\]/i", $this->get("pageError"), $this->parserData);
-			}
-			if(!$this->isExisting("description"))
-			{
-				$this->set("description", $this->yellow->toolbox->createTextDescription($this->parserData, 150));
-			}
-			if(!$this->isExisting("keywords"))
-			{
-				$this->set("keywords", $this->yellow->toolbox->createTextKeywords($this->get("title"), 10));
-			}
-			if(defined("DEBUG") && DEBUG>=3) echo "YellowPage::parseContent location:".$this->location."<br/>\n";
-		}
-	}
-	
-	// Parse page content block
-	function parseContentBlock($name, $text, $shortcut)
-	{
-		$output = null;
-		foreach($this->yellow->plugins->plugins as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onParseContentBlock"))
-			{
-				$output = $value["obj"]->onParseContentBlock($this, $name, $text, $shortcut);
-				if(!is_null($output)) break;
-			}
-		}
-		if(is_null($output))
-		{
-			if($name=="yellow" && $shortcut)
-			{
-				$output = "Datenstrom Yellow ".YellowCore::VERSION;
-				if($text=="error") $output = $this->get("pageError");
-				if($text=="version")
-				{
-					$output = "<span class=\"".htmlspecialchars($name)."\">\n";
-					$serverVersion = $this->yellow->toolbox->getServerVersion();
-					$output .= "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion<br />\n";
-					foreach(array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData()) as $key=>$value)
-					{
-						$output .= htmlspecialchars("$key $value")."<br />\n";
-					}
-					$output .= "</span>\n";
-					if($this->parserSafeMode) $this->error(500, "Yellow '$text' is not available in safe mode!");
-				}
-			}
-		}
-		if(defined("DEBUG") && DEBUG>=3 && !empty($name)) echo "YellowPage::parseContentBlock name:$name shortcut:$shortcut<br/>\n";
-		return $output;
-	}
-	
-	// Parse page
-	function parsePage()
-	{
-		$this->outputData = null;
-		if(!$this->isError())
-		{
-			foreach($this->yellow->plugins->plugins as $key=>$value)
-			{
-				if(method_exists($value["obj"], "onParsePage")) $value["obj"]->onParsePage();
-			}
-		}
-		if(is_null($this->outputData))
-		{
-			ob_start();
-			$this->parseTemplate($this->get("template"));
-			$this->outputData = ob_get_contents();
-			ob_end_clean();
-		}
-		if(!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, must-revalidate");
-		if(!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8");
-		if(!$this->isHeader("Page-Modified")) $this->setHeader("Page-Modified", $this->getModified(true));
-		if(!$this->isHeader("Last-Modified")) $this->setHeader("Last-Modified", $this->getLastModified(true));
-		if(!$this->yellow->text->isLanguage($this->get("language")))
-		{
-			$this->error(500, "Language '".$this->get("language")."' does not exist!");
-		}
-		if(!$this->yellow->themes->isExisting($this->get("theme")))
-		{
-			$this->error(500, "Theme '".$this->get("theme")."' does not exist!");
-		}
-		if(!is_object($this->parser))
-		{
-			$this->error(500, "Parser '".$this->get("parser")."' does not exist!");
-		}
-		if($this->yellow->lookup->isNestedLocation($this->location, $this->fileName, true))
-		{
-			$this->error(500, "Folder '".dirname($this->fileName)."' may not contain subfolders!");
-		}
-		if($this->yellow->toolbox->isRequestSelf())
-		{
-			$serverVersion = $this->yellow->toolbox->getServerVersion(true);
-			$this->error(500, "Rewrite module not working on $serverVersion web server!");
-		}
-		if($this->yellow->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200)
-		{
-			$location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->location);
-			$location = $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, "", $location);
-			$this->clean(301, $location);
-		}
-		if($this->yellow->getRequestHandler()=="core" && !$this->isAvailable() && $this->statusCode==200)
-		{
-			$this->error(404);
-		}
-		if($this->isExisting("pageClean")) $this->outputData = null;
-	}
-	
-	// Parse template
-	function parseTemplate($name)
-	{
-		$fileNameTemplate = $this->yellow->config->get("templateDir").$this->yellow->lookup->normaliseName($name).".html";
-		if(is_file($fileNameTemplate))
-		{
-			$this->setLastModified(filemtime($fileNameTemplate));
-			global $yellow;
-			require($fileNameTemplate);
-		} else {
-			$this->error(500, "Template '$name' does not exist!");
-			echo "Template error<br/>\n";
-		}
-	}
-	
-	// Parse snippet
-	function parseSnippet($name)
-	{
-		$fileNameSnippet = $this->yellow->config->get("snippetDir").$this->yellow->lookup->normaliseName($name).".php";
-		if(is_file($fileNameSnippet))
-		{
-			$this->setLastModified(filemtime($fileNameSnippet));
-			global $yellow;
-			require($fileNameSnippet);
-		} else {
-			$this->error(500, "Snippet '$name' does not exist!");
-			echo "Snippet error<br/>\n";
-		}
-	}
-	
-	// Set page meta data
-	function set($key, $value)
-	{
-		$this->metaData[$key] = $value;
-	}
-	
-	// Return page meta data
-	function get($key)
-	{
-		return $this->isExisting($key) ? $this->metaData[$key] : "";
-	}
+    // Set request information
+    public function setRequestInformation($scheme, $address, $base, $location, $fileName) {
+        $this->scheme = $scheme;
+        $this->address = $address;
+        $this->base = $base;
+        $this->location = $location;
+        $this->fileName = $fileName;
+    }
+    
+    // Parse page data
+    public function parseData($rawData, $cacheable, $statusCode, $pageError = "") {
+        $this->rawData = $rawData;
+        $this->parser = null;
+        $this->parserData = "";
+        $this->parserSafeMode = intval($this->yellow->config->get("parserSafeMode"));
+        $this->available = true;
+        $this->visible = $this->yellow->lookup->isVisibleLocation($this->location, $this->fileName);
+        $this->active = $this->yellow->lookup->isActiveLocation($this->location, $this->yellow->page->location);
+        $this->cacheable = $cacheable;
+        $this->lastModified = 0;
+        $this->statusCode = $statusCode;
+        $this->parseMeta($pageError);
+    }
+    
+    // Parse page data update
+    public function parseDataUpdate() {
+        if ($this->statusCode==0) {
+            $this->rawData = $this->yellow->toolbox->readFile($this->fileName);
+            $this->statusCode = 200;
+            $this->parseMeta();
+        }
+    }
+    
+    // Parse page meta data
+    public function parseMeta($pageError = "") {
+        $this->metaData = new YellowDataCollection();
+        if (!is_null($this->rawData)) {
+            $this->set("title", $this->yellow->toolbox->createTextTitle($this->location));
+            $this->set("language", $this->yellow->lookup->findLanguageFromFile($this->fileName, $this->yellow->config->get("language")));
+            $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName)));
+            $this->parseMetaRaw(array("theme", "template", "sitename", "siteicon", "tagline", "author", "navigation", "sidebar", "parser"));
+            $titleHeader = ($this->location==$this->yellow->pages->getHomeLocation($this->location)) ?
+                $this->get("sitename") : $this->get("title")." - ".$this->get("sitename");
+            if (!$this->isExisting("titleContent")) $this->set("titleContent", $this->get("title"));
+            if (!$this->isExisting("titleNavigation")) $this->set("titleNavigation", $this->get("title"));
+            if (!$this->isExisting("titleHeader")) $this->set("titleHeader", $titleHeader);
+            if ($this->get("status")=="hidden") $this->available = false;
+            $this->set("pageRead", $this->yellow->lookup->normaliseUrl(
+                $this->yellow->config->get("serverScheme"),
+                $this->yellow->config->get("serverAddress"),
+                $this->yellow->config->get("serverBase"),
+                $this->location));
+            $this->set("pageEdit", $this->yellow->lookup->normaliseUrl(
+                $this->yellow->config->get("serverScheme"),
+                $this->yellow->config->get("serverAddress"),
+                $this->yellow->config->get("serverBase"),
+                rtrim($this->yellow->config->get("editLocation"), "/").$this->location));
+        } else {
+            $this->set("type", $this->yellow->toolbox->getFileType($this->fileName));
+            $this->set("group", $this->yellow->toolbox->getFileGroup($this->fileName, $this->yellow->config->get("mediaDir")));
+            $this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName)));
+        }
+        if (!empty($pageError)) $this->set("pageError", $pageError);
+        foreach ($this->yellow->plugins->plugins as $key=>$value) {
+            if (method_exists($value["obj"], "onParseMeta")) $value["obj"]->onParseMeta($this);
+        }
+    }
+    
+    // Parse page meta data from raw data
+    public function parseMetaRaw($defaultKeys) {
+        foreach ($defaultKeys as $key) {
+            $value = $this->yellow->config->get($key);
+            if (!empty($key) && !strempty($value)) $this->set($key, $value);
+        }
+        if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+/s", $this->rawData, $parts)) {
+            $this->metaDataOffsetBytes = strlenb($parts[0]);
+            foreach (preg_split("/[\r\n]+/", $parts[2]) as $line) {
+                preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+                if (!empty($matches[1]) && !strempty($matches[2])) $this->set($matches[1], $matches[2]);
+            }
+        } elseif (preg_match("/^(\xEF\xBB\xBF)?([^\r\n]+)[\r\n]+=+[\r\n]+/", $this->rawData, $parts)) {
+            $this->metaDataOffsetBytes = strlenb($parts[0]);
+            $this->set("title", $parts[2]);
+        }
+    }
+    
+    // Parse page content on demand
+    public function parseContent($sizeMax = 0) {
+        if (!is_object($this->parser)) {
+            if ($this->yellow->plugins->isExisting($this->get("parser"))) {
+                $plugin = $this->yellow->plugins->plugins[$this->get("parser")];
+                if (method_exists($plugin["obj"], "onParseContentRaw")) {
+                    $this->parser = $plugin["obj"];
+                    $this->parserData = $this->getContent(true, $sizeMax);
+                    $this->parserData = preg_replace("/@pageRead/i", $this->get("pageRead"), $this->parserData);
+                    $this->parserData = preg_replace("/@pageEdit/i", $this->get("pageEdit"), $this->parserData);
+                    $this->parserData = $this->parser->onParseContentRaw($this, $this->parserData);
+                    foreach ($this->yellow->plugins->plugins as $key=>$value) {
+                        if (method_exists($value["obj"], "onParseContentText")) {
+                            $output = $value["obj"]->onParseContentText($this, $this->parserData);
+                            if (!is_null($output)) $this->parserData = $output;
+                        }
+                    }
+                }
+            } else {
+                $this->parserData = $this->getContent(true, $sizeMax);
+                $this->parserData = preg_replace("/\[yellow error\]/i", $this->get("pageError"), $this->parserData);
+            }
+            if (!$this->isExisting("description")) {
+                $this->set("description", $this->yellow->toolbox->createTextDescription($this->parserData, 150));
+            }
+            if (!$this->isExisting("keywords")) {
+                $this->set("keywords", $this->yellow->toolbox->createTextKeywords($this->get("title"), 10));
+            }
+            if (defined("DEBUG") && DEBUG>=3) echo "YellowPage::parseContent location:".$this->location."<br/>\n";
+        }
+    }
+    
+    // Parse page content block
+    public function parseContentBlock($name, $text, $shortcut) {
+        $output = null;
+        foreach ($this->yellow->plugins->plugins as $key=>$value) {
+            if (method_exists($value["obj"], "onParseContentBlock")) {
+                $output = $value["obj"]->onParseContentBlock($this, $name, $text, $shortcut);
+                if (!is_null($output)) break;
+            }
+        }
+        if (is_null($output)) {
+            if ($name=="yellow" && $shortcut) {
+                $output = "Datenstrom Yellow ".YellowCore::VERSION;
+                if ($text=="error") $output = $this->get("pageError");
+                if ($text=="version") {
+                    $output = "<span class=\"".htmlspecialchars($name)."\">\n";
+                    $serverVersion = $this->yellow->toolbox->getServerVersion();
+                    $output .= "Datenstrom Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion<br />\n";
+                    foreach (array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData()) as $key=>$value) {
+                        $output .= htmlspecialchars("$key $value")."<br />\n";
+                    }
+                    $output .= "</span>\n";
+                    if ($this->parserSafeMode) $this->error(500, "Yellow '$text' is not available in safe mode!");
+                }
+            }
+        }
+        if (defined("DEBUG") && DEBUG>=3 && !empty($name)) echo "YellowPage::parseContentBlock name:$name shortcut:$shortcut<br/>\n";
+        return $output;
+    }
+    
+    // Parse page
+    public function parsePage() {
+        $this->outputData = null;
+        if (!$this->isError()) {
+            foreach ($this->yellow->plugins->plugins as $key=>$value) {
+                if (method_exists($value["obj"], "onParsePage")) $value["obj"]->onParsePage();
+            }
+        }
+        if (is_null($this->outputData)) {
+            ob_start();
+            $this->parseTemplate($this->get("template"));
+            $this->outputData = ob_get_contents();
+            ob_end_clean();
+        }
+        if (!$this->isCacheable()) $this->setHeader("Cache-Control", "no-cache, must-revalidate");
+        if (!$this->isHeader("Content-Type")) $this->setHeader("Content-Type", "text/html; charset=utf-8");
+        if (!$this->isHeader("Page-Modified")) $this->setHeader("Page-Modified", $this->getModified(true));
+        if (!$this->isHeader("Last-Modified")) $this->setHeader("Last-Modified", $this->getLastModified(true));
+        if (!$this->yellow->text->isLanguage($this->get("language"))) {
+            $this->error(500, "Language '".$this->get("language")."' does not exist!");
+        }
+        if (!$this->yellow->themes->isExisting($this->get("theme"))) {
+            $this->error(500, "Theme '".$this->get("theme")."' does not exist!");
+        }
+        if (!is_object($this->parser)) {
+            $this->error(500, "Parser '".$this->get("parser")."' does not exist!");
+        }
+        if ($this->yellow->lookup->isNestedLocation($this->location, $this->fileName, true)) {
+            $this->error(500, "Folder '".dirname($this->fileName)."' may not contain subfolders!");
+        }
+        if ($this->yellow->toolbox->isRequestSelf()) {
+            $serverVersion = $this->yellow->toolbox->getServerVersion(true);
+            $this->error(500, "Rewrite module not working on $serverVersion web server!");
+        }
+        if ($this->yellow->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200) {
+            $location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->location);
+            $location = $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, "", $location);
+            $this->clean(301, $location);
+        }
+        if ($this->yellow->getRequestHandler()=="core" && !$this->isAvailable() && $this->statusCode==200) {
+            $this->error(404);
+        }
+        if ($this->isExisting("pageClean")) $this->outputData = null;
+    }
+    
+    // Parse template
+    public function parseTemplate($name) {
+        $fileNameTemplate = $this->yellow->config->get("templateDir").$this->yellow->lookup->normaliseName($name).".html";
+        if (is_file($fileNameTemplate)) {
+            $this->setLastModified(filemtime($fileNameTemplate));
+            global $yellow;
+            require($fileNameTemplate);
+        } else {
+            $this->error(500, "Template '$name' does not exist!");
+            echo "Template error<br/>\n";
+        }
+    }
+    
+    // Parse snippet
+    public function parseSnippet($name) {
+        $fileNameSnippet = $this->yellow->config->get("snippetDir").$this->yellow->lookup->normaliseName($name).".php";
+        if (is_file($fileNameSnippet)) {
+            $this->setLastModified(filemtime($fileNameSnippet));
+            global $yellow;
+            require($fileNameSnippet);
+        } else {
+            $this->error(500, "Snippet '$name' does not exist!");
+            echo "Snippet error<br/>\n";
+        }
+    }
+    
+    // Set page meta data
+    public function set($key, $value) {
+        $this->metaData[$key] = $value;
+    }
+    
+    // Return page meta data
+    public function get($key) {
+        return $this->isExisting($key) ? $this->metaData[$key] : "";
+    }
 
-	// Return page meta data, HTML encoded
-	function getHtml($key)
-	{
-		return htmlspecialchars($this->get($key));
-	}
-	
-	// Return page meta data as language specific date
-	function getDate($key, $format = "")
-	{
-		if(!empty($format))
-		{
-			$format = $this->yellow->text->get($format);
-		} else {
-			$format = $this->yellow->text->get("dateFormatMedium");
-		}
-		return $this->yellow->text->getDateFormatted(strtotime($this->get($key)), $format);
-	}
+    // Return page meta data, HTML encoded
+    public function getHtml($key) {
+        return htmlspecialchars($this->get($key));
+    }
+    
+    // Return page meta data as language specific date
+    public function getDate($key, $format = "") {
+        if (!empty($format)) {
+            $format = $this->yellow->text->get($format);
+        } else {
+            $format = $this->yellow->text->get("dateFormatMedium");
+        }
+        return $this->yellow->text->getDateFormatted(strtotime($this->get($key)), $format);
+    }
 
-	// Return page meta data as language specific date, HTML encoded
-	function getDateHtml($key, $format = "")
-	{
-		return htmlspecialchars($this->getDate($key, $format));
-	}
+    // Return page meta data as language specific date, HTML encoded
+    public function getDateHtml($key, $format = "") {
+        return htmlspecialchars($this->getDate($key, $format));
+    }
 
-	// Return page meta data as language specific date and relative to today
-	function getDateRelative($key, $format = "", $daysLimit = 0)
-	{
-		if(!empty($format))
-		{
-			$format = $this->yellow->text->get($format);
-		} else {
-			$format = $this->yellow->text->get("dateFormatMedium");
-		}
-		return $this->yellow->text->getDateRelative(strtotime($this->get($key)), $format, $daysLimit);
-	}
-	
-	// Return page meta data as language specific date and relative to today, HTML encoded
-	function getDateRelativeHtml($key, $format = "", $daysLimit = 0)
-	{
-		return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit));
-	}
+    // Return page meta data as language specific date and relative to today
+    public function getDateRelative($key, $format = "", $daysLimit = 0) {
+        if (!empty($format)) {
+            $format = $this->yellow->text->get($format);
+        } else {
+            $format = $this->yellow->text->get("dateFormatMedium");
+        }
+        return $this->yellow->text->getDateRelative(strtotime($this->get($key)), $format, $daysLimit);
+    }
+    
+    // Return page meta data as language specific date and relative to today, HTML encoded
+    public function getDateRelativeHtml($key, $format = "", $daysLimit = 0) {
+        return htmlspecialchars($this->getDateRelative($key, $format, $daysLimit));
+    }
 
-	// Return page meta data as custom date
-	function getDateFormatted($key, $format)
-	{
-		return $this->yellow->text->getDateFormatted(strtotime($this->get($key)), $format);
-	}
-	
-	// Return page meta data as custom date, HTML encoded
-	function getDateFormattedHtml($key, $format)
-	{
-		return htmlspecialchars($this->getDateFormatted($key, $format));
-	}
+    // Return page meta data as custom date
+    public function getDateFormatted($key, $format) {
+        return $this->yellow->text->getDateFormatted(strtotime($this->get($key)), $format);
+    }
+    
+    // Return page meta data as custom date, HTML encoded
+    public function getDateFormattedHtml($key, $format) {
+        return htmlspecialchars($this->getDateFormatted($key, $format));
+    }
 
-	// Return page content, HTML encoded or raw format
-	function getContent($rawFormat = false, $sizeMax = 0)
-	{
-		if($rawFormat)
-		{
-			$this->parseDataUpdate();
-			$text = substrb($this->rawData, $this->metaDataOffsetBytes);
-		} else {
-			$this->parseContent($sizeMax);
-			$text = $this->parserData;
-		}
-		return $sizeMax ? substrb($text, 0, $sizeMax) : $text;
-	}
-	
-	// Return parent page of current page, null if none
-	function getParent()
-	{
-		$parentLocation = $this->yellow->pages->getParentLocation($this->location);
-		return $this->yellow->pages->find($parentLocation);
-	}
-	
-	// Return top-level page for current page, null if none
-	function getParentTop($homeFailback = true)
-	{
-		$parentTopLocation = $this->yellow->pages->getParentTopLocation($this->location);
-		if(!$this->yellow->pages->find($parentTopLocation) && $homeFailback)
-		{
-			$parentTopLocation = $this->yellow->pages->getHomeLocation($this->location);
-		}
-		return $this->yellow->pages->find($parentTopLocation);
-	}
-	
-	// Return page collection with pages on the same level as current page
-	function getSiblings($showInvisible = false)
-	{
-		$parentLocation = $this->yellow->pages->getParentLocation($this->location);
-		return $this->yellow->pages->getChildren($parentLocation, $showInvisible);
-	}
-	
-	// Return page collection with child pages of current page
-	function getChildren($showInvisible = false)
-	{
-		return $this->yellow->pages->getChildren($this->location, $showInvisible);
-	}
+    // Return page content, HTML encoded or raw format
+    public function getContent($rawFormat = false, $sizeMax = 0) {
+        if ($rawFormat) {
+            $this->parseDataUpdate();
+            $text = substrb($this->rawData, $this->metaDataOffsetBytes);
+        } else {
+            $this->parseContent($sizeMax);
+            $text = $this->parserData;
+        }
+        return $sizeMax ? substrb($text, 0, $sizeMax) : $text;
+    }
+    
+    // Return parent page of current page, null if none
+    public function getParent() {
+        $parentLocation = $this->yellow->pages->getParentLocation($this->location);
+        return $this->yellow->pages->find($parentLocation);
+    }
+    
+    // Return top-level page for current page, null if none
+    public function getParentTop($homeFailback = true) {
+        $parentTopLocation = $this->yellow->pages->getParentTopLocation($this->location);
+        if (!$this->yellow->pages->find($parentTopLocation) && $homeFailback) {
+            $parentTopLocation = $this->yellow->pages->getHomeLocation($this->location);
+        }
+        return $this->yellow->pages->find($parentTopLocation);
+    }
+    
+    // Return page collection with pages on the same level as current page
+    public function getSiblings($showInvisible = false) {
+        $parentLocation = $this->yellow->pages->getParentLocation($this->location);
+        return $this->yellow->pages->getChildren($parentLocation, $showInvisible);
+    }
+    
+    // Return page collection with child pages of current page
+    public function getChildren($showInvisible = false) {
+        return $this->yellow->pages->getChildren($this->location, $showInvisible);
+    }
 
-	// Return page collection with sub pages of current page
-	function getChildrenRecursive($showInvisible = false, $levelMax = 0)
-	{
-		return $this->yellow->pages->getChildrenRecursive($this->location, $showInvisible, $levelMax);
-	}
-	
-	// Set page collection with additional pages for current page
-	function setPages($pages)
-	{
-		$this->pageCollection = $pages;
-	}
+    // Return page collection with sub pages of current page
+    public function getChildrenRecursive($showInvisible = false, $levelMax = 0) {
+        return $this->yellow->pages->getChildrenRecursive($this->location, $showInvisible, $levelMax);
+    }
+    
+    // Set page collection with additional pages for current page
+    public function setPages($pages) {
+        $this->pageCollection = $pages;
+    }
 
-	// Return page collection with additional pages for current page
-	function getPages()
-	{
-		return $this->pageCollection;
-	}
-	
-	// Set related page
-	function setPage($key, $page)
-	{
-		$this->pageRelations[$key] = $page;
-	}
-	
-	// Return related page
-	function getPage($key)
-	{
-		return !is_null($this->pageRelations[$key]) ? $this->pageRelations[$key] : $this;
-	}
-	
-	// Return page base
-	function getBase($multiLanguage = false)
-	{
-		return $multiLanguage ? rtrim($this->base.$this->yellow->pages->getHomeLocation($this->location), '/') :  $this->base;
-	}
-	
-	// Return page location
-	function getLocation($absoluteLocation = false)
-	{
-		return $absoluteLocation ? $this->base.$this->location : $this->location;
-	}
-	
-	// Return page URL
-	function getUrl()
-	{
-		return $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, $this->base, $this->location);
-	}
-	
-	// Return page extra HTML data
-	function getExtra($name)
-	{
-		$output = "";
-		foreach($this->yellow->plugins->plugins as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onExtra"))
-			{
-				$outputPlugin = $value["obj"]->onExtra($name);
-				if(!is_null($outputPlugin)) $output .= $outputPlugin;
-			}
-		}
-		if($name=="header")
-		{
-			if(is_file($this->yellow->config->get("assetDir").$this->get("theme").".css"))
-			{
-				$location = $this->yellow->config->get("serverBase").
-					$this->yellow->config->get("assetLocation").$this->get("theme").".css";
-				$output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"".htmlspecialchars($location)."\" />\n";
-			}
-			if(is_file($this->yellow->config->get("assetDir").$this->get("theme").".js"))
-			{
-				$location = $this->yellow->config->get("serverBase").
-					$this->yellow->config->get("assetLocation").$this->get("theme").".js";
-				$output .= "<script type=\"text/javascript\" src=\"".htmlspecialchars($location)."\"></script>\n";
-			}
-			if(is_file($this->yellow->config->get("assetDir").$this->get("siteicon").".png"))
-			{
-				$location = $this->yellow->config->get("serverBase").
-					$this->yellow->config->get("assetLocation").$this->get("siteicon").".png";
-				$contentType = $this->yellow->toolbox->getMimeContentType($location);
-				$output .= "<link rel=\"icon\" type=\"$contentType\" href=\"".htmlspecialchars($location)."\" />\n";
-				$output .= "<link rel=\"apple-touch-icon\" type=\"$contentType\" href=\"".htmlspecialchars($location)."\" />\n";
-			}
-		}
-		return $this->normaliseExtra($output);
-	}
-	
-	// Normalise page extra HTML data
-	function normaliseExtra($text)
-	{
-		$outputScript = $outputStylesheet = $outputOther = $locations = array();
-		foreach($this->yellow->toolbox->getTextLines($text) as $line)
-		{
-			if(preg_match("/^<script (.*?)src=\"([^\"]+)\"(.*?)><\/script>$/i", $line, $matches))
-			{
-				if(is_null($locations[$matches[2]]))
-				{
-					$locations[$matches[2]] = $matches[2];
-					array_push($outputScript, $line);
-				}
-			} else if(preg_match("/^<link rel=\"stylesheet\"(.*?)href=\"([^\"]+)\"(.*?)>$/i", $line, $matches)) {
-				if(is_null($locations[$matches[2]]))
-				{
-					$locations[$matches[2]] = $matches[2];
-					array_push($outputStylesheet, $line);
-				}
-			} else {
-				array_push($outputOther, $line);
-			}
-		}
-		return implode($outputScript).implode($outputStylesheet).implode($outputOther);
-	}
-	
-	// Set page response output
-	function setOutput($output)
-	{
-		$this->outputData = $output;
-	}
-	
-	// Set page response header
-	function setHeader($key, $value)
-	{
-		$this->headerData[$key] = $value;
-	}
-	
-	// Return page response header
-	function getHeader($key)
-	{
-		return $this->isHeader($key) ? $this->headerData[$key] : "";
-	}
-	
-	// Return page modification date, Unix time or HTTP format
-	function getModified($httpFormat = false)
-	{
-		$modified = strtotime($this->get("modified"));
-		return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified;
-	}
-	
-	// Set last modification date, Unix time
-	function setLastModified($modified)
-	{
-		$this->lastModified = max($this->lastModified, $modified);
-	}
-	
-	// Return last modification date, Unix time or HTTP format
-	function getLastModified($httpFormat = false)
-	{
-		$modified = max($this->lastModified, $this->getModified(), $this->yellow->config->getModified(),
-			$this->yellow->text->getModified(), $this->yellow->plugins->getModified());
-		return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified;
-	}
-	
-	// Return page status code, number or HTTP format
-	function getStatusCode($httpFormat = false)
-	{
-		$statusCode = $this->statusCode;
-		if($httpFormat)
-		{
-			$statusCode = $this->yellow->toolbox->getHttpStatusFormatted($statusCode);
-			if($this->isExisting("pageError")) $statusCode .= ": ".$this->get("pageError");
-		}
-		return $statusCode;
-	}
-	
-	// Respond with error page
-	function error($statusCode, $pageError = "")
-	{
-		if(!$this->isExisting("pageError") && $statusCode>0)
-		{
-			$this->statusCode = $statusCode;
-			$this->set("pageError", empty($pageError) ? "Template/snippet error!" : $pageError);
-		}
-	}
-	
-	// Respond with status code, no page content
-	function clean($statusCode, $location = "")
-	{
-		if(!$this->isExisting("pageClean") && $statusCode>0)
-		{
-			$this->statusCode = $statusCode;
-			$this->lastModified = 0;
-			$this->headerData = array();
-			if(!empty($location))
-			{
-				$this->setHeader("Location", $location);
-				$this->setHeader("Cache-Control", "no-cache, must-revalidate");
-			}
-			$this->set("pageClean", (string)$statusCode);
-		}
-	}
-	
-	// Check if page is available
-	function isAvailable()
-	{
-		return $this->available;
-	}
-	
-	// Check if page is visible
-	function isVisible()
-	{
-		return $this->visible;
-	}
+    // Return page collection with additional pages for current page
+    public function getPages() {
+        return $this->pageCollection;
+    }
+    
+    // Set related page
+    public function setPage($key, $page) {
+        $this->pageRelations[$key] = $page;
+    }
+    
+    // Return related page
+    public function getPage($key) {
+        return !is_null($this->pageRelations[$key]) ? $this->pageRelations[$key] : $this;
+    }
+    
+    // Return page base
+    public function getBase($multiLanguage = false) {
+        return $multiLanguage ? rtrim($this->base.$this->yellow->pages->getHomeLocation($this->location), "/") :  $this->base;
+    }
+    
+    // Return page location
+    public function getLocation($absoluteLocation = false) {
+        return $absoluteLocation ? $this->base.$this->location : $this->location;
+    }
+    
+    // Return page URL
+    public function getUrl() {
+        return $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, $this->base, $this->location);
+    }
+    
+    // Return page extra HTML data
+    public function getExtra($name) {
+        $output = "";
+        foreach ($this->yellow->plugins->plugins as $key=>$value) {
+            if (method_exists($value["obj"], "onExtra")) {
+                $outputPlugin = $value["obj"]->onExtra($name);
+                if (!is_null($outputPlugin)) $output .= $outputPlugin;
+            }
+        }
+        if ($name=="header") {
+            if (is_file($this->yellow->config->get("assetDir").$this->get("theme").".css")) {
+                $location = $this->yellow->config->get("serverBase").
+                    $this->yellow->config->get("assetLocation").$this->get("theme").".css";
+                $output .= "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"".htmlspecialchars($location)."\" />\n";
+            }
+            if (is_file($this->yellow->config->get("assetDir").$this->get("theme").".js")) {
+                $location = $this->yellow->config->get("serverBase").
+                    $this->yellow->config->get("assetLocation").$this->get("theme").".js";
+                $output .= "<script type=\"text/javascript\" src=\"".htmlspecialchars($location)."\"></script>\n";
+            }
+            if (is_file($this->yellow->config->get("assetDir").$this->get("siteicon").".png")) {
+                $location = $this->yellow->config->get("serverBase").
+                    $this->yellow->config->get("assetLocation").$this->get("siteicon").".png";
+                $contentType = $this->yellow->toolbox->getMimeContentType($location);
+                $output .= "<link rel=\"icon\" type=\"$contentType\" href=\"".htmlspecialchars($location)."\" />\n";
+                $output .= "<link rel=\"apple-touch-icon\" type=\"$contentType\" href=\"".htmlspecialchars($location)."\" />\n";
+            }
+        }
+        return $this->normaliseExtra($output);
+    }
+    
+    // Normalise page extra HTML data
+    public function normaliseExtra($text) {
+        $outputScript = $outputStylesheet = $outputOther = $locations = array();
+        foreach ($this->yellow->toolbox->getTextLines($text) as $line) {
+            if (preg_match("/^<script (.*?)src=\"([^\"]+)\"(.*?)><\/script>$/i", $line, $matches)) {
+                if (is_null($locations[$matches[2]])) {
+                    $locations[$matches[2]] = $matches[2];
+                    array_push($outputScript, $line);
+                }
+            } elseif (preg_match("/^<link rel=\"stylesheet\"(.*?)href=\"([^\"]+)\"(.*?)>$/i", $line, $matches)) {
+                if (is_null($locations[$matches[2]])) {
+                    $locations[$matches[2]] = $matches[2];
+                    array_push($outputStylesheet, $line);
+                }
+            } else {
+                array_push($outputOther, $line);
+            }
+        }
+        return implode($outputScript).implode($outputStylesheet).implode($outputOther);
+    }
+    
+    // Set page response output
+    public function setOutput($output) {
+        $this->outputData = $output;
+    }
+    
+    // Set page response header
+    public function setHeader($key, $value) {
+        $this->headerData[$key] = $value;
+    }
+    
+    // Return page response header
+    public function getHeader($key) {
+        return $this->isHeader($key) ? $this->headerData[$key] : "";
+    }
+    
+    // Return page modification date, Unix time or HTTP format
+    public function getModified($httpFormat = false) {
+        $modified = strtotime($this->get("modified"));
+        return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified;
+    }
+    
+    // Set last modification date, Unix time
+    public function setLastModified($modified) {
+        $this->lastModified = max($this->lastModified, $modified);
+    }
+    
+    // Return last modification date, Unix time or HTTP format
+    public function getLastModified($httpFormat = false) {
+        $modified = max($this->lastModified, $this->getModified(), $this->yellow->config->getModified(),
+            $this->yellow->text->getModified(), $this->yellow->plugins->getModified());
+        return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified;
+    }
+    
+    // Return page status code, number or HTTP format
+    public function getStatusCode($httpFormat = false) {
+        $statusCode = $this->statusCode;
+        if ($httpFormat) {
+            $statusCode = $this->yellow->toolbox->getHttpStatusFormatted($statusCode);
+            if ($this->isExisting("pageError")) $statusCode .= ": ".$this->get("pageError");
+        }
+        return $statusCode;
+    }
+    
+    // Respond with error page
+    public function error($statusCode, $pageError = "") {
+        if (!$this->isExisting("pageError") && $statusCode>0) {
+            $this->statusCode = $statusCode;
+            $this->set("pageError", empty($pageError) ? "Template/snippet error!" : $pageError);
+        }
+    }
+    
+    // Respond with status code, no page content
+    public function clean($statusCode, $location = "") {
+        if (!$this->isExisting("pageClean") && $statusCode>0) {
+            $this->statusCode = $statusCode;
+            $this->lastModified = 0;
+            $this->headerData = array();
+            if (!empty($location)) {
+                $this->setHeader("Location", $location);
+                $this->setHeader("Cache-Control", "no-cache, must-revalidate");
+            }
+            $this->set("pageClean", (string)$statusCode);
+        }
+    }
+    
+    // Check if page is available
+    public function isAvailable() {
+        return $this->available;
+    }
+    
+    // Check if page is visible
+    public function isVisible() {
+        return $this->visible;
+    }
 
-	// Check if page is within current request
-	function isActive()
-	{
-		return $this->active;
-	}
-	
-	// Check if page is cacheable
-	function isCacheable()
-	{
-		return $this->cacheable;
-	}
+    // Check if page is within current request
+    public function isActive() {
+        return $this->active;
+    }
+    
+    // Check if page is cacheable
+    public function isCacheable() {
+        return $this->cacheable;
+    }
 
-	// Check if page with error
-	function isError()
-	{
-		return $this->statusCode>=400;
-	}
-	
-	// Check if response header exists
-	function isHeader($key)
-	{
-		return !is_null($this->headerData[$key]);
-	}
-	
-	// Check if page meta data exists
-	function isExisting($key)
-	{
-		return !is_null($this->metaData[$key]);
-	}
-	
-	// Check if related page exists
-	function isPage($key)
-	{
-		return !is_null($this->pageRelations[$key]);
-	}
+    // Check if page with error
+    public function isError() {
+        return $this->statusCode>=400;
+    }
+    
+    // Check if response header exists
+    public function isHeader($key) {
+        return !is_null($this->headerData[$key]);
+    }
+    
+    // Check if page meta data exists
+    public function isExisting($key) {
+        return !is_null($this->metaData[$key]);
+    }
+    
+    // Check if related page exists
+    public function isPage($key) {
+        return !is_null($this->pageRelations[$key]);
+    }
 }
 
-class YellowDataCollection extends ArrayObject
-{
-	function __construct()
-	{
-		parent::__construct(array());
-	}
-	
-	// Return array element
-	function offsetGet($key)
-	{
-		if(is_string($key)) $key = lcfirst($key);
-		return parent::offsetGet($key);
-	}
-	
-	// Set array element
-	function offsetSet($key, $value)
-	{
-		if(is_string($key)) $key = lcfirst($key);
-		parent::offsetSet($key, $value);
-	}
-	
-	// Remove array element
-	function offsetUnset($key)
-	{
-		if(is_string($key)) $key = lcfirst($key);
-		parent::offsetUnset($key);
-	}
-	
-	// Check if array element exists
-	function offsetExists($key)
-	{
-		if(is_string($key)) $key = lcfirst($key);
-		return parent::offsetExists($key);
-	}
+class YellowDataCollection extends ArrayObject {
+    public function __construct() {
+        parent::__construct(array());
+    }
+    
+    // Return array element
+    public function offsetGet($key) {
+        if (is_string($key)) $key = lcfirst($key);
+        return parent::offsetGet($key);
+    }
+    
+    // Set array element
+    public function offsetSet($key, $value) {
+        if (is_string($key)) $key = lcfirst($key);
+        parent::offsetSet($key, $value);
+    }
+    
+    // Remove array element
+    public function offsetUnset($key) {
+        if (is_string($key)) $key = lcfirst($key);
+        parent::offsetUnset($key);
+    }
+    
+    // Check if array element exists
+    public function offsetExists($key) {
+        if (is_string($key)) $key = lcfirst($key);
+        return parent::offsetExists($key);
+    }
 }
 
-class YellowPageCollection extends ArrayObject
-{
-	var $yellow;				//access to API
-	var $filterValue;			//current page filter value
-	var $paginationNumber;		//current page number in pagination
-	var $paginationCount;		//highest page number in pagination
-	
-	function __construct($yellow)
-	{
-		parent::__construct(array());
-		$this->yellow = $yellow;
-	}
-	
-	// Filter page collection by meta data
-	function filter($key, $value, $exactMatch = true)
-	{
-		$array = array();
-		$value = strreplaceu(' ', '-', strtoloweru($value));
-		$valueLength = strlenu($value);
-		$this->filterValue = "";
-		foreach($this->getArrayCopy() as $page)
-		{
-			if($page->isExisting($key))
-			{
-				foreach(preg_split("/\s*,\s*/", $page->get($key)) as $pageValue)
-				{
-					$pageValueLength = $exactMatch ? strlenu($pageValue) : $valueLength;
-					if($value==substru(strreplaceu(' ', '-', strtoloweru($pageValue)), 0, $pageValueLength))
-					{
-						if(empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength);
-						array_push($array, $page);
-						break;
-					}
-				}
-			}
-		}
-		$this->exchangeArray($array);
-		return $this;
-	}
-	
-	// Filter page collection by file name
-	function match($regex = "/.*/")
-	{
-		$array = array();
-		foreach($this->getArrayCopy() as $page)
-		{
-			if(preg_match($regex, $page->fileName)) array_push($array, $page);
-		}
-		$this->exchangeArray($array);
-		return $this;
-	}
-	
-	// Sort page collection by meta data
-	function sort($key, $ascendingOrder = true)
-	{
-		$array = $this->getArrayCopy();
-		foreach($array as $page) $page->set("sortindex", ++$i);
-		$callback = function($a, $b) use ($key, $ascendingOrder)
-		{
-			$result = $ascendingOrder ?
-				strnatcasecmp($a->get($key), $b->get($key)) :
-				strnatcasecmp($b->get($key), $a->get($key));
-			return $result==0 ? $a->get("sortindex") - $b->get("sortindex") : $result;
-		};
-		usort($array, $callback);
-		$this->exchangeArray($array);
-		return $this;
-	}
-	
-	// Sort page collection by meta data similarity
-	function similar($page, $ascendingOrder = false)
-	{
-		$location = $page->location;
-		$keywords = $this->yellow->toolbox->createTextKeywords($page->get("title"));
-		$keywords .= ",".$page->get("tag").",".$page->get("author");
-		$tokens = array_unique(array_filter(preg_split("/\s*,\s*/", $keywords), "strlen"));
-		if(!empty($tokens))
-		{
-			$array = array();
-			foreach($this->getArrayCopy() as $page)
-			{
-				$searchScore = 0;
-				foreach($tokens as $token)
-				{
-					if(stristr($page->get("title"), $token)) $searchScore += 10;
-					if(stristr($page->get("tag"), $token)) $searchScore += 5;
-					if(stristr($page->get("author"), $token)) $searchScore += 2;
-				}
-				if($page->location!=$location)
-				{
-					$page->set("searchscore", $searchScore);
-					array_push($array, $page);
-				}
-			}
-			$this->exchangeArray($array);
-			$this->sort("modified", $ascendingOrder)->sort("searchscore", $ascendingOrder);
-		}
-		return $this;
-	}
+class YellowPageCollection extends ArrayObject {
+    public $yellow;                 //access to API
+    public $filterValue;            //current page filter value
+    public $paginationNumber;       //current page number in pagination
+    public $paginationCount;        //highest page number in pagination
+    
+    public function __construct($yellow) {
+        parent::__construct(array());
+        $this->yellow = $yellow;
+    }
+    
+    // Filter page collection by meta data
+    public function filter($key, $value, $exactMatch = true) {
+        $array = array();
+        $value = strreplaceu(" ", "-", strtoloweru($value));
+        $valueLength = strlenu($value);
+        $this->filterValue = "";
+        foreach ($this->getArrayCopy() as $page) {
+            if ($page->isExisting($key)) {
+                foreach (preg_split("/\s*,\s*/", $page->get($key)) as $pageValue) {
+                    $pageValueLength = $exactMatch ? strlenu($pageValue) : $valueLength;
+                    if ($value==substru(strreplaceu(" ", "-", strtoloweru($pageValue)), 0, $pageValueLength)) {
+                        if (empty($this->filterValue)) $this->filterValue = substru($pageValue, 0, $pageValueLength);
+                        array_push($array, $page);
+                        break;
+                    }
+                }
+            }
+        }
+        $this->exchangeArray($array);
+        return $this;
+    }
+    
+    // Filter page collection by file name
+    public function match($regex = "/.*/") {
+        $array = array();
+        foreach ($this->getArrayCopy() as $page) {
+            if (preg_match($regex, $page->fileName)) array_push($array, $page);
+        }
+        $this->exchangeArray($array);
+        return $this;
+    }
+    
+    // Sort page collection by meta data
+    public function sort($key, $ascendingOrder = true) {
+        $array = $this->getArrayCopy();
+        foreach ($array as $page) {
+            $page->set("sortindex", ++$i);
+        }
+        $callback = function ($a, $b) use ($key, $ascendingOrder) {
+            $result = $ascendingOrder ?
+                strnatcasecmp($a->get($key), $b->get($key)) :
+                strnatcasecmp($b->get($key), $a->get($key));
+            return $result==0 ? $a->get("sortindex") - $b->get("sortindex") : $result;
+        };
+        usort($array, $callback);
+        $this->exchangeArray($array);
+        return $this;
+    }
+    
+    // Sort page collection by meta data similarity
+    public function similar($page, $ascendingOrder = false) {
+        $location = $page->location;
+        $keywords = $this->yellow->toolbox->createTextKeywords($page->get("title"));
+        $keywords .= ",".$page->get("tag").",".$page->get("author");
+        $tokens = array_unique(array_filter(preg_split("/\s*,\s*/", $keywords), "strlen"));
+        if (!empty($tokens)) {
+            $array = array();
+            foreach ($this->getArrayCopy() as $page) {
+                $searchScore = 0;
+                foreach ($tokens as $token) {
+                    if (stristr($page->get("title"), $token)) $searchScore += 10;
+                    if (stristr($page->get("tag"), $token)) $searchScore += 5;
+                    if (stristr($page->get("author"), $token)) $searchScore += 2;
+                }
+                if ($page->location!=$location) {
+                    $page->set("searchscore", $searchScore);
+                    array_push($array, $page);
+                }
+            }
+            $this->exchangeArray($array);
+            $this->sort("modified", $ascendingOrder)->sort("searchscore", $ascendingOrder);
+        }
+        return $this;
+    }
 
-	// Merge page collection
-	function merge($input)
-	{
-		$this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input));
-		return $this;
-	}
-	
-	// Append to end of page collection
-	function append($page)
-	{
-		parent::append($page);
-		return $this;
-	}
-	
-	// Prepend to start of page collection
-	function prepend($page)
-	{
-		$array = $this->getArrayCopy();
-		array_unshift($array, $page);
-		$this->exchangeArray($array);
-		return $this;
-	}
-	
-	// Limit the number of pages in page collection
-	function limit($pagesMax)
-	{
-		$this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax));
-		return $this;
-	}
-	
-	// Reverse page collection
-	function reverse()
-	{
-		$this->exchangeArray(array_reverse($this->getArrayCopy()));
-		return $this;
-	}
-	
-	// Randomize page collection
-	function shuffle()
-	{
-		$array = $this->getArrayCopy();
-		shuffle($array);
-		$this->exchangeArray($array);
-		return $this;
-	}
+    // Merge page collection
+    public function merge($input) {
+        $this->exchangeArray(array_merge($this->getArrayCopy(), (array)$input));
+        return $this;
+    }
+    
+    // Append to end of page collection
+    public function append($page) {
+        parent::append($page);
+        return $this;
+    }
+    
+    // Prepend to start of page collection
+    public function prepend($page) {
+        $array = $this->getArrayCopy();
+        array_unshift($array, $page);
+        $this->exchangeArray($array);
+        return $this;
+    }
+    
+    // Limit the number of pages in page collection
+    public function limit($pagesMax) {
+        $this->exchangeArray(array_slice($this->getArrayCopy(), 0, $pagesMax));
+        return $this;
+    }
+    
+    // Reverse page collection
+    public function reverse() {
+        $this->exchangeArray(array_reverse($this->getArrayCopy()));
+        return $this;
+    }
+    
+    // Randomize page collection
+    public function shuffle() {
+        $array = $this->getArrayCopy();
+        shuffle($array);
+        $this->exchangeArray($array);
+        return $this;
+    }
 
-	// Paginate page collection
-	function pagination($limit, $reverse = true)
-	{
-		$this->paginationNumber = 1;
-		$this->paginationCount = ceil($this->count() / $limit);
-		$pagination = $this->yellow->config->get("contentPagination");
-		if(isset($_REQUEST[$pagination])) $this->paginationNumber = intval($_REQUEST[$pagination]);
-		if($this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0;
-		if($this->paginationNumber>=1)
-		{
-			$array = $this->getArrayCopy();
-			if($reverse) $array = array_reverse($array);
-			$this->exchangeArray(array_slice($array, ($this->paginationNumber - 1) * $limit, $limit));
-		}
-		return $this;
-	}
-	
-	// Return current page number in pagination 
-	function getPaginationNumber()
-	{
-		return $this->paginationNumber;
-	}
-	
-	// Return highest page number in pagination
-	function getPaginationCount()
-	{
-		return $this->paginationCount;
-	}
-	
-	// Return location for a page in pagination
-	function getPaginationLocation($absoluteLocation = true, $pageNumber = 1)
-	{
-		if($pageNumber>=1 && $pageNumber<=$this->paginationCount)
-		{
-			$pagination = $this->yellow->config->get("contentPagination");
-			$location = $this->yellow->page->getLocation($absoluteLocation);
-			$locationArgs = $this->yellow->toolbox->getLocationArgsNew(
-				$pageNumber>1 ? "$pagination:$pageNumber" : "$pagination:", $pagination);
-		}
-		return $location.$locationArgs;
-	}
-	
-	// Return location for previous page in pagination
-	function getPaginationPrevious($absoluteLocation = true)
-	{
-		$pageNumber = $this->paginationNumber-1;
-		return $this->getPaginationLocation($absoluteLocation, $pageNumber);
-	}
-	
-	// Return location for next page in pagination
-	function getPaginationNext($absoluteLocation = true)
-	{
-		$pageNumber = $this->paginationNumber+1;
-		return $this->getPaginationLocation($absoluteLocation, $pageNumber);
-	}
-	
-	// Return current page number in collection
-	function getPageNumber($page)
-	{
-		$pageNumber = 0;
-		foreach($this->getIterator() as $key=>$value)
-		{
-			if($page->getLocation()==$value->getLocation()) { $pageNumber = $key+1; break; }
-		}
-		return $pageNumber;
-	}
-	
-	// Return page in collection, null if none
-	function getPage($pageNumber = 1)
-	{
-		return ($pageNumber>=1 && $pageNumber<=$this->count()) ? $this->offsetGet($pageNumber-1) : null;
-	}
-	
-	// Return previous page in collection, null if none
-	function getPagePrevious($page)
-	{
-		$pageNumber = $this->getPageNumber($page)-1;
-		return $this->getPage($pageNumber);
-	}
-	
-	// Return next page in collection, null if none
-	function getPageNext($page)
-	{
-		$pageNumber = $this->getPageNumber($page)+1;
-		return $this->getPage($pageNumber);
-	}
-	
-	// Return current page filter
-	function getFilter()
-	{
-		return $this->filterValue;
-	}
-	
-	// Return page collection modification date, Unix time or HTTP format
-	function getModified($httpFormat = false)
-	{
-		$modified = 0;
-		foreach($this->getIterator() as $page) $modified = max($modified, $page->getModified());
-		return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified;
-	}
-	
-	// Check if there is a pagination
-	function isPagination()
-	{
-		return $this->paginationCount>1;
-	}
+    // Paginate page collection
+    public function pagination($limit, $reverse = true) {
+        $this->paginationNumber = 1;
+        $this->paginationCount = ceil($this->count() / $limit);
+        $pagination = $this->yellow->config->get("contentPagination");
+        if (isset($_REQUEST[$pagination])) $this->paginationNumber = intval($_REQUEST[$pagination]);
+        if ($this->paginationNumber>$this->paginationCount) $this->paginationNumber = 0;
+        if ($this->paginationNumber>=1) {
+            $array = $this->getArrayCopy();
+            if ($reverse) $array = array_reverse($array);
+            $this->exchangeArray(array_slice($array, ($this->paginationNumber - 1) * $limit, $limit));
+        }
+        return $this;
+    }
+    
+    // Return current page number in pagination
+    public function getPaginationNumber() {
+        return $this->paginationNumber;
+    }
+    
+    // Return highest page number in pagination
+    public function getPaginationCount() {
+        return $this->paginationCount;
+    }
+    
+    // Return location for a page in pagination
+    public function getPaginationLocation($absoluteLocation = true, $pageNumber = 1) {
+        if ($pageNumber>=1 && $pageNumber<=$this->paginationCount) {
+            $pagination = $this->yellow->config->get("contentPagination");
+            $location = $this->yellow->page->getLocation($absoluteLocation);
+            $locationArgs = $this->yellow->toolbox->getLocationArgsNew(
+                $pageNumber>1 ? "$pagination:$pageNumber" : "$pagination:", $pagination);
+        }
+        return $location.$locationArgs;
+    }
+    
+    // Return location for previous page in pagination
+    public function getPaginationPrevious($absoluteLocation = true) {
+        $pageNumber = $this->paginationNumber-1;
+        return $this->getPaginationLocation($absoluteLocation, $pageNumber);
+    }
+    
+    // Return location for next page in pagination
+    public function getPaginationNext($absoluteLocation = true) {
+        $pageNumber = $this->paginationNumber+1;
+        return $this->getPaginationLocation($absoluteLocation, $pageNumber);
+    }
+    
+    // Return current page number in collection
+    public function getPageNumber($page) {
+        $pageNumber = 0;
+        foreach ($this->getIterator() as $key=>$value) {
+            if ($page->getLocation()==$value->getLocation()) {
+                $pageNumber = $key+1;
+                break;
+            }
+        }
+        return $pageNumber;
+    }
+    
+    // Return page in collection, null if none
+    public function getPage($pageNumber = 1) {
+        return ($pageNumber>=1 && $pageNumber<=$this->count()) ? $this->offsetGet($pageNumber-1) : null;
+    }
+    
+    // Return previous page in collection, null if none
+    public function getPagePrevious($page) {
+        $pageNumber = $this->getPageNumber($page)-1;
+        return $this->getPage($pageNumber);
+    }
+    
+    // Return next page in collection, null if none
+    public function getPageNext($page) {
+        $pageNumber = $this->getPageNumber($page)+1;
+        return $this->getPage($pageNumber);
+    }
+    
+    // Return current page filter
+    public function getFilter() {
+        return $this->filterValue;
+    }
+    
+    // Return page collection modification date, Unix time or HTTP format
+    public function getModified($httpFormat = false) {
+        $modified = 0;
+        foreach ($this->getIterator() as $page) {
+            $modified = max($modified, $page->getModified());
+        }
+        return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($modified) : $modified;
+    }
+    
+    // Check if there is a pagination
+    public function isPagination() {
+        return $this->paginationCount>1;
+    }
 }
 
-class YellowPages
-{
-	var $yellow;			//access to API
-	var $pages;				//scanned pages
-	
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->pages = array();
-	}
-	
-	// Scan file system on demand
-	function scanLocation($location)
-	{
-		if(is_null($this->pages[$location]))
-		{
-			if(defined("DEBUG") && DEBUG>=2) echo "YellowPages::scanLocation location:$location<br/>\n";
-			$this->pages[$location] = array();
-			$scheme = $this->yellow->page->scheme;
-			$address = $this->yellow->page->address;
-			$base = $this->yellow->page->base;
-			if(empty($location))
-			{
-				$rootLocations = $this->yellow->lookup->findRootLocations();
-				foreach($rootLocations as $rootLocation)
-				{
-					list($rootLocation, $fileName) = explode(' ', $rootLocation, 2);
-					$page = new YellowPage($this->yellow);
-					$page->setRequestInformation($scheme, $address, $base, $rootLocation, $fileName);
-					$page->parseData("", false, 0);
-					array_push($this->pages[$location], $page);
-				}
-			} else {
-				$fileNames = $this->yellow->lookup->findChildrenFromLocation($location);
-				foreach($fileNames as $fileName)
-				{
-					$page = new YellowPage($this->yellow);
-					$page->setRequestInformation($scheme, $address, $base,
-						$this->yellow->lookup->findLocationFromFile($fileName), $fileName);
-					$page->parseData($this->yellow->toolbox->readFile($fileName, 4096), false, 0);
-					if(strlenb($page->rawData)<4096) $page->statusCode = 200;
-					array_push($this->pages[$location], $page);
-				}
-			}
-		}
-		return $this->pages[$location];
-	}	
+class YellowPages {
+    public $yellow;         //access to API
+    public $pages;          //scanned pages
+    
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+        $this->pages = array();
+    }
+    
+    // Scan file system on demand
+    public function scanLocation($location) {
+        if (is_null($this->pages[$location])) {
+            if (defined("DEBUG") && DEBUG>=2) echo "YellowPages::scanLocation location:$location<br/>\n";
+            $this->pages[$location] = array();
+            $scheme = $this->yellow->page->scheme;
+            $address = $this->yellow->page->address;
+            $base = $this->yellow->page->base;
+            if (empty($location)) {
+                $rootLocations = $this->yellow->lookup->findRootLocations();
+                foreach ($rootLocations as $rootLocation) {
+                    list($rootLocation, $fileName) = explode(" ", $rootLocation, 2);
+                    $page = new YellowPage($this->yellow);
+                    $page->setRequestInformation($scheme, $address, $base, $rootLocation, $fileName);
+                    $page->parseData("", false, 0);
+                    array_push($this->pages[$location], $page);
+                }
+            } else {
+                $fileNames = $this->yellow->lookup->findChildrenFromLocation($location);
+                foreach ($fileNames as $fileName) {
+                    $page = new YellowPage($this->yellow);
+                    $page->setRequestInformation($scheme, $address, $base,
+                        $this->yellow->lookup->findLocationFromFile($fileName), $fileName);
+                    $page->parseData($this->yellow->toolbox->readFile($fileName, 4096), false, 0);
+                    if (strlenb($page->rawData)<4096) $page->statusCode = 200;
+                    array_push($this->pages[$location], $page);
+                }
+            }
+        }
+        return $this->pages[$location];
+    }
 
-	// Return page from file system, null if not found
-	function find($location, $absoluteLocation = false)
-	{
-		if($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base));
-		foreach($this->scanLocation($this->getParentLocation($location)) as $page)
-		{
-			if($page->location==$location)
-			{
-				if(!$this->yellow->lookup->isRootLocation($page->location)) { $found = true; break; }
-			}
-		}
-		return $found ? $page : null;
-	}
-	
-	// Return page collection with all pages
-	function index($showInvisible = false, $multiLanguage = false, $levelMax = 0)
-	{
-		$rootLocation = $multiLanguage ? "" : $this->getRootLocation($this->yellow->page->location);
-		return $this->getChildrenRecursive($rootLocation, $showInvisible, $levelMax);
-	}
-	
-	// Return page collection with top-level navigation
-	function top($showInvisible = false)
-	{
-		$rootLocation = $this->getRootLocation($this->yellow->page->location);
-		return $this->getChildren($rootLocation, $showInvisible);
-	}
-	
-	// Return page collection with path ancestry
-	function path($location, $absoluteLocation = false)
-	{
-		$pages = new YellowPageCollection($this->yellow);
-		if($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base));
-		if($page = $this->find($location))
-		{
-			$pages->prepend($page);
-			for(; $parent = $page->getParent(); $page=$parent) $pages->prepend($parent);
-			$home = $this->find($this->getHomeLocation($page->location));
-			if($home && $home->location!=$page->location) $pages->prepend($home);
-		}
-		return $pages;
-	}
-	
-	// Return page collection with multiple languages
-	function multi($location, $absoluteLocation = false, $showInvisible = false)
-	{
-		$pages = new YellowPageCollection($this->yellow);
-		if($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base));
-		$locationEnd = substru($location, strlenu($this->getRootLocation($location)) - 4);
-		foreach($this->scanLocation("") as $page)
-		{
-			if($content = $this->find(substru($page->location, 4).$locationEnd))
-			{
-				if($content->isAvailable() && ($content->isVisible() || $showInvisible))
-				{
-					if(!$this->yellow->lookup->isRootLocation($content->location)) $pages->append($content);
-				}
-			}
-		}
-		return $pages;
-	}
-	
-	// Return page collection that's empty
-	function clean()
-	{
-		return new YellowPageCollection($this->yellow);
-	}
-	
-	// Return languages in multi language mode
-	function getLanguages($showInvisible = false)
-	{
-		$languages = array();
-		foreach($this->scanLocation("") as $page)
-		{
-			if($page->isAvailable() && ($page->isVisible() || $showInvisible))
-			{
-				array_push($languages, $page->get("language"));
-			}
-		}
-		return $languages;
-	}
-	
-	// Return child pages
-	function getChildren($location, $showInvisible = false)
-	{
-		$pages = new YellowPageCollection($this->yellow);
-		foreach($this->scanLocation($location) as $page)
-		{
-			if($page->isAvailable() && ($page->isVisible() || $showInvisible))
-			{
-				if(!$this->yellow->lookup->isRootLocation($page->location) && is_readable($page->fileName)) $pages->append($page);
-			}
-		}
-		return $pages;
-	}
-	
-	// Return sub pages
-	function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0)
-	{
-		--$levelMax;
-		$pages = new YellowPageCollection($this->yellow);
-		foreach($this->scanLocation($location) as $page)
-		{
-			if($page->isAvailable() && ($page->isVisible() || $showInvisible))
-			{
-				if(!$this->yellow->lookup->isRootLocation($page->location) && is_readable($page->fileName)) $pages->append($page);
-				if(!$this->yellow->lookup->isFileLocation($page->location) && $levelMax!=0)
-				{
-					$pages->merge($this->getChildrenRecursive($page->location, $showInvisible, $levelMax));
-				}
-			}
-		}
-		return $pages;
-	}
-	
-	// Return root location
-	function getRootLocation($location)
-	{
-		$rootLocation = "root/";
-		if($this->yellow->config->get("multiLanguageMode"))
-		{
-			foreach($this->scanLocation("") as $page)
-			{
-				$token = substru($page->location, 4);
-				if($token!="/" && substru($location, 0, strlenu($token))==$token) { $rootLocation = "root$token"; break; }
-			}
-		}
-		return $rootLocation;
-	}
+    // Return page from file system, null if not found
+    public function find($location, $absoluteLocation = false) {
+        if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base));
+        foreach ($this->scanLocation($this->getParentLocation($location)) as $page) {
+            if ($page->location==$location) {
+                if (!$this->yellow->lookup->isRootLocation($page->location)) {
+                    $found = true;
+                    break;
+                }
+            }
+        }
+        return $found ? $page : null;
+    }
+    
+    // Return page collection with all pages
+    public function index($showInvisible = false, $multiLanguage = false, $levelMax = 0) {
+        $rootLocation = $multiLanguage ? "" : $this->getRootLocation($this->yellow->page->location);
+        return $this->getChildrenRecursive($rootLocation, $showInvisible, $levelMax);
+    }
+    
+    // Return page collection with top-level navigation
+    public function top($showInvisible = false) {
+        $rootLocation = $this->getRootLocation($this->yellow->page->location);
+        return $this->getChildren($rootLocation, $showInvisible);
+    }
+    
+    // Return page collection with path ancestry
+    public function path($location, $absoluteLocation = false) {
+        $pages = new YellowPageCollection($this->yellow);
+        if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base));
+        if ($page = $this->find($location)) {
+            $pages->prepend($page);
+            for (; $parent = $page->getParent(); $page=$parent) {
+                $pages->prepend($parent);
+            }
+            $home = $this->find($this->getHomeLocation($page->location));
+            if ($home && $home->location!=$page->location) $pages->prepend($home);
+        }
+        return $pages;
+    }
+    
+    // Return page collection with multiple languages
+    public function multi($location, $absoluteLocation = false, $showInvisible = false) {
+        $pages = new YellowPageCollection($this->yellow);
+        if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base));
+        $locationEnd = substru($location, strlenu($this->getRootLocation($location)) - 4);
+        foreach ($this->scanLocation("") as $page) {
+            if ($content = $this->find(substru($page->location, 4).$locationEnd)) {
+                if ($content->isAvailable() && ($content->isVisible() || $showInvisible)) {
+                    if (!$this->yellow->lookup->isRootLocation($content->location)) $pages->append($content);
+                }
+            }
+        }
+        return $pages;
+    }
+    
+    // Return page collection that's empty
+    public function clean() {
+        return new YellowPageCollection($this->yellow);
+    }
+    
+    // Return languages in multi language mode
+    public function getLanguages($showInvisible = false) {
+        $languages = array();
+        foreach ($this->scanLocation("") as $page) {
+            if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) array_push($languages, $page->get("language"));
+        }
+        return $languages;
+    }
+    
+    // Return child pages
+    public function getChildren($location, $showInvisible = false) {
+        $pages = new YellowPageCollection($this->yellow);
+        foreach ($this->scanLocation($location) as $page) {
+            if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) {
+                if (!$this->yellow->lookup->isRootLocation($page->location) && is_readable($page->fileName)) $pages->append($page);
+            }
+        }
+        return $pages;
+    }
+    
+    // Return sub pages
+    public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) {
+        --$levelMax;
+        $pages = new YellowPageCollection($this->yellow);
+        foreach ($this->scanLocation($location) as $page) {
+            if ($page->isAvailable() && ($page->isVisible() || $showInvisible)) {
+                if (!$this->yellow->lookup->isRootLocation($page->location) && is_readable($page->fileName)) $pages->append($page);
+                if (!$this->yellow->lookup->isFileLocation($page->location) && $levelMax!=0) {
+                    $pages->merge($this->getChildrenRecursive($page->location, $showInvisible, $levelMax));
+                }
+            }
+        }
+        return $pages;
+    }
+    
+    // Return root location
+    public function getRootLocation($location) {
+        $rootLocation = "root/";
+        if ($this->yellow->config->get("multiLanguageMode")) {
+            foreach ($this->scanLocation("") as $page) {
+                $token = substru($page->location, 4);
+                if ($token!="/" && substru($location, 0, strlenu($token))==$token) {
+                    $rootLocation = "root$token";
+                    break;
+                }
+            }
+        }
+        return $rootLocation;
+    }
 
-	// Return home location
-	function getHomeLocation($location)
-	{
-		return substru($this->getRootLocation($location), 4);
-	}
-	
-	// Return parent location
-	function getParentLocation($location)
-	{
-		$token = rtrim(substru($this->getRootLocation($location), 4), '/');
-		if(preg_match("#^($token.*\/).+?$#", $location, $matches))
-		{
-			if($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1];
-		}
-		if(empty($parentLocation)) $parentLocation = "root$token/";
-		return $parentLocation;
-	}
-	
-	// Return top-level location
-	function getParentTopLocation($location)
-	{
-		$token = rtrim(substru($this->getRootLocation($location), 4), '/');
-		if(preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1];
-		if(empty($parentTopLocation)) $parentTopLocation = "$token/";
-		return $parentTopLocation;
-	}
+    // Return home location
+    public function getHomeLocation($location) {
+        return substru($this->getRootLocation($location), 4);
+    }
+    
+    // Return parent location
+    public function getParentLocation($location) {
+        $token = rtrim(substru($this->getRootLocation($location), 4), "/");
+        if (preg_match("#^($token.*\/).+?$#", $location, $matches)) {
+            if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1];
+        }
+        if (empty($parentLocation)) $parentLocation = "root$token/";
+        return $parentLocation;
+    }
+    
+    // Return top-level location
+    public function getParentTopLocation($location) {
+        $token = rtrim(substru($this->getRootLocation($location), 4), "/");
+        if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1];
+        if (empty($parentTopLocation)) $parentTopLocation = "$token/";
+        return $parentTopLocation;
+    }
 }
-	
-class YellowFiles
-{
-	var $yellow;		//access to API
-	var $files;			//scanned files
-	
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->files = array();
-	}
+    
+class YellowFiles {
+    public $yellow;     //access to API
+    public $files;      //scanned files
+    
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+        $this->files = array();
+    }
 
-	// Scan file system on demand
-	function scanLocation($location)
-	{
-		if(is_null($this->files[$location]))
-		{
-			if(defined("DEBUG") && DEBUG>=2) echo "YellowFiles::scanLocation location:$location<br/>\n";
-			$this->files[$location] = array();
-			$scheme = $this->yellow->page->scheme;
-			$address = $this->yellow->page->address;
-			$base = $this->yellow->config->get("serverBase");
-			if(empty($location))
-			{
-				$fileNames = array($this->yellow->config->get("mediaDir"));
-			} else {
-				$fileNames = array();
-				$path = substru($location, 1);
-				foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, true) as $entry)
-				{
-					array_push($fileNames, $entry."/");
-				}
-				foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, false, true) as $entry)
-				{
-					array_push($fileNames, $entry);
-				}
-			}
-			foreach($fileNames as $fileName)
-			{
-				$file = new YellowPage($this->yellow);
-				$file->setRequestInformation($scheme, $address, $base, "/".$fileName, $fileName);
-				$file->parseData(null, false, 0);
-				array_push($this->files[$location], $file);
-			}
-		}
-		return $this->files[$location];
-	}
-	
-	// Return page with media file information, null if not found
-	function find($location, $absoluteLocation = false)
-	{
-		if($absoluteLocation) $location = substru($location, strlenu($this->yellow->config->get("serverBase")));
-		foreach($this->scanLocation($this->getParentLocation($location)) as $file)
-		{
-			if($file->location==$location)
-			{
-				if($this->yellow->lookup->isFileLocation($file->location)) { $found = true; break; }
-			}
-		}
-		return $found ? $file : null;
-	}
-	
-	// Return page collection with all media files
-	function index($showInvisible = false, $multiPass = false, $levelMax = 0)
-	{
-		return $this->getChildrenRecursive("", $showInvisible, $levelMax);
-	}
-	
-	// Return page collection that's empty
-	function clean()
-	{
-		return new YellowPageCollection($this->yellow);
-	}
-	
-	// Return child files
-	function getChildren($location, $showInvisible = false)
-	{
-		$files = new YellowPageCollection($this->yellow);
-		foreach($this->scanLocation($location) as $file)
-		{
-			if($file->isAvailable() && ($file->isVisible() || $showInvisible))
-			{
-				if($this->yellow->lookup->isFileLocation($file->location)) $files->append($file);
-			}
-		}
-		return $files;
-	}
-	
-	// Return sub files
-	function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0)
-	{
-		--$levelMax;
-		$files = new YellowPageCollection($this->yellow);
-		foreach($this->scanLocation($location) as $file)
-		{
-			if($file->isAvailable() && ($file->isVisible() || $showInvisible))
-			{
-				if($this->yellow->lookup->isFileLocation($file->location)) $files->append($file);
-				if(!$this->yellow->lookup->isFileLocation($file->location) && $levelMax!=0)
-				{
-					$files->merge($this->getChildrenRecursive($file->location, $showInvisible, $levelMax));
-				}
-			}
-		}
-		return $files;
-	}
-	
-	// Return home location
-	function getHomeLocation($location)
-	{
-		return $this->yellow->config->get("mediaLocation");
-	}
+    // Scan file system on demand
+    public function scanLocation($location) {
+        if (is_null($this->files[$location])) {
+            if (defined("DEBUG") && DEBUG>=2) echo "YellowFiles::scanLocation location:$location<br/>\n";
+            $this->files[$location] = array();
+            $scheme = $this->yellow->page->scheme;
+            $address = $this->yellow->page->address;
+            $base = $this->yellow->config->get("serverBase");
+            if (empty($location)) {
+                $fileNames = array($this->yellow->config->get("mediaDir"));
+            } else {
+                $fileNames = array();
+                $path = substru($location, 1);
+                foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, true) as $entry) {
+                    array_push($fileNames, $entry."/");
+                }
+                foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, false, true) as $entry) {
+                    array_push($fileNames, $entry);
+                }
+            }
+            foreach ($fileNames as $fileName) {
+                $file = new YellowPage($this->yellow);
+                $file->setRequestInformation($scheme, $address, $base, "/".$fileName, $fileName);
+                $file->parseData(null, false, 0);
+                array_push($this->files[$location], $file);
+            }
+        }
+        return $this->files[$location];
+    }
+    
+    // Return page with media file information, null if not found
+    public function find($location, $absoluteLocation = false) {
+        if ($absoluteLocation) $location = substru($location, strlenu($this->yellow->config->get("serverBase")));
+        foreach ($this->scanLocation($this->getParentLocation($location)) as $file) {
+            if ($file->location==$location) {
+                if ($this->yellow->lookup->isFileLocation($file->location)) {
+                    $found = true;
+                    break;
+                }
+            }
+        }
+        return $found ? $file : null;
+    }
+    
+    // Return page collection with all media files
+    public function index($showInvisible = false, $multiPass = false, $levelMax = 0) {
+        return $this->getChildrenRecursive("", $showInvisible, $levelMax);
+    }
+    
+    // Return page collection that's empty
+    public function clean() {
+        return new YellowPageCollection($this->yellow);
+    }
+    
+    // Return child files
+    public function getChildren($location, $showInvisible = false) {
+        $files = new YellowPageCollection($this->yellow);
+        foreach ($this->scanLocation($location) as $file) {
+            if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) {
+                if ($this->yellow->lookup->isFileLocation($file->location)) $files->append($file);
+            }
+        }
+        return $files;
+    }
+    
+    // Return sub files
+    public function getChildrenRecursive($location, $showInvisible = false, $levelMax = 0) {
+        --$levelMax;
+        $files = new YellowPageCollection($this->yellow);
+        foreach ($this->scanLocation($location) as $file) {
+            if ($file->isAvailable() && ($file->isVisible() || $showInvisible)) {
+                if ($this->yellow->lookup->isFileLocation($file->location)) $files->append($file);
+                if (!$this->yellow->lookup->isFileLocation($file->location) && $levelMax!=0) {
+                    $files->merge($this->getChildrenRecursive($file->location, $showInvisible, $levelMax));
+                }
+            }
+        }
+        return $files;
+    }
+    
+    // Return home location
+    public function getHomeLocation($location) {
+        return $this->yellow->config->get("mediaLocation");
+    }
 
-	// Return parent location
-	function getParentLocation($location)
-	{
-		$token = rtrim($this->yellow->config->get("mediaLocation"), '/');
-		if(preg_match("#^($token.*\/).+?$#", $location, $matches))
-		{
-			if($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1];
-		}
-		if(empty($parentLocation)) $parentLocation = "";
-		return $parentLocation;
-	}
-	
-	// Return top-level location
-	function getParentTopLocation($location)
-	{
-		$token = rtrim($this->yellow->config->get("mediaLocation"), '/');
-		if(preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1];
-		if(empty($parentTopLocation)) $parentTopLocation = "$token/";
-		return $parentTopLocation;
-	}
+    // Return parent location
+    public function getParentLocation($location) {
+        $token = rtrim($this->yellow->config->get("mediaLocation"), "/");
+        if (preg_match("#^($token.*\/).+?$#", $location, $matches)) {
+            if ($matches[1]!="$token/" || $this->yellow->lookup->isFileLocation($location)) $parentLocation = $matches[1];
+        }
+        if (empty($parentLocation)) $parentLocation = "";
+        return $parentLocation;
+    }
+    
+    // Return top-level location
+    public function getParentTopLocation($location) {
+        $token = rtrim($this->yellow->config->get("mediaLocation"), "/");
+        if (preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1];
+        if (empty($parentTopLocation)) $parentTopLocation = "$token/";
+        return $parentTopLocation;
+    }
 }
 
-class YellowPlugins
-{
-	var $yellow;		//access to API
-	var $modified;		//plugin modification date
-	var $plugins;		//registered plugins
+class YellowPlugins {
+    public $yellow;     //access to API
+    public $modified;   //plugin modification date
+    public $plugins;    //registered plugins
 
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->modified = 0;
-		$this->plugins = array();
-	}
-	
-	// Load plugins
-	function load($path = "")
-	{
-		$path = empty($path) ? $this->yellow->config->get("pluginDir") : $path;
-		foreach($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry)
-		{
-			if(defined("DEBUG") && DEBUG>=3) echo "YellowPlugins::load file:$entry<br/>\n";
-			$this->modified = max($this->modified, filemtime($entry));
-			global $yellow;
-			require_once($entry);
-		}
-		$callback = function($a, $b)
-		{
-			return $a["priority"] - $b["priority"];
-		};
-		uasort($this->plugins, $callback);
-		foreach($this->plugins as $key=>$value)
-		{
-			$this->plugins[$key]["obj"] = new $value["plugin"];
-			if(method_exists($this->plugins[$key]["obj"], "onLoad")) $this->plugins[$key]["obj"]->onLoad($yellow);
-		}
-	}
-	
-	// Register plugin
-	function register($name, $plugin, $version, $priority = 0)
-	{
-		if(!$this->isExisting($name))
-		{
-			if($priority==0) $priority = count($this->plugins) + 10;
-			$this->plugins[$name] = array();
-			$this->plugins[$name]["plugin"] = $plugin;
-			$this->plugins[$name]["version"] = $version;
-			$this->plugins[$name]["priority"] = $priority;
-		}
-	}
-	
-	// Return plugin
-	function get($name)
-	{
-		return $this->plugins[$name]["obj"];
-	}
-	
-	// Return plugin version
-	function getData()
-	{
-		$data = array();
-		$data["YellowCore"] = YellowCore::VERSION;
-		foreach($this->plugins as $key=>$value)
-		{
-			if(empty($value["plugin"]) || empty($value["version"])) continue;
-			$data[$value["plugin"]] = $value["version"];
-		}
-		uksort($data, "strnatcasecmp");
-		return $data;
-	}
-	
-	// Return plugin modification date, Unix time or HTTP format
-	function getModified($httpFormat = false)
-	{
-		return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
-	}
-	
-	// Check if plugin exists
-	function isExisting($name)
-	{
-		return !is_null($this->plugins[$name]);
-	}
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+        $this->modified = 0;
+        $this->plugins = array();
+    }
+    
+    // Load plugins
+    public function load($path = "") {
+        $path = empty($path) ? $this->yellow->config->get("pluginDir") : $path;
+        foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) {
+            if (defined("DEBUG") && DEBUG>=3) echo "YellowPlugins::load file:$entry<br/>\n";
+            $this->modified = max($this->modified, filemtime($entry));
+            global $yellow;
+            require_once($entry);
+        }
+        $callback = function ($a, $b) {
+            return $a["priority"] - $b["priority"];
+        };
+        uasort($this->plugins, $callback);
+        foreach ($this->plugins as $key=>$value) {
+            $this->plugins[$key]["obj"] = new $value["plugin"];
+            if (method_exists($this->plugins[$key]["obj"], "onLoad")) $this->plugins[$key]["obj"]->onLoad($yellow);
+        }
+    }
+    
+    // Register plugin
+    public function register($name, $plugin, $version, $priority = 0) {
+        if (!$this->isExisting($name)) {
+            if ($priority==0) $priority = count($this->plugins) + 10;
+            $this->plugins[$name] = array();
+            $this->plugins[$name]["plugin"] = $plugin;
+            $this->plugins[$name]["version"] = $version;
+            $this->plugins[$name]["priority"] = $priority;
+        }
+    }
+    
+    // Return plugin
+    public function get($name) {
+        return $this->plugins[$name]["obj"];
+    }
+    
+    // Return plugin version
+    public function getData() {
+        $data = array();
+        $data["YellowCore"] = YellowCore::VERSION;
+        foreach ($this->plugins as $key=>$value) {
+            if (empty($value["plugin"]) || empty($value["version"])) continue;
+            $data[$value["plugin"]] = $value["version"];
+        }
+        uksort($data, "strnatcasecmp");
+        return $data;
+    }
+    
+    // Return plugin modification date, Unix time or HTTP format
+    public function getModified($httpFormat = false) {
+        return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
+    }
+    
+    // Check if plugin exists
+    public function isExisting($name) {
+        return !is_null($this->plugins[$name]);
+    }
 }
 
-class YellowThemes
-{
-	var $yellow;		//access to API
-	var $modified;		//theme modification date
-	var $themes;		//themes
+class YellowThemes {
+    public $yellow;     //access to API
+    public $modified;   //theme modification date
+    public $themes;     //themes
 
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->modified = 0;
-		$this->themes = array();
-	}
-	
-	// Load themes
-	function load($path = "")
-	{
-		$path = empty($path) ? $this->yellow->config->get("assetDir") : $path;
-		foreach($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry)
-		{
-			if(defined("DEBUG") && DEBUG>=3) echo "YellowThemes::load file:$entry<br/>\n";
-			$this->modified = max($this->modified, filemtime($entry));
-			global $yellow;
-			require_once($entry);
-		}
-		foreach($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false) as $entry)
-		{
-			if(defined("DEBUG") && DEBUG>=3) echo "YellowThemes::load file:$entry<br/>\n";
-			$this->modified = max($this->modified, filemtime($entry));
-			$name = $this->yellow->lookup->normaliseName(basename($entry), true, true);
-			$this->register($name, "", "");
-		}
-		$callback = function($a, $b)
-		{
-			return $a["priority"] - $b["priority"];
-		};
-		uasort($this->themes, $callback);
-		foreach($this->themes as $key=>$value)
-		{
-			$this->themes[$key]["obj"] = empty($value["theme"]) ? new stdClass : new $value["theme"];
-			if(method_exists($this->themes[$key]["obj"], "onLoad")) $this->themes[$key]["obj"]->onLoad($yellow);
-		}
-	}
-	
-	// Register theme
-	function register($name, $theme, $version, $priority = 0)
-	{
-		if(!$this->isExisting($name))
-		{
-			if($priority==0) $priority = count($this->themes) + 10;
-			$this->themes[$name] = array();
-			$this->themes[$name]["theme"] = $theme;
-			$this->themes[$name]["version"] = $version;
-			$this->themes[$name]["priority"] = $priority;
-		}
-	}
-	
-	// Return theme
-	function get($name)
-	{
-		return $this->theme[$name]["obj"];
-	}
-	
-	// Return theme version
-	function getData()
-	{
-		$data = array();
-		foreach($this->themes as $key=>$value)
-		{
-			if(empty($value["theme"]) || empty($value["version"])) continue;
-			$data[$value["theme"]] = $value["version"];
-		}
-		uksort($data, "strnatcasecmp");
-		return $data;
-	}
-	
-	// Return theme modification date, Unix time or HTTP format
-	function getModified($httpFormat = false)
-	{
-		return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
-	}
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+        $this->modified = 0;
+        $this->themes = array();
+    }
+    
+    // Load themes
+    public function load($path = "") {
+        $path = empty($path) ? $this->yellow->config->get("assetDir") : $path;
+        foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.php$/", true, false) as $entry) {
+            if (defined("DEBUG") && DEBUG>=3) echo "YellowThemes::load file:$entry<br/>\n";
+            $this->modified = max($this->modified, filemtime($entry));
+            global $yellow;
+            require_once($entry);
+        }
+        foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false) as $entry) {
+            if (defined("DEBUG") && DEBUG>=3) echo "YellowThemes::load file:$entry<br/>\n";
+            $this->modified = max($this->modified, filemtime($entry));
+            $name = $this->yellow->lookup->normaliseName(basename($entry), true, true);
+            $this->register($name, "", "");
+        }
+        $callback = function ($a, $b) {
+            return $a["priority"] - $b["priority"];
+        };
+        uasort($this->themes, $callback);
+        foreach ($this->themes as $key=>$value) {
+            $this->themes[$key]["obj"] = empty($value["theme"]) ? new stdClass : new $value["theme"];
+            if (method_exists($this->themes[$key]["obj"], "onLoad")) $this->themes[$key]["obj"]->onLoad($yellow);
+        }
+    }
+    
+    // Register theme
+    public function register($name, $theme, $version, $priority = 0) {
+        if (!$this->isExisting($name)) {
+            if ($priority==0) $priority = count($this->themes) + 10;
+            $this->themes[$name] = array();
+            $this->themes[$name]["theme"] = $theme;
+            $this->themes[$name]["version"] = $version;
+            $this->themes[$name]["priority"] = $priority;
+        }
+    }
+    
+    // Return theme
+    public function get($name) {
+        return $this->theme[$name]["obj"];
+    }
+    
+    // Return theme version
+    public function getData() {
+        $data = array();
+        foreach ($this->themes as $key=>$value) {
+            if (empty($value["theme"]) || empty($value["version"])) continue;
+            $data[$value["theme"]] = $value["version"];
+        }
+        uksort($data, "strnatcasecmp");
+        return $data;
+    }
+    
+    // Return theme modification date, Unix time or HTTP format
+    public function getModified($httpFormat = false) {
+        return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
+    }
 
-	// Check if theme exists
-	function isExisting($name)
-	{
-		return !is_null($this->themes[$name]);
-	}
+    // Check if theme exists
+    public function isExisting($name) {
+        return !is_null($this->themes[$name]);
+    }
 }
-	
-class YellowConfig
-{
-	var $yellow;			//access to API
-	var $modified;			//configuration modification date
-	var $config;			//configuration
-	var $configDefaults;	//configuration defaults
-	
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->modified = 0;
-		$this->config = new YellowDataCollection();
-		$this->configDefaults = new YellowDataCollection();
-	}
-	
-	// Load configuration from file
-	function load($fileName)
-	{
-		if(defined("DEBUG") && DEBUG>=2) echo "YellowConfig::load file:$fileName<br/>\n";
-		$this->modified = $this->yellow->toolbox->getFileModified($fileName);
-		$fileData = $this->yellow->toolbox->readFile($fileName);
-		foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-		{
-			if(preg_match("/^\#/", $line)) continue;
-			preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-			if(!empty($matches[1]) && !strempty($matches[2]))
-			{
-				$this->set($matches[1], $matches[2]);
-				if(defined("DEBUG") && DEBUG>=3) echo "YellowConfig::load $matches[1]:$matches[2]<br/>\n";
-			}
-		}
-	}
-	
-	// Save configuration to file
-	function save($fileName, $config)
-	{
-		$configNew = new YellowDataCollection();
-		foreach($config as $key=>$value)
-		{
-			if(!empty($key) && !strempty($value))
-			{
-				$this->set($key, $value);
-				$configNew[$key] = $value;
-			}
-		}
-		$this->modified = time();
-		$fileData = $this->yellow->toolbox->readFile($fileName);
-		foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-		{
-			preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-			if(!empty($matches[1]) && !is_null($configNew[$matches[1]]))
-			{
-				$fileDataNew .= "$matches[1]: ".$configNew[$matches[1]]."\n";
-				unset($configNew[$matches[1]]);
-			} else {
-				$fileDataNew .= $line;
-			}
-		}
-		foreach($configNew as $key=>$value)
-		{
-			$fileDataNew .= ucfirst($key).": $value\n";
-		}
-		return $this->yellow->toolbox->createFile($fileName, $fileDataNew);
-	}
-	
-	// Set default configuration
-	function setDefault($key, $value)
-	{
-		$this->configDefaults[$key] = $value;
-	}
-	
-	// Set configuration
-	function set($key, $value)
-	{
-		$this->config[$key] = $value;
-	}
-	
-	// Return configuration
-	function get($key)
-	{
-		if(!is_null($this->config[$key]))
-		{
-			$value = $this->config[$key];
-		} else {
-			$value = !is_null($this->configDefaults[$key]) ? $this->configDefaults[$key] : "";
-		}
-		return $value;
-	}
-	
-	// Return configuration, HTML encoded
-	function getHtml($key)
-	{
-		return htmlspecialchars($this->get($key));
-	}
-	
-	// Return configuration strings
-	function getData($filterStart = "", $filterEnd = "")
-	{
-		$config = array();
-		if(empty($filterStart) && empty($filterEnd))
-		{
-			$config = array_merge($this->configDefaults->getArrayCopy(), $this->config->getArrayCopy());
-		} else {
-			foreach(array_merge($this->configDefaults->getArrayCopy(), $this->config->getArrayCopy()) as $key=>$value)
-			{
-				if(!empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $config[$key] = $value;
-				if(!empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $config[$key] = $value;
-			}
-		}
-		return $config;
-	}
-	
-	// Return configuration modification date, Unix time or HTTP format
-	function getModified($httpFormat = false)
-	{
-		return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
-	}
-	
-	// Check if configuration exists
-	function isExisting($key)
-	{
-		return !is_null($this->config[$key]);
-	}
+    
+class YellowConfig {
+    public $yellow;         //access to API
+    public $modified;       //configuration modification date
+    public $config;         //configuration
+    public $configDefaults; //configuration defaults
+    
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+        $this->modified = 0;
+        $this->config = new YellowDataCollection();
+        $this->configDefaults = new YellowDataCollection();
+    }
+    
+    // Load configuration from file
+    public function load($fileName) {
+        if (defined("DEBUG") && DEBUG>=2) echo "YellowConfig::load file:$fileName<br/>\n";
+        $this->modified = $this->yellow->toolbox->getFileModified($fileName);
+        $fileData = $this->yellow->toolbox->readFile($fileName);
+        foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+            if (preg_match("/^\#/", $line)) continue;
+            preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+            if (!empty($matches[1]) && !strempty($matches[2])) {
+                $this->set($matches[1], $matches[2]);
+                if (defined("DEBUG") && DEBUG>=3) echo "YellowConfig::load $matches[1]:$matches[2]<br/>\n";
+            }
+        }
+    }
+    
+    // Save configuration to file
+    public function save($fileName, $config) {
+        $configNew = new YellowDataCollection();
+        foreach ($config as $key=>$value) {
+            if (!empty($key) && !strempty($value)) {
+                $this->set($key, $value);
+                $configNew[$key] = $value;
+            }
+        }
+        $this->modified = time();
+        $fileData = $this->yellow->toolbox->readFile($fileName);
+        foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+            preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+            if (!empty($matches[1]) && !is_null($configNew[$matches[1]])) {
+                $fileDataNew .= "$matches[1]: ".$configNew[$matches[1]]."\n";
+                unset($configNew[$matches[1]]);
+            } else {
+                $fileDataNew .= $line;
+            }
+        }
+        foreach ($configNew as $key=>$value) {
+            $fileDataNew .= ucfirst($key).": $value\n";
+        }
+        return $this->yellow->toolbox->createFile($fileName, $fileDataNew);
+    }
+    
+    // Set default configuration
+    public function setDefault($key, $value) {
+        $this->configDefaults[$key] = $value;
+    }
+    
+    // Set configuration
+    public function set($key, $value) {
+        $this->config[$key] = $value;
+    }
+    
+    // Return configuration
+    public function get($key) {
+        if (!is_null($this->config[$key])) {
+            $value = $this->config[$key];
+        } else {
+            $value = !is_null($this->configDefaults[$key]) ? $this->configDefaults[$key] : "";
+        }
+        return $value;
+    }
+    
+    // Return configuration, HTML encoded
+    public function getHtml($key) {
+        return htmlspecialchars($this->get($key));
+    }
+    
+    // Return configuration strings
+    public function getData($filterStart = "", $filterEnd = "") {
+        $config = array();
+        if (empty($filterStart) && empty($filterEnd)) {
+            $config = array_merge($this->configDefaults->getArrayCopy(), $this->config->getArrayCopy());
+        } else {
+            foreach (array_merge($this->configDefaults->getArrayCopy(), $this->config->getArrayCopy()) as $key=>$value) {
+                if (!empty($filterStart) && substru($key, 0, strlenu($filterStart))==$filterStart) $config[$key] = $value;
+                if (!empty($filterEnd) && substru($key, -strlenu($filterEnd))==$filterEnd) $config[$key] = $value;
+            }
+        }
+        return $config;
+    }
+    
+    // Return configuration modification date, Unix time or HTTP format
+    public function getModified($httpFormat = false) {
+        return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
+    }
+    
+    // Check if configuration exists
+    public function isExisting($key) {
+        return !is_null($this->config[$key]);
+    }
 }
 
-class YellowText
-{
-	var $yellow;		//access to API
-	var $modified;		//text modification date
-	var $text;			//text
-	var $language;		//current language
-	
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->modified = 0;
-		$this->text = new YellowDataCollection();
-	}
-	
-	// Load text strings from file
-	function load($fileName, $languageDefault)
-	{
-		$path = dirname($fileName);
-		$regex = "/^".basename($fileName)."$/";
-		foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false) as $entry)
-		{
-			if(defined("DEBUG") && DEBUG>=2) echo "YellowText::load file:$entry<br/>\n";
-			$language = $languageDefault;
-			$this->modified = max($this->modified, filemtime($entry));
-			$fileData = $this->yellow->toolbox->readFile($entry);
-			foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-			{
-				if(preg_match("/^\#/", $line)) continue;
-				preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-				if(lcfirst($matches[1])=="language" && !strempty($matches[2])) $language = $matches[2];
-				if(!empty($language) && !empty($matches[1]) && !strempty($matches[2]))
-				{
-					$this->setText($matches[1], $matches[2], $language);
-					if(defined("DEBUG") && DEBUG>=3) echo "YellowText::load $matches[1]:$matches[2]<br/>\n";
-				}
-			}
-		}
-	}
-	
-	// Set current language
-	function setLanguage($language)
-	{
-		$this->language = $language;
-	}
-	
-	// Set text string for specific language
-	function setText($key, $value, $language)
-	{
-		if(is_null($this->text[$language])) $this->text[$language] = new YellowDataCollection();
-		$this->text[$language][$key] = $value;
-	}
-	
-	// Return text string
-	function get($key)
-	{
-		return $this->getText($key, $this->language);
-	}
-	
-	// Return text string, HTML encoded
-	function getHtml($key)
-	{
-		return htmlspecialchars($this->getText($key, $this->language));
-	}
-	
-	// Return text string for specific language
-	function getText($key, $language)
-	{
-		return $this->isExisting($key, $language) ? $this->text[$language][$key] : "[$key]";
-	}
-	
-	// Return text string for specific language, HTML encoded
-	function getTextHtml($key, $language)
-	{
-		return htmlspecialchars($this->getText($key, $language));
-	}
-	
-	// Return text strings
-	function getData($filterStart = "", $language = "")
-	{
-		$text = array();
-		if(empty($language)) $language = $this->language;
-		if($this->isLanguage($language))
-		{
-			if(empty($filterStart))
-			{
-				$text = $this->text[$language];
-			} else {
-				foreach($this->text[$language] as $key=>$value)
-				{
-					if(substru($key, 0, strlenu($filterStart))==$filterStart) $text[$key] = $value;
-				}
-			}
-		}
-		return $text;
-	}
-	
-	// Return human readable date, custom date
-	function getDateFormatted($timestamp, $format)
-	{
-		$dateMonths = preg_split("/\s*,\s*/", $this->get("dateMonths"));
-		$dateWeekdays = preg_split("/\s*,\s*/", $this->get("dateWeekdays"));
-		$month = $dateMonths[date('n', $timestamp) - 1];
-		$weekday = $dateWeekdays[date('N', $timestamp) - 1];
-		$timeZone = $this->yellow->config->get("timezone");
-		$timeZoneHelper = new DateTime(null, new DateTimeZone($timeZone));
-		$timeZoneOffset = $timeZoneHelper->getOffset();
-		$timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600));
-		$format = preg_replace("/(?<!\\\)F/", addcslashes($month, 'A..Za..z'), $format);
-		$format = preg_replace("/(?<!\\\)M/", addcslashes(substru($month, 0, 3), 'A..Za..z'), $format);
-		$format = preg_replace("/(?<!\\\)D/", addcslashes(substru($weekday, 0, 3), 'A..Za..z'), $format);
-		$format = preg_replace("/(?<!\\\)l/", addcslashes($weekday, 'A..Za..z'), $format);
-		$format = preg_replace("/(?<!\\\)T/", addcslashes($timeZoneAbbreviation, 'A..Za..z'), $format);
-		return date($format, $timestamp);
-	}
-	
-	// Return human readable date, relative to today
-	function getDateRelative($timestamp, $format, $daysLimit)
-	{
-		$timeDifference = time() - $timestamp;
-		$days = abs(intval($timeDifference / 86400));
-		if($days<=$daysLimit || $daysLimit==0)
-		{
-			$tokens = preg_split("/\s*,\s*/", $this->get($timeDifference>=0 ? "datePast" : "dateFuture"));
-			if($days==0)
-			{
-				$output = $tokens[0];
-			} else if($days==1) {
-				$output = $tokens[1];
-			} else if($days>=2 && $days<=29) {
-				$output = preg_replace("/@x/i", $days, $tokens[2]);
-			} else if($days>=30 && $days<=59) {
-				$output = $tokens[3];
-			} else if($days>=60 && $days<=364) {
-				$output = preg_replace("/@x/i", intval($days/30), $tokens[4]);
-			} else if($days>=365 && $days<=729) {
-				$output = $tokens[5];
-			} else {
-				$output = preg_replace("/@x/i", intval($days/365.25), $tokens[6]);
-			}
-		} else {
-			$output = $this->getDateFormatted($timestamp, $format);
-		}
-		return $output;
-	}
-	
-	// Return languages
-	function getLanguages()
-	{
-		$languages = array();
-		foreach($this->text as $key=>$value) array_push($languages, $key);
-		return $languages;
-	}
-	
-	// Return text modification date, Unix time or HTTP format
-	function getModified($httpFormat = false)
-	{
-		return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
-	}
+class YellowText {
+    public $yellow;         //access to API
+    public $modified;       //text modification date
+    public $text;           //text
+    public $language;       //current language
+    
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+        $this->modified = 0;
+        $this->text = new YellowDataCollection();
+    }
+    
+    // Load text strings from file
+    public function load($fileName, $languageDefault) {
+        $path = dirname($fileName);
+        $regex = "/^".basename($fileName)."$/";
+        foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false) as $entry) {
+            if (defined("DEBUG") && DEBUG>=2) echo "YellowText::load file:$entry<br/>\n";
+            $language = $languageDefault;
+            $this->modified = max($this->modified, filemtime($entry));
+            $fileData = $this->yellow->toolbox->readFile($entry);
+            foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+                if (preg_match("/^\#/", $line)) continue;
+                preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+                if (lcfirst($matches[1])=="language" && !strempty($matches[2])) $language = $matches[2];
+                if (!empty($language) && !empty($matches[1]) && !strempty($matches[2])) {
+                    $this->setText($matches[1], $matches[2], $language);
+                    if (defined("DEBUG") && DEBUG>=3) echo "YellowText::load $matches[1]:$matches[2]<br/>\n";
+                }
+            }
+        }
+    }
+    
+    // Set current language
+    public function setLanguage($language) {
+        $this->language = $language;
+    }
+    
+    // Set text string for specific language
+    public function setText($key, $value, $language) {
+        if (is_null($this->text[$language])) $this->text[$language] = new YellowDataCollection();
+        $this->text[$language][$key] = $value;
+    }
+    
+    // Return text string
+    public function get($key) {
+        return $this->getText($key, $this->language);
+    }
+    
+    // Return text string, HTML encoded
+    public function getHtml($key) {
+        return htmlspecialchars($this->getText($key, $this->language));
+    }
+    
+    // Return text string for specific language
+    public function getText($key, $language) {
+        return $this->isExisting($key, $language) ? $this->text[$language][$key] : "[$key]";
+    }
+    
+    // Return text string for specific language, HTML encoded
+    public function getTextHtml($key, $language) {
+        return htmlspecialchars($this->getText($key, $language));
+    }
+    
+    // Return text strings
+    public function getData($filterStart = "", $language = "") {
+        $text = array();
+        if (empty($language)) $language = $this->language;
+        if ($this->isLanguage($language)) {
+            if (empty($filterStart)) {
+                $text = $this->text[$language];
+            } else {
+                foreach ($this->text[$language] as $key=>$value) {
+                    if (substru($key, 0, strlenu($filterStart))==$filterStart) $text[$key] = $value;
+                }
+            }
+        }
+        return $text;
+    }
+    
+    // Return human readable date, custom date
+    public function getDateFormatted($timestamp, $format) {
+        $dateMonths = preg_split("/\s*,\s*/", $this->get("dateMonths"));
+        $dateWeekdays = preg_split("/\s*,\s*/", $this->get("dateWeekdays"));
+        $month = $dateMonths[date("n", $timestamp) - 1];
+        $weekday = $dateWeekdays[date("N", $timestamp) - 1];
+        $timeZone = $this->yellow->config->get("timezone");
+        $timeZoneHelper = new DateTime(null, new DateTimeZone($timeZone));
+        $timeZoneOffset = $timeZoneHelper->getOffset();
+        $timeZoneAbbreviation = "GMT".($timeZoneOffset<0 ? "-" : "+").abs(intval($timeZoneOffset/3600));
+        $format = preg_replace("/(?<!\\\)F/", addcslashes($month, "A..Za..z"), $format);
+        $format = preg_replace("/(?<!\\\)M/", addcslashes(substru($month, 0, 3), "A..Za..z"), $format);
+        $format = preg_replace("/(?<!\\\)D/", addcslashes(substru($weekday, 0, 3), "A..Za..z"), $format);
+        $format = preg_replace("/(?<!\\\)l/", addcslashes($weekday, "A..Za..z"), $format);
+        $format = preg_replace("/(?<!\\\)T/", addcslashes($timeZoneAbbreviation, "A..Za..z"), $format);
+        return date($format, $timestamp);
+    }
+    
+    // Return human readable date, relative to today
+    public function getDateRelative($timestamp, $format, $daysLimit) {
+        $timeDifference = time() - $timestamp;
+        $days = abs(intval($timeDifference / 86400));
+        if ($days<=$daysLimit || $daysLimit==0) {
+            $tokens = preg_split("/\s*,\s*/", $this->get($timeDifference>=0 ? "datePast" : "dateFuture"));
+            if ($days==0) {
+                $output = $tokens[0];
+            } elseif ($days==1) {
+                $output = $tokens[1];
+            } elseif ($days>=2 && $days<=29) {
+                $output = preg_replace("/@x/i", $days, $tokens[2]);
+            } elseif ($days>=30 && $days<=59) {
+                $output = $tokens[3];
+            } elseif ($days>=60 && $days<=364) {
+                $output = preg_replace("/@x/i", intval($days/30), $tokens[4]);
+            } elseif ($days>=365 && $days<=729) {
+                $output = $tokens[5];
+            } else {
+                $output = preg_replace("/@x/i", intval($days/365.25), $tokens[6]);
+            }
+        } else {
+            $output = $this->getDateFormatted($timestamp, $format);
+        }
+        return $output;
+    }
+    
+    // Return languages
+    public function getLanguages() {
+        $languages = array();
+        foreach ($this->text as $key=>$value) {
+            array_push($languages, $key);
+        }
+        return $languages;
+    }
+    
+    // Return text modification date, Unix time or HTTP format
+    public function getModified($httpFormat = false) {
+        return $httpFormat ? $this->yellow->toolbox->getHttpDateFormatted($this->modified) : $this->modified;
+    }
 
-	// Normalise date into known format
-	function normaliseDate($text)
-	{
-		if(preg_match("/^\d+\-\d+$/", $text))
-		{
-			$output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatShort"));
-		} else if(preg_match("/^\d+\-\d+\-\d+$/", $text)) {
-			$output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatMedium"));
-		} else if(preg_match("/^\d+\-\d+\-\d+ \d+\:\d+$/", $text)) {
-			$output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatLong"));
-		} else {
-			$output = $text;
-		}
-		return $output;
-	}
-	
-	// Check if language exists
-	function isLanguage($language)
-	{
-		return !is_null($this->text[$language]);
-	}
-	
-	// Check if text string exists
-	function isExisting($key, $language = "")
-	{
-		if(empty($language)) $language = $this->language;
-		return !is_null($this->text[$language]) && !is_null($this->text[$language][$key]);
-	}
+    // Normalise date into known format
+    public function normaliseDate($text) {
+        if (preg_match("/^\d+\-\d+$/", $text)) {
+            $output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatShort"));
+        } elseif (preg_match("/^\d+\-\d+\-\d+$/", $text)) {
+            $output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatMedium"));
+        } elseif (preg_match("/^\d+\-\d+\-\d+ \d+\:\d+$/", $text)) {
+            $output = $this->getDateFormatted(strtotime($text), $this->get("dateFormatLong"));
+        } else {
+            $output = $text;
+        }
+        return $output;
+    }
+    
+    // Check if language exists
+    public function isLanguage($language) {
+        return !is_null($this->text[$language]);
+    }
+    
+    // Check if text string exists
+    public function isExisting($key, $language = "") {
+        if (empty($language)) $language = $this->language;
+        return !is_null($this->text[$language]) && !is_null($this->text[$language][$key]);
+    }
 }
 
-class YellowLookup
-{
-	var $yellow;			//access to API
-	var $requestHandler;	//request handler name
-	var $commandHandler;	//command handler name
-	var $snippetArgs;		//snippet arguments
-	
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-	}
+class YellowLookup {
+    public $yellow;             //access to API
+    public $requestHandler;     //request handler name
+    public $commandHandler;     //command handler name
+    public $snippetArgs;        //snippet arguments
+    
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+    }
 
-	// Load file system information
-	function load()
-	{
-		list($pathRoot, $pathHome) = $this->detectFileSystem();
-		$this->yellow->config->set("contentRootDir", $pathRoot);
-		$this->yellow->config->set("contentHomeDir", $pathHome);
-		date_default_timezone_set($this->yellow->config->get("timezone"));
-	}
-	
-	// Detect file system
-	function detectFileSystem()
-	{
-		$path = $this->yellow->config->get("contentDir");
-		$pathRoot = $this->yellow->config->get("contentRootDir");
-		$pathHome = $this->yellow->config->get("contentHomeDir");
-		if(!$this->yellow->config->get("multiLanguageMode")) $pathRoot = "";
-		if(!empty($pathRoot))
-		{
-			$token = $root = rtrim($pathRoot, '/');
-			foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry)
-			{
-				if(empty($firstRoot)) { $firstRoot = $token = $entry; }
-				if($this->normaliseToken($entry)==$root) { $token = $entry; break; }
-			}
-			$pathRoot = $this->normaliseToken($token)."/";
-			$path .= "$firstRoot/";
-		}
-		if(!empty($pathHome))
-		{
-			$token = $home = rtrim($pathHome, '/');
-			foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry)
-			{
-				if(empty($firstHome)) { $firstHome = $token = $entry; }
-				if($this->normaliseToken($entry)==$home) { $token = $entry; break; }
-			}
-			$pathHome = $this->normaliseToken($token)."/";
-		}
-		return array($pathRoot, $pathHome);
-	}
-	
-	// Return root locations
-	function findRootLocations($includePath = true)
-	{
-		$locations = array();
-		$pathBase = $this->yellow->config->get("contentDir");
-		$pathRoot = $this->yellow->config->get("contentRootDir");
-		if(!empty($pathRoot))
-		{
-			foreach($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry)
-			{
-				$token = $this->normaliseToken($entry)."/";
-				if($token==$pathRoot) $token = "";
-				array_push($locations, $includePath ? "root/$token $pathBase$entry/" : "root/$token");
-				if(defined("DEBUG") && DEBUG>=2) echo "YellowLookup::findRootLocations root/$token<br/>\n";
-			}
-		} else {
-			array_push($locations, $includePath ? "root/ $pathBase" : "root/");
-		}
-		return $locations;
-	}
-	
-	// Return location from file path
-	function findLocationFromFile($fileName)
-	{
-		$location = "/";
-		$pathBase = $this->yellow->config->get("contentDir");
-		$pathRoot = $this->yellow->config->get("contentRootDir");
-		$pathHome = $this->yellow->config->get("contentHomeDir");
-		$fileDefault = $this->yellow->config->get("contentDefaultFile");
-		$fileExtension = $this->yellow->config->get("contentExtension");
-		if(substru($fileName, 0, strlenu($pathBase))==$pathBase)
-		{
-			$fileName = substru($fileName, strlenu($pathBase));
-			$tokens = explode('/', $fileName);
-			if(!empty($pathRoot))
-			{
-				$token = $this->normaliseToken($tokens[0]).'/';
-				if($token!=$pathRoot) $location .= $token;
-				array_shift($tokens);
-			}
-			for($i=0; $i<count($tokens)-1; ++$i)
-			{
-				$token = $this->normaliseToken($tokens[$i]).'/';
-				if($i || $token!=$pathHome) $location .= $token;
-			}
-			$token = $this->normaliseToken($tokens[$i], $fileExtension);
-			$fileFolder = $this->normaliseToken($tokens[$i-1], $fileExtension);
-			if($token!=$fileDefault && $token!=$fileFolder) $location .= $this->normaliseToken($tokens[$i], $fileExtension, true);
-			$extension = ($pos = strrposu($fileName, '.')) ? substru($fileName, $pos) : "";
-			if($extension!=$fileExtension) $invalid = true;
-		} else {
-			$invalid = true;
-		}
-		if(defined("DEBUG") && DEBUG>=2)
-		{
-			$debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName";
-			echo "YellowLookup::findLocationFromFile $debug<br/>\n";
-		}
-		return $invalid ? "" : $location;
-	}
-	
-	// Return file path from location
-	function findFileFromLocation($location, $directory = false)
-	{
-		$path = $this->yellow->config->get("contentDir");
-		$pathRoot = $this->yellow->config->get("contentRootDir");
-		$pathHome = $this->yellow->config->get("contentHomeDir");
-		$fileDefault = $this->yellow->config->get("contentDefaultFile");
-		$fileExtension = $this->yellow->config->get("contentExtension");
-		$tokens = explode('/', $location);
-		if($this->isRootLocation($location))
-		{
-			if(!empty($pathRoot))
-			{
-				$token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, '/');
-				$path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid);
-			}
-		} else {
-			if(!empty($pathRoot))
-			{
-				if(count($tokens)>2)
-				{
-					if($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathRoot, '/'))) $invalid = true;
-					$path .= $this->findFileDirectory($path, $tokens[1], "", true, false, $found, $invalid);
-					if($found) array_shift($tokens);
-				}
-				if(!$found) $path .= $this->findFileDirectory($path, rtrim($pathRoot, '/'), "", true, true, $found, $invalid);
-				
-			}
-			if(count($tokens)>2)
-			{
-				if($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathHome, '/'))) $invalid = true;
-				for($i=1; $i<count($tokens)-1; ++$i)
-				{
-					$path .= $this->findFileDirectory($path, $tokens[$i], "", true, true, $found, $invalid);
-				}
-			} else {
-				$i = 1;
-				$tokens[0] = rtrim($pathHome, '/');
-				$path .= $this->findFileDirectory($path, $tokens[0], "", true, true, $found, $invalid);
-			}
-			if(!$directory)
-			{
-				if(!strempty($tokens[$i]))
-				{
-					$token = $tokens[$i].$fileExtension;
-					$fileFolder = $tokens[$i-1].$fileExtension;
-					if($token==$fileDefault || $token==$fileFolder) $invalid = true;
-					$path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid);
-				} else {
-					$path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false);
-				}
-				if(defined("DEBUG") && DEBUG>=2)
-				{
-					$debug = "$location -> ".($invalid ? "INVALID" : $path);
-					echo "YellowLookup::findFileFromLocation $debug<br/>\n";
-				}
-			}
-		}
-		return $invalid ? "" : $path;
-	}
-	
-	// Return file or directory that matches token
-	function findFileDirectory($path, $token, $fileExtension, $directory, $default, &$found, &$invalid)
-	{
-		if($this->normaliseToken($token, $fileExtension)!=$token) $invalid = true;
-		if(!$invalid)
-		{
-			$regex = "/^[\d\-\_\.]*".strreplaceu('-', '.', $token)."$/";
-			foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, $directory, false) as $entry)
-			{
-				if($this->normaliseToken($entry, $fileExtension)==$token) { $token = $entry; $found = true; break; }
-			}
-		}
-		if($directory) $token .= '/';
-		return ($default || $found) ? $token : "";
-	}
-	
-	// Return default file in directory
-	function findFileDefault($path, $fileDefault, $fileExtension, $includePath = true)
-	{
-		$token = $fileDefault;
-		if(!is_file($path."/".$fileDefault))
-		{
-			$fileFolder = $this->normaliseToken(basename($path), $fileExtension);
-			$regex = "/^[\d\-\_\.]*($fileDefault|$fileFolder)$/";
-			foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry)
-			{
-				if($this->normaliseToken($entry, $fileExtension)==$fileDefault) { $token = $entry; break; }
-				if($this->normaliseToken($entry, $fileExtension)==$fileFolder) { $token = $entry; break; }
-			}
-		}
-		return $includePath ? "$path/$token" : $token;
-	}
-	
-	// Return new file
-	function findFileNew($location, $filePrefix = "")
-	{
-		$fileName = $this->findFileFromLocation($location);
-		if(!empty($filePrefix) && !empty($fileName))
-		{
-			preg_match("/^([\d\-\_\.]*)(.*)$/", $filePrefix, $matches);
-			$filePrefix = empty($matches[1]) ? "" : $matches[1].'-';
-			$fileText = $this->normaliseName(basename($fileName), true, true);
-			if(preg_match("/^[\d\-\_\.]*$/", $fileText) && !empty($filePrefix)) $filePrefix = "";
-			$fileName = dirname($fileName)."/".$filePrefix.$fileText.$this->yellow->config->get("contentExtension");
-		}
-		if(!is_dir(dirname($fileName)))
-		{
-			$tokens = explode('/', $fileName);
-			for($i=0; $i<count($tokens)-1; ++$i)
-			{
-				if(!is_dir($path.$tokens[$i]))
-				{
-					if(!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i]))
-					{
-						$number = 1;
-						foreach($this->yellow->toolbox->getDirectoryEntries($path, "/^[\d\-\_\.]+(.*)$/", true, true, false) as $entry)
-						{
-							if($number!=1 && $number!=intval($entry)) break;
-							$number = intval($entry)+1;
-						}
-						$tokens[$i] = "$number-".$tokens[$i];
-					}
-					$tokens[$i] = $this->normaliseName($tokens[$i], false, false, true);
-				}
-				$path .= $tokens[$i]."/";
-			}
-			$fileName = $path.$tokens[$i];
-		}
-		return $fileName;
-	}
-	
-	// Return static file if possible
-	function findFileStatic($location, $fileName, $cacheable)
-	{
-		if($cacheable)
-		{
-			$location .= $this->yellow->toolbox->getLocationArgs();
-			$fileNameStatic = rtrim($this->yellow->config->get("staticDir"), '/').$location;
-			if(!$this->isFileLocation($location)) $fileNameStatic .= $this->yellow->config->get("staticDefaultFile");
-			if(is_readable($fileNameStatic)) $fileName = $fileNameStatic;
-		}
-		return $fileName;
-	}
-	
-	// Return children from location
-	function findChildrenFromLocation($location)
-	{
-		$fileNames = array();
-		$fileDefault = $this->yellow->config->get("contentDefaultFile");
-		$fileExtension = $this->yellow->config->get("contentExtension");
-		if(!$this->isFileLocation($location))
-		{
-			$path = $this->findFileFromLocation($location, true);
-			foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry)
-			{
-				$token = $this->findFileDefault($path.$entry, $fileDefault, $fileExtension, false);
-				array_push($fileNames, $path.$entry."/".$token);
-			}
-			if(!$this->isRootLocation($location))
-			{
-				$fileFolder = $this->normaliseToken(basename($path), $fileExtension);
-				$regex = "/^.*\\".$fileExtension."$/";
-				foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry)
-				{
-					if($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue;
-					if($this->normaliseToken($entry, $fileExtension)==$fileFolder) continue;
-					array_push($fileNames, $path.$entry);
-				}
-			}
-		}
-		return $fileNames;
-	}
+    // Load file system information
+    public function load() {
+        list($pathRoot, $pathHome) = $this->detectFileSystem();
+        $this->yellow->config->set("contentRootDir", $pathRoot);
+        $this->yellow->config->set("contentHomeDir", $pathHome);
+        date_default_timezone_set($this->yellow->config->get("timezone"));
+    }
+    
+    // Detect file system
+    public function detectFileSystem() {
+        $path = $this->yellow->config->get("contentDir");
+        $pathRoot = $this->yellow->config->get("contentRootDir");
+        $pathHome = $this->yellow->config->get("contentHomeDir");
+        if (!$this->yellow->config->get("multiLanguageMode")) $pathRoot = "";
+        if (!empty($pathRoot)) {
+            $token = $root = rtrim($pathRoot, "/");
+            foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) {
+                if (empty($firstRoot)) $firstRoot = $token = $entry;
+                if ($this->normaliseToken($entry)==$root) {
+                    $token = $entry;
+                    break;
+                }
+            }
+            $pathRoot = $this->normaliseToken($token)."/";
+            $path .= "$firstRoot/";
+        }
+        if (!empty($pathHome)) {
+            $token = $home = rtrim($pathHome, "/");
+            foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) {
+                if (empty($firstHome)) $firstHome = $token = $entry;
+                if ($this->normaliseToken($entry)==$home) {
+                    $token = $entry;
+                    break;
+                }
+            }
+            $pathHome = $this->normaliseToken($token)."/";
+        }
+        return array($pathRoot, $pathHome);
+    }
+    
+    // Return root locations
+    public function findRootLocations($includePath = true) {
+        $locations = array();
+        $pathBase = $this->yellow->config->get("contentDir");
+        $pathRoot = $this->yellow->config->get("contentRootDir");
+        if (!empty($pathRoot)) {
+            foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true, false) as $entry) {
+                $token = $this->normaliseToken($entry)."/";
+                if ($token==$pathRoot) $token = "";
+                array_push($locations, $includePath ? "root/$token $pathBase$entry/" : "root/$token");
+                if (defined("DEBUG") && DEBUG>=2) echo "YellowLookup::findRootLocations root/$token<br/>\n";
+            }
+        } else {
+            array_push($locations, $includePath ? "root/ $pathBase" : "root/");
+        }
+        return $locations;
+    }
+    
+    // Return location from file path
+    public function findLocationFromFile($fileName) {
+        $location = "/";
+        $pathBase = $this->yellow->config->get("contentDir");
+        $pathRoot = $this->yellow->config->get("contentRootDir");
+        $pathHome = $this->yellow->config->get("contentHomeDir");
+        $fileDefault = $this->yellow->config->get("contentDefaultFile");
+        $fileExtension = $this->yellow->config->get("contentExtension");
+        if (substru($fileName, 0, strlenu($pathBase))==$pathBase) {
+            $fileName = substru($fileName, strlenu($pathBase));
+            $tokens = explode("/", $fileName);
+            if (!empty($pathRoot)) {
+                $token = $this->normaliseToken($tokens[0])."/";
+                if ($token!=$pathRoot) $location .= $token;
+                array_shift($tokens);
+            }
+            for ($i=0; $i<count($tokens)-1; ++$i) {
+                $token = $this->normaliseToken($tokens[$i])."/";
+                if ($i || $token!=$pathHome) $location .= $token;
+            }
+            $token = $this->normaliseToken($tokens[$i], $fileExtension);
+            $fileFolder = $this->normaliseToken($tokens[$i-1], $fileExtension);
+            if ($token!=$fileDefault && $token!=$fileFolder) {
+                $location .= $this->normaliseToken($tokens[$i], $fileExtension, true);
+            }
+            $extension = ($pos = strrposu($fileName, ".")) ? substru($fileName, $pos) : "";
+            if ($extension!=$fileExtension) $invalid = true;
+        } else {
+            $invalid = true;
+        }
+        if (defined("DEBUG") && DEBUG>=2) {
+            $debug = ($invalid ? "INVALID" : $location)." <- $pathBase$fileName";
+            echo "YellowLookup::findLocationFromFile $debug<br/>\n";
+        }
+        return $invalid ? "" : $location;
+    }
+    
+    // Return file path from location
+    public function findFileFromLocation($location, $directory = false) {
+        $path = $this->yellow->config->get("contentDir");
+        $pathRoot = $this->yellow->config->get("contentRootDir");
+        $pathHome = $this->yellow->config->get("contentHomeDir");
+        $fileDefault = $this->yellow->config->get("contentDefaultFile");
+        $fileExtension = $this->yellow->config->get("contentExtension");
+        $tokens = explode("/", $location);
+        if ($this->isRootLocation($location)) {
+            if (!empty($pathRoot)) {
+                $token = (count($tokens)>2) ? $tokens[1] : rtrim($pathRoot, "/");
+                $path .= $this->findFileDirectory($path, $token, "", true, true, $found, $invalid);
+            }
+        } else {
+            if (!empty($pathRoot)) {
+                if (count($tokens)>2) {
+                    if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathRoot, "/"))) $invalid = true;
+                    $path .= $this->findFileDirectory($path, $tokens[1], "", true, false, $found, $invalid);
+                    if ($found) array_shift($tokens);
+                }
+                if (!$found) {
+                    $path .= $this->findFileDirectory($path, rtrim($pathRoot, "/"), "", true, true, $found, $invalid);
+                }
+            }
+            if (count($tokens)>2) {
+                if ($this->normaliseToken($tokens[1])==$this->normaliseToken(rtrim($pathHome, "/"))) $invalid = true;
+                for ($i=1; $i<count($tokens)-1; ++$i) {
+                    $path .= $this->findFileDirectory($path, $tokens[$i], "", true, true, $found, $invalid);
+                }
+            } else {
+                $i = 1;
+                $tokens[0] = rtrim($pathHome, "/");
+                $path .= $this->findFileDirectory($path, $tokens[0], "", true, true, $found, $invalid);
+            }
+            if (!$directory) {
+                if (!strempty($tokens[$i])) {
+                    $token = $tokens[$i].$fileExtension;
+                    $fileFolder = $tokens[$i-1].$fileExtension;
+                    if ($token==$fileDefault || $token==$fileFolder) $invalid = true;
+                    $path .= $this->findFileDirectory($path, $token, $fileExtension, false, true, $found, $invalid);
+                } else {
+                    $path .= $this->findFileDefault($path, $fileDefault, $fileExtension, false);
+                }
+                if (defined("DEBUG") && DEBUG>=2) {
+                    $debug = "$location -> ".($invalid ? "INVALID" : $path);
+                    echo "YellowLookup::findFileFromLocation $debug<br/>\n";
+                }
+            }
+        }
+        return $invalid ? "" : $path;
+    }
+    
+    // Return file or directory that matches token
+    public function findFileDirectory($path, $token, $fileExtension, $directory, $default, &$found, &$invalid) {
+        if ($this->normaliseToken($token, $fileExtension)!=$token) $invalid = true;
+        if (!$invalid) {
+            $regex = "/^[\d\-\_\.]*".strreplaceu("-", ".", $token)."$/";
+            foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, $directory, false) as $entry) {
+                if ($this->normaliseToken($entry, $fileExtension)==$token) {
+                    $token = $entry;
+                    $found = true;
+                    break;
+                }
+            }
+        }
+        if ($directory) $token .= "/";
+        return ($default || $found) ? $token : "";
+    }
+    
+    // Return default file in directory
+    public function findFileDefault($path, $fileDefault, $fileExtension, $includePath = true) {
+        $token = $fileDefault;
+        if (!is_file($path."/".$fileDefault)) {
+            $fileFolder = $this->normaliseToken(basename($path), $fileExtension);
+            $regex = "/^[\d\-\_\.]*($fileDefault|$fileFolder)$/";
+            foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) {
+                if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) {
+                    $token = $entry;
+                    break;
+                }
+                if ($this->normaliseToken($entry, $fileExtension)==$fileFolder) {
+                    $token = $entry;
+                    break;
+                }
+            }
+        }
+        return $includePath ? "$path/$token" : $token;
+    }
+    
+    // Return new file
+    public function findFileNew($location, $filePrefix = "") {
+        $fileName = $this->findFileFromLocation($location);
+        if (!empty($filePrefix) && !empty($fileName)) {
+            preg_match("/^([\d\-\_\.]*)(.*)$/", $filePrefix, $matches);
+            $filePrefix = empty($matches[1]) ? "" : $matches[1]."-";
+            $fileText = $this->normaliseName(basename($fileName), true, true);
+            if (preg_match("/^[\d\-\_\.]*$/", $fileText) && !empty($filePrefix)) $filePrefix = "";
+            $fileName = dirname($fileName)."/".$filePrefix.$fileText.$this->yellow->config->get("contentExtension");
+        }
+        if (!is_dir(dirname($fileName))) {
+            $tokens = explode("/", $fileName);
+            for ($i=0; $i<count($tokens)-1; ++$i) {
+                if (!is_dir($path.$tokens[$i])) {
+                    if (!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i])) {
+                        $number = 1;
+                        foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^[\d\-\_\.]+(.*)$/", true, true, false) as $entry) {
+                            if ($number!=1 && $number!=intval($entry)) break;
+                            $number = intval($entry)+1;
+                        }
+                        $tokens[$i] = "$number-".$tokens[$i];
+                    }
+                    $tokens[$i] = $this->normaliseName($tokens[$i], false, false, true);
+                }
+                $path .= $tokens[$i]."/";
+            }
+            $fileName = $path.$tokens[$i];
+        }
+        return $fileName;
+    }
+    
+    // Return static file if possible
+    public function findFileStatic($location, $fileName, $cacheable) {
+        if ($cacheable) {
+            $location .= $this->yellow->toolbox->getLocationArgs();
+            $fileNameStatic = rtrim($this->yellow->config->get("staticDir"), "/").$location;
+            if (!$this->isFileLocation($location)) $fileNameStatic .= $this->yellow->config->get("staticDefaultFile");
+            if (is_readable($fileNameStatic)) $fileName = $fileNameStatic;
+        }
+        return $fileName;
+    }
+    
+    // Return children from location
+    public function findChildrenFromLocation($location) {
+        $fileNames = array();
+        $fileDefault = $this->yellow->config->get("contentDefaultFile");
+        $fileExtension = $this->yellow->config->get("contentExtension");
+        if (!$this->isFileLocation($location)) {
+            $path = $this->findFileFromLocation($location, true);
+            foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false) as $entry) {
+                $token = $this->findFileDefault($path.$entry, $fileDefault, $fileExtension, false);
+                array_push($fileNames, $path.$entry."/".$token);
+            }
+            if (!$this->isRootLocation($location)) {
+                $fileFolder = $this->normaliseToken(basename($path), $fileExtension);
+                $regex = "/^.*\\".$fileExtension."$/";
+                foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) {
+                    if ($this->normaliseToken($entry, $fileExtension)==$fileDefault) continue;
+                    if ($this->normaliseToken($entry, $fileExtension)==$fileFolder) continue;
+                    array_push($fileNames, $path.$entry);
+                }
+            }
+        }
+        return $fileNames;
+    }
 
-	// Return language from file path
-	function findLanguageFromFile($fileName, $languageDefault)
-	{
-		$language = $languageDefault;
-		$pathBase = $this->yellow->config->get("contentDir");
-		$pathRoot = $this->yellow->config->get("contentRootDir");
-		if(!empty($pathRoot))
-		{
-			$fileName = substru($fileName, strlenu($pathBase));
-			if(preg_match("/^(.+?)\//", $fileName, $matches)) $name = $this->normaliseToken($matches[1]);
-			if(strlenu($name)==2) $language = $name;
-		}
-		return $language;
-	}
+    // Return language from file path
+    public function findLanguageFromFile($fileName, $languageDefault) {
+        $language = $languageDefault;
+        $pathBase = $this->yellow->config->get("contentDir");
+        $pathRoot = $this->yellow->config->get("contentRootDir");
+        if (!empty($pathRoot)) {
+            $fileName = substru($fileName, strlenu($pathBase));
+            if (preg_match("/^(.+?)\//", $fileName, $matches)) $name = $this->normaliseToken($matches[1]);
+            if (strlenu($name)==2) $language = $name;
+        }
+        return $language;
+    }
 
-	// Return file path from media location
-	function findFileFromMedia($location)
-	{
-		if($this->isFileLocation($location))
-		{
-			$mediaLocationLength = strlenu($this->yellow->config->get("mediaLocation"));
-			if(substru($location, 0, $mediaLocationLength)==$this->yellow->config->get("mediaLocation"))
-			{
-				$fileName = $this->yellow->config->get("mediaDir").substru($location, 7);
-			}
-		}
-		return $fileName;
-	}
-	
-	// Return file path from system location
-	function findFileFromSystem($location)
-	{
-		if(preg_match("/\.(css|gif|ico|js|jpg|png|svg|txt|woff|woff2)$/", $location))
-		{
-			$pluginLocationLength = strlenu($this->yellow->config->get("pluginLocation"));
-			$themeLocationLength = strlenu($this->yellow->config->get("themeLocation"));
-			if(substru($location, 0, $pluginLocationLength)==$this->yellow->config->get("pluginLocation")) {
-				$fileName = $this->yellow->config->get("pluginDir").substru($location, $pluginLocationLength);
-			} else if(substru($location, 0, $themeLocationLength)==$this->yellow->config->get("themeLocation")) {
-				$fileName = $this->yellow->config->get("themeDir").substru($location, $themeLocationLength);
-			} else if($location=="/".$this->yellow->config->get("robotsFile")) {
-				$fileName = $this->yellow->config->get("configDir").$this->yellow->config->get("robotsFile");
-			} else if($location=="/".$this->yellow->config->get("faviconFile")) {
-				$fileName = $this->yellow->config->get("assetDir").$this->yellow->config->get("siteicon").".png";
-			}
-		}
-		return $fileName;
-	}
-	
-	// Normalise file/directory token
-	function normaliseToken($text, $fileExtension = "", $removeExtension = false)
-	{
-		if(!empty($fileExtension)) $text = ($pos = strrposu($text, '.')) ? substru($text, 0, $pos) : $text;
-		if(preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !empty($matches[1])) $text = $matches[1];
-		return preg_replace("/[^\pL\d\-\_]/u", "-", $text).($removeExtension ? "" : $fileExtension);
-	}
-	
-	// Normalise name
-	function normaliseName($text, $removePrefix = false, $removeExtension = false, $filterStrict = false)
-	{
-		if($removeExtension) $text = ($pos = strrposu($text, '.')) ? substru($text, 0, $pos) : $text;
-		if($removePrefix && preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !empty($matches[1])) $text = $matches[1];
-		if($filterStrict) $text = strtoloweru($text);
-		return preg_replace("/[^\pL\d\-\_]/u", "-", $text);
-	}
-	
-	// Normalise array, make keys with same upper/lower case
-	function normaliseUpperLower($input)
-	{
-		$array = array();
-		foreach($input as $key=>$value)
-		{
-			if(empty($key) || strempty($value)) continue;
-			$keySearch = strtoloweru($key);
-			foreach($array as $keyNew=>$valueNew) if(strtoloweru($keyNew)==$keySearch) { $key = $keyNew; break; }
-			$array[$key] += $value;
-		}
-		return $array;
-	}
-	
-	// Normalise location, make absolute location
-	function normaliseLocation($location, $pageLocation, $filterStrict = true)
-	{
-		if(!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8"))))
-		{
-			$pageBase = $this->yellow->page->base;
-			$mediaBase = $this->yellow->config->get("serverBase").$this->yellow->config->get("mediaLocation");
-			if(preg_match("/^\#/", $location))
-			{
-				$location = $pageBase.$pageLocation.$location;
-			} else if(!preg_match("/^\//", $location)) {
-				$location = $this->getDirectoryLocation($pageBase.$pageLocation).$location;
-			} else if(!preg_match("#^($pageBase|$mediaBase)#", $location)) {
-				$location = $pageBase.$location;
-			}
-			$location = strreplaceu("/./", "/", $location);
-			$location = strreplaceu(':', $this->yellow->toolbox->getLocationArgsSeparator(), $location);
-		} else {
-			if($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter";
-		}
-		return $location;
-	}
-	
-	// Normalise URL, make absolute URL
-	function normaliseUrl($scheme, $address, $base, $location, $filterStrict = true)
-	{
-		if(!preg_match("/^\w+:/", $location))
-		{
-			$url = "$scheme://$address$base$location";
-		} else {
-			if($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter";
-			$url = $location;
-		}
-		return $url;
-	}
-	
-	// Return URL information
-	function getUrlInformation($url)
-	{
-		if(preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, '/'), $matches))
-		{
-			$scheme = $matches[1];
-			$address = $matches[2];
-			$base = $matches[3];
-		}
-		return array($scheme, $address, $base);
-	}
-	
-	// Return directory location
-	function getDirectoryLocation($location)
-	{
-		return ($pos = strrposu($location, '/')) ? substru($location, 0, $pos+1) : "/";
-	}
-	
-	// Check if location is specifying root
-	function isRootLocation($location)
-	{
-		return $location[0]!="/";
-	}
-	
-	// Check if location is specifying file or directory
-	function isFileLocation($location)
-	{
-		return substru($location, -1, 1)!="/";
-	}
-	
-	// Check if location can be redirected into directory
-	function isRedirectLocation($location)
-	{
-		$redirect = false;
-		if($this->isFileLocation($location))
-		{
-			$redirect = is_dir($this->findFileFromLocation("$location/", true));
-		} else if($location=="/") {
-			$redirect = $this->yellow->config->get("multiLanguageMode");
-		}
-		return $redirect;
-	}
-	
-	// Check if location contains nested directories
-	function isNestedLocation($location, $fileName, $checkHomeLocation = false)
-	{
-		$nested = false;
-		if(!$checkHomeLocation || $location==$this->yellow->pages->getHomeLocation($location))
-		{
-			$path = dirname($fileName);
-			if(count($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true;
-		}
-		return $nested;
-	}
-	
-	// Check if location is visible
-	function isVisibleLocation($location, $fileName)
-	{
-		$visible = true;
-		$pathBase = $this->yellow->config->get("contentDir");
-		if(substru($fileName, 0, strlenu($pathBase))==$pathBase)
-		{
-			$fileName = substru($fileName, strlenu($pathBase));
-			$tokens = explode('/', $fileName);
-			for($i=0; $i<count($tokens)-1; ++$i)
-			{
-				if(!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i])) { $visible = false; break; }
-			}
-		} else {
-			$visible = false;
-		}
-		return $visible;
-	}
-	
-	// Check if location is within current request
-	function isActiveLocation($location, $currentLocation)
-	{
-		if($this->isFileLocation($location))
-		{
-			$active = $currentLocation==$location;
-		} else {
-			if($location==$this->yellow->pages->getHomeLocation($location))
-			{
-				$active = $this->getDirectoryLocation($currentLocation)==$location;
-			} else {
-				$active = substru($currentLocation, 0, strlenu($location))==$location;
-			}
-		}
-		return $active;
-	}
-	
-	// Check if file is valid
-	function isValidFile($fileName)
-	{
-		$contentDirLength = strlenu($this->yellow->config->get("contentDir"));
-		$mediaDirLength = strlenu($this->yellow->config->get("mediaDir"));
-		$systemDirLength = strlenu($this->yellow->config->get("systemDir"));
-		return substru($fileName, 0, $contentDirLength)==$this->yellow->config->get("contentDir") ||
-			substru($fileName, 0, $mediaDirLength)==$this->yellow->config->get("mediaDir") ||
-			substru($fileName, 0, $systemDirLength)==$this->yellow->config->get("systemDir");
-	}
-	
-	// Check if content file
-	function isContentFile($fileName)
-	{
-		$contentDirLength = strlenu($this->yellow->config->get("contentDir"));
-		return substru($fileName, 0, $contentDirLength)==$this->yellow->config->get("contentDir");
-	}
-	
-	// Check if media file
-	function isMediaFile($fileName)
-	{
-		$mediaDirLength = strlenu($this->yellow->config->get("mediaDir"));
-		return substru($fileName, 0, $mediaDirLength)==$this->yellow->config->get("mediaDir");
-	}
-	
-	// Check if system file
-	function isSystemFile($fileName)
-	{
-		$systemDirLength = strlenu($this->yellow->config->get("systemDir"));
-		return substru($fileName, 0, $systemDirLength)==$this->yellow->config->get("systemDir");
-	}
+    // Return file path from media location
+    public function findFileFromMedia($location) {
+        if ($this->isFileLocation($location)) {
+            $mediaLocationLength = strlenu($this->yellow->config->get("mediaLocation"));
+            if (substru($location, 0, $mediaLocationLength)==$this->yellow->config->get("mediaLocation")) {
+                $fileName = $this->yellow->config->get("mediaDir").substru($location, 7);
+            }
+        }
+        return $fileName;
+    }
+    
+    // Return file path from system location
+    public function findFileFromSystem($location) {
+        if (preg_match("/\.(css|gif|ico|js|jpg|png|svg|txt|woff|woff2)$/", $location)) {
+            $pluginLocationLength = strlenu($this->yellow->config->get("pluginLocation"));
+            $themeLocationLength = strlenu($this->yellow->config->get("themeLocation"));
+            if (substru($location, 0, $pluginLocationLength)==$this->yellow->config->get("pluginLocation")) {
+                $fileName = $this->yellow->config->get("pluginDir").substru($location, $pluginLocationLength);
+            } elseif (substru($location, 0, $themeLocationLength)==$this->yellow->config->get("themeLocation")) {
+                $fileName = $this->yellow->config->get("themeDir").substru($location, $themeLocationLength);
+            } elseif ($location=="/".$this->yellow->config->get("robotsFile")) {
+                $fileName = $this->yellow->config->get("configDir").$this->yellow->config->get("robotsFile");
+            } elseif ($location=="/".$this->yellow->config->get("faviconFile")) {
+                $fileName = $this->yellow->config->get("assetDir").$this->yellow->config->get("siteicon").".png";
+            }
+        }
+        return $fileName;
+    }
+    
+    // Normalise file/directory token
+    public function normaliseToken($text, $fileExtension = "", $removeExtension = false) {
+        if (!empty($fileExtension)) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text;
+        if (preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !empty($matches[1])) $text = $matches[1];
+        return preg_replace("/[^\pL\d\-\_]/u", "-", $text).($removeExtension ? "" : $fileExtension);
+    }
+    
+    // Normalise name
+    public function normaliseName($text, $removePrefix = false, $removeExtension = false, $filterStrict = false) {
+        if ($removeExtension) $text = ($pos = strrposu($text, ".")) ? substru($text, 0, $pos) : $text;
+        if ($removePrefix && preg_match("/^[\d\-\_\.]+(.*)$/", $text, $matches) && !empty($matches[1])) $text = $matches[1];
+        if ($filterStrict) $text = strtoloweru($text);
+        return preg_replace("/[^\pL\d\-\_]/u", "-", $text);
+    }
+    
+    // Normalise array, make keys with same upper/lower case
+    public function normaliseUpperLower($input) {
+        $array = array();
+        foreach ($input as $key=>$value) {
+            if (empty($key) || strempty($value)) continue;
+            $keySearch = strtoloweru($key);
+            foreach ($array as $keyNew=>$valueNew) {
+                if (strtoloweru($keyNew)==$keySearch) {
+                    $key = $keyNew;
+                    break;
+                }
+            }
+            $array[$key] += $value;
+        }
+        return $array;
+    }
+    
+    // Normalise location, make absolute location
+    public function normaliseLocation($location, $pageLocation, $filterStrict = true) {
+        if (!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8")))) {
+            $pageBase = $this->yellow->page->base;
+            $mediaBase = $this->yellow->config->get("serverBase").$this->yellow->config->get("mediaLocation");
+            if (preg_match("/^\#/", $location)) {
+                $location = $pageBase.$pageLocation.$location;
+            } elseif (!preg_match("/^\//", $location)) {
+                $location = $this->getDirectoryLocation($pageBase.$pageLocation).$location;
+            } elseif (!preg_match("#^($pageBase|$mediaBase)#", $location)) {
+                $location = $pageBase.$location;
+            }
+            $location = strreplaceu("/./", "/", $location);
+            $location = strreplaceu(":", $this->yellow->toolbox->getLocationArgsSeparator(), $location);
+        } else {
+            if ($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter";
+        }
+        return $location;
+    }
+    
+    // Normalise URL, make absolute URL
+    public function normaliseUrl($scheme, $address, $base, $location, $filterStrict = true) {
+        if (!preg_match("/^\w+:/", $location)) {
+            $url = "$scheme://$address$base$location";
+        } else {
+            if ($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter";
+            $url = $location;
+        }
+        return $url;
+    }
+    
+    // Return URL information
+    public function getUrlInformation($url) {
+        if (preg_match("#^(\w+)://([^/]+)(.*)$#", rtrim($url, "/"), $matches)) {
+            $scheme = $matches[1];
+            $address = $matches[2];
+            $base = $matches[3];
+        }
+        return array($scheme, $address, $base);
+    }
+    
+    // Return directory location
+    public function getDirectoryLocation($location) {
+        return ($pos = strrposu($location, "/")) ? substru($location, 0, $pos+1) : "/";
+    }
+    
+    // Check if location is specifying root
+    public function isRootLocation($location) {
+        return $location[0]!="/";
+    }
+    
+    // Check if location is specifying file or directory
+    public function isFileLocation($location) {
+        return substru($location, -1, 1)!="/";
+    }
+    
+    // Check if location can be redirected into directory
+    public function isRedirectLocation($location) {
+        $redirect = false;
+        if ($this->isFileLocation($location)) {
+            $redirect = is_dir($this->findFileFromLocation("$location/", true));
+        } elseif ($location=="/") {
+            $redirect = $this->yellow->config->get("multiLanguageMode");
+        }
+        return $redirect;
+    }
+    
+    // Check if location contains nested directories
+    public function isNestedLocation($location, $fileName, $checkHomeLocation = false) {
+        $nested = false;
+        if (!$checkHomeLocation || $location==$this->yellow->pages->getHomeLocation($location)) {
+            $path = dirname($fileName);
+            if (count($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true;
+        }
+        return $nested;
+    }
+    
+    // Check if location is visible
+    public function isVisibleLocation($location, $fileName) {
+        $visible = true;
+        $pathBase = $this->yellow->config->get("contentDir");
+        if (substru($fileName, 0, strlenu($pathBase))==$pathBase) {
+            $fileName = substru($fileName, strlenu($pathBase));
+            $tokens = explode("/", $fileName);
+            for ($i=0; $i<count($tokens)-1; ++$i) {
+                if (!preg_match("/^[\d\-\_\.]+(.*)$/", $tokens[$i])) {
+                    $visible = false;
+                    break;
+                }
+            }
+        } else {
+            $visible = false;
+        }
+        return $visible;
+    }
+    
+    // Check if location is within current request
+    public function isActiveLocation($location, $currentLocation) {
+        if ($this->isFileLocation($location)) {
+            $active = $currentLocation==$location;
+        } else {
+            if ($location==$this->yellow->pages->getHomeLocation($location)) {
+                $active = $this->getDirectoryLocation($currentLocation)==$location;
+            } else {
+                $active = substru($currentLocation, 0, strlenu($location))==$location;
+            }
+        }
+        return $active;
+    }
+    
+    // Check if file is valid
+    public function isValidFile($fileName) {
+        $contentDirLength = strlenu($this->yellow->config->get("contentDir"));
+        $mediaDirLength = strlenu($this->yellow->config->get("mediaDir"));
+        $systemDirLength = strlenu($this->yellow->config->get("systemDir"));
+        return substru($fileName, 0, $contentDirLength)==$this->yellow->config->get("contentDir") ||
+            substru($fileName, 0, $mediaDirLength)==$this->yellow->config->get("mediaDir") ||
+            substru($fileName, 0, $systemDirLength)==$this->yellow->config->get("systemDir");
+    }
+    
+    // Check if content file
+    public function isContentFile($fileName) {
+        $contentDirLength = strlenu($this->yellow->config->get("contentDir"));
+        return substru($fileName, 0, $contentDirLength)==$this->yellow->config->get("contentDir");
+    }
+    
+    // Check if media file
+    public function isMediaFile($fileName) {
+        $mediaDirLength = strlenu($this->yellow->config->get("mediaDir"));
+        return substru($fileName, 0, $mediaDirLength)==$this->yellow->config->get("mediaDir");
+    }
+    
+    // Check if system file
+    public function isSystemFile($fileName) {
+        $systemDirLength = strlenu($this->yellow->config->get("systemDir"));
+        return substru($fileName, 0, $systemDirLength)==$this->yellow->config->get("systemDir");
+    }
 }
 
-class YellowToolbox
-{
-	// Return server version from current HTTP request
-	function getServerVersion($shortFormat = false)
-	{
-		$serverVersion = strtoupperu(PHP_SAPI)." ".PHP_OS;
-		if(preg_match("/^(\S+)/", $_SERVER["SERVER_SOFTWARE"], $matches)) $serverVersion = $matches[1]." ".PHP_OS;
-		if($shortFormat && preg_match("/^(\pL+)/u", $serverVersion, $matches)) $serverVersion = $matches[1];
-		return $serverVersion;
-	}
-	
-	// Return server URL from current HTTP request
-	function getServerUrl()
-	{
-		$scheme = $this->getScheme();
-		$address = $this->getAddress();
-		$base = $this->getBase();
-		return "$scheme://$address$base/";
-	}
-	
-	// Return scheme from current HTTP request
-	function getScheme()
-	{
-		$scheme = "";
-		if(preg_match("/^HTTP\//", $_SERVER["SERVER_PROTOCOL"]))
-		{
-			$secure = isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"]!="off";
-			$scheme = $secure ? "https" : "http";
-		}
-		return $scheme;
-	}
-	
-	// Return address from current HTTP request
-	function getAddress()
-	{
-		$address = $_SERVER["SERVER_NAME"];
-		$port = $_SERVER["SERVER_PORT"];
-		if($port!=80 && $port!=443) $address .= ":$port";
-		return $address;
-	}
-	
-	// Return base from current HTTP request
-	function getBase()
-	{
-		$base = "";
-		if(preg_match("/^(.*)\/.*\.php$/", $_SERVER["SCRIPT_NAME"], $matches)) $base = $matches[1];
-		return $base;
-	}
-	
-	// Return location from current HTTP request
-	function getLocation($filterStrict = true)
-	{
-		$location = $_SERVER["REQUEST_URI"];
-		$location = rawurldecode(($pos = strposu($location, '?')) ? substru($location, 0, $pos) : $location);
-		if($filterStrict)
-		{
-			$location = $this->normaliseTokens($location, true);
-			$separator = $this->getLocationArgsSeparator();
-			if(preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches))
-			{
-				$_SERVER["LOCATION"] = $location = $matches[1];
-				$_SERVER["LOCATION_ARGS"] = $matches[2];
-				foreach(explode('/', $matches[2]) as $token)
-				{
-					preg_match("/^(.*?)$separator(.*)$/", $token, $matches);
-					if(!empty($matches[1]) && !strempty($matches[2]))
-					{
-						$matches[1] = strreplaceu(array("\x1c", "\x1d", "\x1e"), array('/', ':', '='), $matches[1]);
-						$matches[2] = strreplaceu(array("\x1c", "\x1d", "\x1e"), array('/', ':', '='), $matches[2]);
-						$_REQUEST[$matches[1]] = $matches[2];
-					}
-				}
-			} else {
-				$_SERVER["LOCATION"] = $location;
-				$_SERVER["LOCATION_ARGS"] = "";
-			}
-		}
-		return $location;
-	}
-	
-	// Return location arguments from current HTTP request
-	function getLocationArgs()
-	{
-		return $_SERVER["LOCATION_ARGS"];
-	}
-	
-	// Return location arguments from current HTTP request, modify existing arguments
-	function getLocationArgsNew($arg, $pagination)
-	{
-		$separator = $this->getLocationArgsSeparator();
-		preg_match("/^(.*?):(.*)$/", $arg, $args);
-		foreach(explode('/', $_SERVER["LOCATION_ARGS"]) as $token)
-		{
-			preg_match("/^(.*?)$separator(.*)$/", $token, $matches);
-			if($matches[1]==$args[1]) { $matches[2] = $args[2]; $found = true; }
-			if(!empty($matches[1]) && !strempty($matches[2]))
-			{
-				if(!empty($locationArgs)) $locationArgs .= '/';
-				$locationArgs .= "$matches[1]:$matches[2]";
-			}
-		}
-		if(!$found && !empty($args[1]) && !strempty($args[2]))
-		{
-			if(!empty($locationArgs)) $locationArgs .= '/';
-			$locationArgs .= "$args[1]:$args[2]";
-		}
-		if(!empty($locationArgs))
-		{
-			$locationArgs = $this->normaliseArgs($locationArgs, false, false);
-			if(!$this->isLocationArgsPagination($locationArgs, $pagination)) $locationArgs .= '/';
-		}
-		return $locationArgs;
-	}
-	
-	// Return location arguments from current HTTP request, convert form parameters
-	function getLocationArgsClean($pagination)
-	{
-		foreach(array_merge($_GET, $_POST) as $key=>$value)
-		{
-			if(!empty($key) && !strempty($value))
-			{
-				if(!empty($locationArgs)) $locationArgs .= '/';
-				$key = strreplaceu(array('/', ':', '='), array("\x1c", "\x1d", "\x1e"), $key);
-				$value = strreplaceu(array('/', ':', '='), array("\x1c", "\x1d", "\x1e"), $value);
-				$locationArgs .= "$key:$value";
-			}
-		}
-		if(!empty($locationArgs))
-		{
-			$locationArgs = $this->normaliseArgs($locationArgs, false, false);
-			if(!$this->isLocationArgsPagination($locationArgs, $pagination)) $locationArgs .= '/';
-		}
-		return $locationArgs;
-	}
+class YellowToolbox {
+    
+    // Return server version from current HTTP request
+    public function getServerVersion($shortFormat = false) {
+        $serverVersion = strtoupperu(PHP_SAPI)." ".PHP_OS;
+        if (preg_match("/^(\S+)/", $_SERVER["SERVER_SOFTWARE"], $matches)) $serverVersion = $matches[1]." ".PHP_OS;
+        if ($shortFormat && preg_match("/^(\pL+)/u", $serverVersion, $matches)) $serverVersion = $matches[1];
+        return $serverVersion;
+    }
+    
+    // Return server URL from current HTTP request
+    public function getServerUrl() {
+        $scheme = $this->getScheme();
+        $address = $this->getAddress();
+        $base = $this->getBase();
+        return "$scheme://$address$base/";
+    }
+    
+    // Return scheme from current HTTP request
+    public function getScheme() {
+        $scheme = "";
+        if (preg_match("/^HTTP\//", $_SERVER["SERVER_PROTOCOL"])) {
+            $secure = isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"]!="off";
+            $scheme = $secure ? "https" : "http";
+        }
+        return $scheme;
+    }
+    
+    // Return address from current HTTP request
+    public function getAddress() {
+        $address = $_SERVER["SERVER_NAME"];
+        $port = $_SERVER["SERVER_PORT"];
+        if ($port!=80 && $port!=443) $address .= ":$port";
+        return $address;
+    }
+    
+    // Return base from current HTTP request
+    public function getBase() {
+        $base = "";
+        if (preg_match("/^(.*)\/.*\.php$/", $_SERVER["SCRIPT_NAME"], $matches)) $base = $matches[1];
+        return $base;
+    }
+    
+    // Return location from current HTTP request
+    public function getLocation($filterStrict = true) {
+        $location = $_SERVER["REQUEST_URI"];
+        $location = rawurldecode(($pos = strposu($location, "?")) ? substru($location, 0, $pos) : $location);
+        if ($filterStrict) {
+            $location = $this->normaliseTokens($location, true);
+            $separator = $this->getLocationArgsSeparator();
+            if (preg_match("/^(.*?\/)([^\/]+$separator.*)$/", $location, $matches)) {
+                $_SERVER["LOCATION"] = $location = $matches[1];
+                $_SERVER["LOCATION_ARGS"] = $matches[2];
+                foreach (explode("/", $matches[2]) as $token) {
+                    preg_match("/^(.*?)$separator(.*)$/", $token, $matches);
+                    if (!empty($matches[1]) && !strempty($matches[2])) {
+                        $matches[1] = strreplaceu(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[1]);
+                        $matches[2] = strreplaceu(array("\x1c", "\x1d", "\x1e"), array("/", ":", "="), $matches[2]);
+                        $_REQUEST[$matches[1]] = $matches[2];
+                    }
+                }
+            } else {
+                $_SERVER["LOCATION"] = $location;
+                $_SERVER["LOCATION_ARGS"] = "";
+            }
+        }
+        return $location;
+    }
+    
+    // Return location arguments from current HTTP request
+    public function getLocationArgs() {
+        return $_SERVER["LOCATION_ARGS"];
+    }
+    
+    // Return location arguments from current HTTP request, modify existing arguments
+    public function getLocationArgsNew($arg, $pagination) {
+        $separator = $this->getLocationArgsSeparator();
+        preg_match("/^(.*?):(.*)$/", $arg, $args);
+        foreach (explode("/", $_SERVER["LOCATION_ARGS"]) as $token) {
+            preg_match("/^(.*?)$separator(.*)$/", $token, $matches);
+            if ($matches[1]==$args[1]) {
+                $matches[2] = $args[2];
+                $found = true;
+            }
+            if (!empty($matches[1]) && !strempty($matches[2])) {
+                if (!empty($locationArgs)) $locationArgs .= "/";
+                $locationArgs .= "$matches[1]:$matches[2]";
+            }
+        }
+        if (!$found && !empty($args[1]) && !strempty($args[2])) {
+            if (!empty($locationArgs)) $locationArgs .= "/";
+            $locationArgs .= "$args[1]:$args[2]";
+        }
+        if (!empty($locationArgs)) {
+            $locationArgs = $this->normaliseArgs($locationArgs, false, false);
+            if (!$this->isLocationArgsPagination($locationArgs, $pagination)) $locationArgs .= "/";
+        }
+        return $locationArgs;
+    }
+    
+    // Return location arguments from current HTTP request, convert form parameters
+    public function getLocationArgsClean($pagination) {
+        foreach (array_merge($_GET, $_POST) as $key=>$value) {
+            if (!empty($key) && !strempty($value)) {
+                if (!empty($locationArgs)) $locationArgs .= "/";
+                $key = strreplaceu(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $key);
+                $value = strreplaceu(array("/", ":", "="), array("\x1c", "\x1d", "\x1e"), $value);
+                $locationArgs .= "$key:$value";
+            }
+        }
+        if (!empty($locationArgs)) {
+            $locationArgs = $this->normaliseArgs($locationArgs, false, false);
+            if (!$this->isLocationArgsPagination($locationArgs, $pagination)) $locationArgs .= "/";
+        }
+        return $locationArgs;
+    }
 
-	// Return location arguments separator
-	function getLocationArgsSeparator()
-	{
-		return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ':' : '=';
-	}
-	
-	// Check if there are location arguments in current HTTP request
-	function isLocationArgs($location = "")
-	{
-		$location = empty($location) ? $_SERVER["LOCATION"].$_SERVER["LOCATION_ARGS"] : $location;
-		$separator = $this->getLocationArgsSeparator();
-		return preg_match("/[^\/]+$separator.*$/", $location);
-	}
-	
-	// Check if there are pagination arguments in current HTTP request
-	function isLocationArgsPagination($location, $pagination)
-	{
-		$separator = $this->getLocationArgsSeparator();
-		return preg_match("/^(.*\/)?$pagination$separator.*$/", $location);
-	}
+    // Return location arguments separator
+    public function getLocationArgsSeparator() {
+        return (strtoupperu(substru(PHP_OS, 0, 3))!="WIN") ? ":" : "=";
+    }
+    
+    // Check if there are location arguments in current HTTP request
+    public function isLocationArgs($location = "") {
+        $location = empty($location) ? $_SERVER["LOCATION"].$_SERVER["LOCATION_ARGS"] : $location;
+        $separator = $this->getLocationArgsSeparator();
+        return preg_match("/[^\/]+$separator.*$/", $location);
+    }
+    
+    // Check if there are pagination arguments in current HTTP request
+    public function isLocationArgsPagination($location, $pagination) {
+        $separator = $this->getLocationArgsSeparator();
+        return preg_match("/^(.*\/)?$pagination$separator.*$/", $location);
+    }
 
-	// Check if script location is requested
-	function isRequestSelf()
-	{
-		return substru($_SERVER["REQUEST_URI"], -10, 10)=="yellow.php";
-	}
+    // Check if script location is requested
+    public function isRequestSelf() {
+        return substru($_SERVER["REQUEST_URI"], -10, 10)=="yellow.php";
+    }
 
-	// Check if clean URL is requested
-	function isRequestCleanUrl($location)
-	{
-		return (isset($_GET["clean-url"]) || isset($_POST["clean-url"])) && substru($location, -1, 1)=="/";
-	}
+    // Check if clean URL is requested
+    public function isRequestCleanUrl($location) {
+        return (isset($_GET["clean-url"]) || isset($_POST["clean-url"])) && substru($location, -1, 1)=="/";
+    }
 
-	// Check if unmodified since last HTTP request
-	function isRequestNotModified($lastModifiedFormatted)
-	{
-		return isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && $_SERVER["HTTP_IF_MODIFIED_SINCE"]==$lastModifiedFormatted;
-	}
-	
-	// Normalise path or location, take care of relative path tokens
-	function normaliseTokens($text, $prependSlash = false)
-	{
-		$textFiltered = "";
-		if($prependSlash && $text[0]!='/') $textFiltered .= '/';
-		for($pos=0; $pos<strlenb($text); ++$pos)
-		{
-			if($text[$pos]=='/' || $pos==0)
-			{
-				if($text[$pos+1]=='/') continue;
-				if($text[$pos+1]=='.')
-				{
-					$posNew = $pos+1; while($text[$posNew]=='.') ++$posNew;
-					if($text[$posNew]=='/' || $text[$posNew]=='')
-					{
-						$pos = $posNew-1;
-						continue;
-					}
-				}
-			}
-			$textFiltered .= $text[$pos];
-		}
-		return $textFiltered;
-	}
-	
-	// Normalise location arguments
-	function normaliseArgs($text, $appendSlash = true, $filterStrict = true)
-	{
-		if($appendSlash) $text .= '/';
-		if($filterStrict) $text = strreplaceu(' ', '-', strtoloweru($text));
-		$text = strreplaceu(':', $this->getLocationArgsSeparator(), $text);
-		return strreplaceu(array('%2F','%3A','%3D'), array('/',':','='), rawurlencode($text));
-	}
-	
-	// Normalise text into UTF-8 NFC
-	function normaliseUnicode($text)
-	{
-		if(PHP_OS=="Darwin" && !mb_check_encoding($text, "ASCII"))
-		{
-			$utf8nfc = preg_match("//u", $text) && !preg_match('/[^\x00-\x{2FF}]/u', $text);
-			if(!$utf8nfc) $text = iconv("UTF-8-MAC", "UTF-8", $text);
-		}
-		return $text;
-	}
-	
-	// Return timezone
-	function getTimezone()
-	{
-		$timezone = @date_default_timezone_get();
-		if(PHP_OS=="Darwin" && $timezone=="UTC")
-		{
-			if(preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1];
-		}
-		return $timezone;
-	}
-	
-	// Return human readable HTTP server status
-	function getHttpStatusFormatted($statusCode, $shortFormat = false)
-	{
-		switch($statusCode)
-		{
-			case 0:		$text = "No data"; break;
-			case 200:	$text = "OK"; break;
-			case 301:	$text = "Moved permanently"; break;
-			case 302:	$text = "Moved temporarily"; break;
-			case 303:	$text = "Reload please"; break;
-			case 304:	$text = "Not modified"; break;
-			case 400:	$text = "Bad request"; break;
-			case 403:	$text = "Forbidden"; break;
-			case 404:	$text = "Not found"; break;
-			case 430:	$text = "Login failed"; break;
-			case 434:	$text = "Not existing"; break;
-			case 500:	$text = "Server error"; break;
-			case 503:	$text = "Service unavailable"; break;
-			default:	$text = "Error $statusCode";
-		}
-		$serverProtocol = $_SERVER["SERVER_PROTOCOL"];
-		if(!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1";
-		return $shortFormat ? $text : "$serverProtocol $statusCode $text";
-	}
-							  
-	// Return human readable HTTP date
-	function getHttpDateFormatted($timestamp)
-	{
-		return gmdate("D, d M Y H:i:s", $timestamp)." GMT";
-	}
-				
-	// Return MIME content type
-	function getMimeContentType($fileName)
-	{
-		$contentType = "";
-		$contentTypes = array(
-			"css" => "text/css",
-			"gif" => "image/gif",
-			"html" => "text/html; charset=utf-8",
-			"ico" => "image/x-icon",
-			"js" => "application/javascript",
-			"json" => "application/json",
-			"jpg" => "image/jpeg",
-			"png" => "image/png",
-			"svg" => "image/svg+xml",
-			"txt" => "text/plain",
-			"woff" => "application/font-woff",
-			"woff2" => "application/font-woff2",
-			"xml" => "text/xml; charset=utf-8");
-		$fileType = $this->getFileType($fileName);
-		if(empty($fileType))
-		{
-			$contentType = $contentTypes["html"];
-		} else if(array_key_exists($fileType, $contentTypes)) {
-			$contentType = $contentTypes[$fileType];
-		}
-		return $contentType;
-	}
-	
-	// Return file type
-	function getFileType($fileName)
-	{
-		return strtoloweru(($pos = strrposu($fileName, '.')) ? substru($fileName, $pos+1) : "");
-	}
-	
-	// Return file group
-	function getFileGroup($fileName, $path)
-	{
-		preg_match("#^$path(.+?)\/#", $fileName, $matches);
-		return strtoloweru($matches[1]);
-	}
-	
-	// Return number of bytes
-	function getNumberBytes($string)
-	{
-		$bytes = intval($string);
-		switch(strtoupperu(substru($string, -1)))
-		{
-			case 'G': $bytes *= 1024*1024*1024; break;
-			case 'M': $bytes *= 1024*1024; break;
-			case 'K': $bytes *= 1024; break;
-		}
-		return $bytes;
-	}
-	
-	// Return files and directories
-	function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true)
-	{
-		$entries = array();
-		$dirHandle = @opendir($path);
-		if($dirHandle)
-		{
-			$path = rtrim($path, '/');
-			while(($entry = readdir($dirHandle))!==false)
-			{
-				if(substru($entry, 0, 1)==".") continue;
-				$entry = $this->normaliseUnicode($entry);
-				if(preg_match($regex, $entry))
-				{
-					if($directories)
-					{
-						if(is_dir("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry);
-					} else {
-						if(is_file("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry);
-					}
-				}
-			}
-			if($sort) natcasesort($entries);
-			closedir($dirHandle);
-		}
-		return $entries;
-	}
-	
-	// Return files and directories recursively
-	function getDirectoryEntriesRecursive($path, $regex = "/.*/", $sort = true, $directories = true, $levelMax = 0)
-	{
-		--$levelMax;
-		$entries = $this->getDirectoryEntries($path, $regex, $sort, $directories);
-		if($levelMax!=0)
-		{
-			foreach($this->getDirectoryEntries($path, "/.*/", $sort, true) as $entry)
-			{
-				$entries = array_merge($entries, $this->getDirectoryEntriesRecursive($entry, $regex, $sort, $directories, $levelMax));
-			}
-		}
-		return $entries;
-	}
-	
-	// Read file, empty string if not found
-	function readFile($fileName, $sizeMax = 0)
-	{
-		$fileData = "";
-		$fileHandle = @fopen($fileName, "rb");
-		if($fileHandle)
-		{
-			clearstatcache(true, $fileName);
-			$fileSize = $sizeMax ? $sizeMax : filesize($fileName);
-			if($fileSize) $fileData = fread($fileHandle, $fileSize);
-			fclose($fileHandle);
-		}
-		return $fileData;
-	}
-	
-	// Create file
-	function createFile($fileName, $fileData, $mkdir = false)
-	{
-		$ok = false;
-		if($mkdir)
-		{
-			$path = dirname($fileName);
-			if(!empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
-		}
-		$fileHandle = @fopen($fileName, "wb");
-		if($fileHandle)
-		{
-			clearstatcache(true, $fileName);
-			if(flock($fileHandle, LOCK_EX))
-			{
-				ftruncate($fileHandle, 0);
-				fwrite($fileHandle, $fileData);
-				flock($fileHandle, LOCK_UN);
-			}
-			fclose($fileHandle);
-			$ok = true;
-		}
-		return $ok;
-	}
-	
-	// Copy file
-	function copyFile($fileNameSource, $fileNameDestination, $mkdir = false)
-	{
-		clearstatcache();
-		if($mkdir)
-		{
-			$path = dirname($fileNameDestination);
-			if(!empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
-		}
-		return @copy($fileNameSource, $fileNameDestination);
-	}
-	
-	// Rename file
-	function renameFile($fileNameSource, $fileNameDestination, $mkdir = false)
-	{
-		clearstatcache();
-		if($mkdir)
-		{
-			$path = dirname($fileNameDestination);
-			if(!empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
-		}
-		return @rename($fileNameSource, $fileNameDestination);
-	}
-	
-	// Delete file
-	function deleteFile($fileName, $pathTrash = "")
-	{
-		clearstatcache();
-		if(empty($pathTrash))
-		{
-			$ok = @unlink($fileName);
-		} else {
-			if(!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true);
-			$fileNameDestination = $pathTrash;
-			$fileNameDestination .= pathinfo($fileName, PATHINFO_FILENAME);
-			$fileNameDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s", filemtime($fileName)));
-			$fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION);
-			$ok = @rename($fileName, $fileNameDestination);
-		}
-		return $ok;
-	}
-	
-	// Delete directory
-	function deleteDirectory($path, $pathTrash = "")
-	{
-		clearstatcache();
-		if(empty($pathTrash))
-		{
-			$iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
-			$files = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST);
-			foreach($files as $file)
-			{
-				if($file->isDir())
-				{
-					@rmdir($file->getRealPath());
-				} else {
-					@unlink($file->getRealPath());
-				}
-			}
-			$ok = @rmdir($path);
-		} else {
-			if(!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true);
-			$pathDestination = $pathTrash;
-			$pathDestination .= basename($path);
-			$pathDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s", filemtime($path)));
-			$ok = @rename($path, $pathDestination);
-		}
-		return $ok;
-	}
-	
-	// Set file modification date, Unix time
-	function modifyFile($fileName, $modified)
-	{
-		clearstatcache(true, $fileName);
-		return @touch($fileName, $modified);
-	}
-	
-	// Return file modification date, Unix time
-	function getFileModified($fileName)
-	{
-		return is_file($fileName) ? filemtime($fileName) : 0;
-	}
-	
-	// Return lines from text string, including newline
-	function getTextLines($text)
-	{
-		$lines = preg_split("/\n/", $text);
-		foreach($lines as &$line) $line = $line."\n";
-		if(strempty($text) || substru($text, -1, 1)=="\n") array_pop($lines);
-		return $lines;
-	}
-	
-	// Return arguments from text string, space separated
-	function getTextArgs($text, $optional = "-")
-	{
-		$text = preg_replace("/\s+/s", " ", trim($text));
-		$tokens = str_getcsv($text, ' ', '"');
-		foreach($tokens as $key=>$value) if($value==$optional) $tokens[$key] = "";
-		return $tokens;
-	}
+    // Check if unmodified since last HTTP request
+    public function isRequestNotModified($lastModifiedFormatted) {
+        return isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && $_SERVER["HTTP_IF_MODIFIED_SINCE"]==$lastModifiedFormatted;
+    }
+    
+    // Normalise path or location, take care of relative path tokens
+    public function normaliseTokens($text, $prependSlash = false) {
+        $textFiltered = "";
+        if ($prependSlash && $text[0]!="/") $textFiltered .= "/";
+        for ($pos=0; $pos<strlenb($text); ++$pos) {
+            if ($text[$pos]=="/" || $pos==0) {
+                if ($text[$pos+1]=="/") continue;
+                if ($text[$pos+1]==".") {
+                    $posNew = $pos+1;
+                    while ($text[$posNew]==".") {
+                        ++$posNew;
+                    }
+                    if ($text[$posNew]=="/" || $text[$posNew]=="") {
+                        $pos = $posNew-1;
+                        continue;
+                    }
+                }
+            }
+            $textFiltered .= $text[$pos];
+        }
+        return $textFiltered;
+    }
+    
+    // Normalise location arguments
+    public function normaliseArgs($text, $appendSlash = true, $filterStrict = true) {
+        if ($appendSlash) $text .= "/";
+        if ($filterStrict) $text = strreplaceu(" ", "-", strtoloweru($text));
+        $text = strreplaceu(":", $this->getLocationArgsSeparator(), $text);
+        return strreplaceu(array("%2F","%3A","%3D"), array("/",":","="), rawurlencode($text));
+    }
+    
+    // Normalise text into UTF-8 NFC
+    public function normaliseUnicode($text) {
+        if (PHP_OS=="Darwin" && !mb_check_encoding($text, "ASCII")) {
+            $utf8nfc = preg_match("//u", $text) && !preg_match("/[^\\x00-\\x{2FF}]/u", $text);
+            if (!$utf8nfc) $text = iconv("UTF-8-MAC", "UTF-8", $text);
+        }
+        return $text;
+    }
+    
+    // Return timezone
+    public function getTimezone() {
+        $timezone = @date_default_timezone_get();
+        if (PHP_OS=="Darwin" && $timezone=="UTC") {
+            if (preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1];
+        }
+        return $timezone;
+    }
+    
+    // Return human readable HTTP server status
+    public function getHttpStatusFormatted($statusCode, $shortFormat = false) {
+        switch ($statusCode) {
+            case 0:     $text = "No data"; break;
+            case 200:   $text = "OK"; break;
+            case 301:   $text = "Moved permanently"; break;
+            case 302:   $text = "Moved temporarily"; break;
+            case 303:   $text = "Reload please"; break;
+            case 304:   $text = "Not modified"; break;
+            case 400:   $text = "Bad request"; break;
+            case 403:   $text = "Forbidden"; break;
+            case 404:   $text = "Not found"; break;
+            case 430:   $text = "Login failed"; break;
+            case 434:   $text = "Not existing"; break;
+            case 500:   $text = "Server error"; break;
+            case 503:   $text = "Service unavailable"; break;
+            default:    $text = "Error $statusCode";
+        }
+        $serverProtocol = $_SERVER["SERVER_PROTOCOL"];
+        if (!preg_match("/^HTTP\//", $serverProtocol)) $serverProtocol = "HTTP/1.1";
+        return $shortFormat ? $text : "$serverProtocol $statusCode $text";
+    }
+                              
+    // Return human readable HTTP date
+    public function getHttpDateFormatted($timestamp) {
+        return gmdate("D, d M Y H:i:s", $timestamp)." GMT";
+    }
+                
+    // Return MIME content type
+    public function getMimeContentType($fileName) {
+        $contentType = "";
+        $contentTypes = array(
+            "css" => "text/css",
+            "gif" => "image/gif",
+            "html" => "text/html; charset=utf-8",
+            "ico" => "image/x-icon",
+            "js" => "application/javascript",
+            "json" => "application/json",
+            "jpg" => "image/jpeg",
+            "png" => "image/png",
+            "svg" => "image/svg+xml",
+            "txt" => "text/plain",
+            "woff" => "application/font-woff",
+            "woff2" => "application/font-woff2",
+            "xml" => "text/xml; charset=utf-8");
+        $fileType = $this->getFileType($fileName);
+        if (empty($fileType)) {
+            $contentType = $contentTypes["html"];
+        } elseif (array_key_exists($fileType, $contentTypes)) {
+            $contentType = $contentTypes[$fileType];
+        }
+        return $contentType;
+    }
+    
+    // Return file type
+    public function getFileType($fileName) {
+        return strtoloweru(($pos = strrposu($fileName, ".")) ? substru($fileName, $pos+1) : "");
+    }
+    
+    // Return file group
+    public function getFileGroup($fileName, $path) {
+        preg_match("#^$path(.+?)\/#", $fileName, $matches);
+        return strtoloweru($matches[1]);
+    }
+    
+    // Return number of bytes
+    public function getNumberBytes($string) {
+        $bytes = intval($string);
+        switch (strtoupperu(substru($string, -1))) {
+            case "G": $bytes *= 1024*1024*1024; break;
+            case "M": $bytes *= 1024*1024; break;
+            case "K": $bytes *= 1024; break;
+        }
+        return $bytes;
+    }
+    
+    // Return files and directories
+    public function getDirectoryEntries($path, $regex = "/.*/", $sort = true, $directories = true, $includePath = true) {
+        $entries = array();
+        $dirHandle = @opendir($path);
+        if ($dirHandle) {
+            $path = rtrim($path, "/");
+            while (($entry = readdir($dirHandle))!==false) {
+                if (substru($entry, 0, 1)==".") continue;
+                $entry = $this->normaliseUnicode($entry);
+                if (preg_match($regex, $entry)) {
+                    if ($directories) {
+                        if (is_dir("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry);
+                    } else {
+                        if (is_file("$path/$entry")) array_push($entries, $includePath ? "$path/$entry" : $entry);
+                    }
+                }
+            }
+            if ($sort) natcasesort($entries);
+            closedir($dirHandle);
+        }
+        return $entries;
+    }
+    
+    // Return files and directories recursively
+    public function getDirectoryEntriesRecursive($path, $regex = "/.*/", $sort = true, $directories = true, $levelMax = 0) {
+        --$levelMax;
+        $entries = $this->getDirectoryEntries($path, $regex, $sort, $directories);
+        if ($levelMax!=0) {
+            foreach ($this->getDirectoryEntries($path, "/.*/", $sort, true) as $entry) {
+                $entries = array_merge($entries, $this->getDirectoryEntriesRecursive($entry, $regex, $sort, $directories, $levelMax));
+            }
+        }
+        return $entries;
+    }
+    
+    // Read file, empty string if not found
+    public function readFile($fileName, $sizeMax = 0) {
+        $fileData = "";
+        $fileHandle = @fopen($fileName, "rb");
+        if ($fileHandle) {
+            clearstatcache(true, $fileName);
+            $fileSize = $sizeMax ? $sizeMax : filesize($fileName);
+            if ($fileSize) $fileData = fread($fileHandle, $fileSize);
+            fclose($fileHandle);
+        }
+        return $fileData;
+    }
+    
+    // Create file
+    public function createFile($fileName, $fileData, $mkdir = false) {
+        $ok = false;
+        if ($mkdir) {
+            $path = dirname($fileName);
+            if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
+        }
+        $fileHandle = @fopen($fileName, "wb");
+        if ($fileHandle) {
+            clearstatcache(true, $fileName);
+            if (flock($fileHandle, LOCK_EX)) {
+                ftruncate($fileHandle, 0);
+                fwrite($fileHandle, $fileData);
+                flock($fileHandle, LOCK_UN);
+            }
+            fclose($fileHandle);
+            $ok = true;
+        }
+        return $ok;
+    }
+    
+    // Copy file
+    public function copyFile($fileNameSource, $fileNameDestination, $mkdir = false) {
+        clearstatcache();
+        if ($mkdir) {
+            $path = dirname($fileNameDestination);
+            if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
+        }
+        return @copy($fileNameSource, $fileNameDestination);
+    }
+    
+    // Rename file
+    public function renameFile($fileNameSource, $fileNameDestination, $mkdir = false) {
+        clearstatcache();
+        if ($mkdir) {
+            $path = dirname($fileNameDestination);
+            if (!empty($path) && !is_dir($path)) @mkdir($path, 0777, true);
+        }
+        return @rename($fileNameSource, $fileNameDestination);
+    }
+    
+    // Delete file
+    public function deleteFile($fileName, $pathTrash = "") {
+        clearstatcache();
+        if (empty($pathTrash)) {
+            $ok = @unlink($fileName);
+        } else {
+            if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true);
+            $fileNameDestination = $pathTrash;
+            $fileNameDestination .= pathinfo($fileName, PATHINFO_FILENAME);
+            $fileNameDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s", filemtime($fileName)));
+            $fileNameDestination .= ".".pathinfo($fileName, PATHINFO_EXTENSION);
+            $ok = @rename($fileName, $fileNameDestination);
+        }
+        return $ok;
+    }
+    
+    // Delete directory
+    public function deleteDirectory($path, $pathTrash = "") {
+        clearstatcache();
+        if (empty($pathTrash)) {
+            $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
+            $files = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST);
+            foreach ($files as $file) {
+                if ($file->isDir()) {
+                    @rmdir($file->getRealPath());
+                } else {
+                    @unlink($file->getRealPath());
+                }
+            }
+            $ok = @rmdir($path);
+        } else {
+            if (!is_dir($pathTrash)) @mkdir($pathTrash, 0777, true);
+            $pathDestination = $pathTrash;
+            $pathDestination .= basename($path);
+            $pathDestination .= "-".str_replace(array(" ", ":"), "-", date("Y-m-d H:i:s", filemtime($path)));
+            $ok = @rename($path, $pathDestination);
+        }
+        return $ok;
+    }
+    
+    // Set file modification date, Unix time
+    public function modifyFile($fileName, $modified) {
+        clearstatcache(true, $fileName);
+        return @touch($fileName, $modified);
+    }
+    
+    // Return file modification date, Unix time
+    public function getFileModified($fileName) {
+        return is_file($fileName) ? filemtime($fileName) : 0;
+    }
+    
+    // Return lines from text string, including newline
+    public function getTextLines($text) {
+        $lines = preg_split("/\n/", $text);
+        foreach ($lines as &$line) {
+            $line = $line."\n";
+        }
+        if (strempty($text) || substru($text, -1, 1)=="\n") array_pop($lines);
+        return $lines;
+    }
+    
+    // Return arguments from text string, space separated
+    public function getTextArgs($text, $optional = "-") {
+        $text = preg_replace("/\s+/s", " ", trim($text));
+        $tokens = str_getcsv($text, " ", "\"");
+        foreach ($tokens as $key=>$value) {
+            if ($value==$optional) $tokens[$key] = "";
+        }
+        return $tokens;
+    }
 
-	// Return number of words in text string
-	function getTextWords($text)
-	{
-		$text = preg_replace("/([\p{Han}\p{Hiragana}\p{Katakana}]{3})/u", "$1 ", $text);
-		$text = preg_replace("/(\pL|\p{N})/u", "x", $text);
-		return str_word_count($text);
-	}
-	
-	// Create description from text string
-	function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "")
-	{
-		if(preg_match("/^<h1>.*?<\/h1>(.*)$/si", $text, $matches)) $text = $matches[1];
-		if($lengthMax==0) $lengthMax = strlenu($text);
-		if($removeHtml)
-		{
-			while(true)
-			{
-				$elementFound = preg_match("/<\s*?([\/!]?\w*)(.*?)\s*?\>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes);
-				$element = $matches[0][0];
-				$elementName = $matches[1][0];
-				$elementText = $matches[2][0];
-				$elementOffsetBytes = $elementFound ? $matches[0][1] : strlenb($text);
-				$string = html_entity_decode(substrb($text, $offsetBytes, $elementOffsetBytes - $offsetBytes), ENT_QUOTES, "UTF-8");
-				if(preg_match("/^(blockquote|br|div|h\d|hr|li|ol|p|pre|ul)/i", $elementName)) $string .= ' ';
-				if(preg_match("/^\/(code|pre)/i", $elementName)) $string = preg_replace("/^(\d+\n){2,}$/", "", $string);
-				$string = preg_replace("/\s+/s", " ", $string);
-				if(substru($string, 0, 1)==" " && (empty($output) || substru($output, -1)==' ')) $string = substru($string, 1);
-				$length = strlenu($string);
-				$output .= substru($string, 0, $length<$lengthMax ? $length : $lengthMax-1);
-				$lengthMax -= $length;
-				if(!empty($element) && $element==$endMarker) { $lengthMax = 0; $endMarkerFound = true; }
-				if($lengthMax<=0 || !$elementFound) break;
-				$offsetBytes = $elementOffsetBytes + strlenb($element);
-			}
-			$output = rtrim($output);
-			if($lengthMax<=0) $output .= $endMarkerFound ? $endMarkerText : "…";
-		} else {
-			$elementsOpen = array();
-			while(true)
-			{
-				$elementFound = preg_match("/&.*?\;|<\s*?([\/!]?\w*)(.*?)\s*?\>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes);
-				$element = $matches[0][0];
-				$elementName = $matches[1][0];
-				$elementText = $matches[2][0];
-				$elementOffsetBytes = $elementFound ? $matches[0][1] : strlenb($text);
-				$string = substrb($text, $offsetBytes, $elementOffsetBytes - $offsetBytes);
-				$length = strlenu($string);
-				$output .= substru($string, 0, $length<$lengthMax ? $length : $lengthMax-1);
-				$lengthMax -= $length + ($element[0]=='&' ? 1 : 0);
-				if(!empty($element) && $element==$endMarker) { $lengthMax = 0; $endMarkerFound = true; }
-				if($lengthMax<=0 || !$elementFound) break;
-				if(!empty($elementName) && substru($elementText, -1)!='/' &&
-				   !preg_match("/^(area|br|col|hr|img|input|col|param|!)/i", $elementName))
-				{
-					if($elementName[0]!='/')
-					{
-						array_push($elementsOpen, $elementName);
-					} else {
-						array_pop($elementsOpen);
-					}
-				}
-				$output .= $element;
-				$offsetBytes = $elementOffsetBytes + strlenb($element);
-			}
-			$output = rtrim($output);
-			for($i=count($elementsOpen)-1; $i>=0; --$i)
-			{
-				if(!preg_match("/^(dl|ol|ul|table|tbody|thead|tfoot|tr)/i", $elementsOpen[$i])) break;
-				$output .= "</".$elementsOpen[$i].">";
-			}
-			if($lengthMax<=0) $output .= $endMarkerFound ? $endMarkerText : "…";
-			for(; $i>=0; --$i) $output .= "</".$elementsOpen[$i].">";
-		}
-		return $output;
-	}
-	
-	// Create keywords from text string
-	function createTextKeywords($text, $keywordsMax = 0)
-	{
-		$tokens = array_unique(preg_split("/[,\s\(\)\+\-]/", strtoloweru($text)));
-		foreach($tokens as $key=>$value) if(strlenu($value)<3) unset($tokens[$key]);
-		if($keywordsMax) $tokens = array_slice($tokens, 0, $keywordsMax);
-		return implode(", ", $tokens);
-	}
-	
-	// Create title from text string
-	function createTextTitle($text)
-	{
-		if(preg_match("/^.*\/([\w\-]+)/", $text, $matches)) $text = strreplaceu('-', ' ', ucfirst($matches[1]));
-		return $text;
-	}
+    // Return number of words in text string
+    public function getTextWords($text) {
+        $text = preg_replace("/([\p{Han}\p{Hiragana}\p{Katakana}]{3})/u", "$1 ", $text);
+        $text = preg_replace("/(\pL|\p{N})/u", "x", $text);
+        return str_word_count($text);
+    }
+    
+    // Create description from text string
+    public function createTextDescription($text, $lengthMax = 0, $removeHtml = true, $endMarker = "", $endMarkerText = "") {
+        if (preg_match("/^<h1>.*?<\/h1>(.*)$/si", $text, $matches)) $text = $matches[1];
+        if ($lengthMax==0) $lengthMax = strlenu($text);
+        if ($removeHtml) {
+            while (true) {
+                $elementFound = preg_match("/<\s*?([\/!]?\w*)(.*?)\s*?\>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes);
+                $element = $matches[0][0];
+                $elementName = $matches[1][0];
+                $elementText = $matches[2][0];
+                $elementOffsetBytes = $elementFound ? $matches[0][1] : strlenb($text);
+                $string = html_entity_decode(substrb($text, $offsetBytes, $elementOffsetBytes - $offsetBytes), ENT_QUOTES, "UTF-8");
+                if (preg_match("/^(blockquote|br|div|h\d|hr|li|ol|p|pre|ul)/i", $elementName)) $string .= " ";
+                if (preg_match("/^\/(code|pre)/i", $elementName)) $string = preg_replace("/^(\d+\n){2,}$/", "", $string);
+                $string = preg_replace("/\s+/s", " ", $string);
+                if (substru($string, 0, 1)==" " && (empty($output) || substru($output, -1)==" ")) $string = substru($string, 1);
+                $length = strlenu($string);
+                $output .= substru($string, 0, $length<$lengthMax ? $length : $lengthMax-1);
+                $lengthMax -= $length;
+                if (!empty($element) && $element==$endMarker) {
+                    $lengthMax = 0;
+                    $endMarkerFound = true;
+                }
+                if ($lengthMax<=0 || !$elementFound) break;
+                $offsetBytes = $elementOffsetBytes + strlenb($element);
+            }
+            $output = rtrim($output);
+            if ($lengthMax<=0) $output .= $endMarkerFound ? $endMarkerText : "…";
+        } else {
+            $elementsOpen = array();
+            while (true) {
+                $elementFound = preg_match("/&.*?\;|<\s*?([\/!]?\w*)(.*?)\s*?\>/s", $text, $matches, PREG_OFFSET_CAPTURE, $offsetBytes);
+                $element = $matches[0][0];
+                $elementName = $matches[1][0];
+                $elementText = $matches[2][0];
+                $elementOffsetBytes = $elementFound ? $matches[0][1] : strlenb($text);
+                $string = substrb($text, $offsetBytes, $elementOffsetBytes - $offsetBytes);
+                $length = strlenu($string);
+                $output .= substru($string, 0, $length<$lengthMax ? $length : $lengthMax-1);
+                $lengthMax -= $length + ($element[0]=="&" ? 1 : 0);
+                if (!empty($element) && $element==$endMarker) {
+                    $lengthMax = 0;
+                    $endMarkerFound = true;
+                }
+                if ($lengthMax<=0 || !$elementFound) break;
+                if (!empty($elementName) && substru($elementText, -1)!="/" &&
+                   !preg_match("/^(area|br|col|hr|img|input|col|param|!)/i", $elementName)) {
+                    if ($elementName[0]!="/") {
+                        array_push($elementsOpen, $elementName);
+                    } else {
+                        array_pop($elementsOpen);
+                    }
+                }
+                $output .= $element;
+                $offsetBytes = $elementOffsetBytes + strlenb($element);
+            }
+            $output = rtrim($output);
+            for ($i=count($elementsOpen)-1; $i>=0; --$i) {
+                if (!preg_match("/^(dl|ol|ul|table|tbody|thead|tfoot|tr)/i", $elementsOpen[$i])) break;
+                $output .= "</".$elementsOpen[$i].">";
+            }
+            if ($lengthMax<=0) $output .= $endMarkerFound ? $endMarkerText : "…";
+            for (; $i>=0; --$i) {
+                $output .= "</".$elementsOpen[$i].">";
+            }
+        }
+        return $output;
+    }
+    
+    // Create keywords from text string
+    public function createTextKeywords($text, $keywordsMax = 0) {
+        $tokens = array_unique(preg_split("/[,\s\(\)\+\-]/", strtoloweru($text)));
+        foreach ($tokens as $key=>$value) {
+            if (strlenu($value)<3) unset($tokens[$key]);
+        }
+        if ($keywordsMax) $tokens = array_slice($tokens, 0, $keywordsMax);
+        return implode(", ", $tokens);
+    }
+    
+    // Create title from text string
+    public function createTextTitle($text) {
+        if (preg_match("/^.*\/([\w\-]+)/", $text, $matches)) $text = strreplaceu("-", " ", ucfirst($matches[1]));
+        return $text;
+    }
 
-	// Create random text for cryptography
-	function createSalt($length, $bcryptFormat = false)
-	{
-		$dataBuffer = $salt = "";
-		$dataBufferSize = $bcryptFormat ? intval(ceil($length/4) * 3) : intval(ceil($length/2));
-		if(empty($dataBuffer) && function_exists("random_bytes"))
-		{
-			$dataBuffer = @random_bytes($dataBufferSize);
-		}
-		if(empty($dataBuffer) && function_exists("mcrypt_create_iv"))
-		{
-			$dataBuffer = @mcrypt_create_iv($dataBufferSize, MCRYPT_DEV_URANDOM);
-		}
-		if(empty($dataBuffer) && function_exists("openssl_random_pseudo_bytes"))
-		{
-			$dataBuffer = @openssl_random_pseudo_bytes($dataBufferSize);
-		}
-		if(strlenb($dataBuffer)==$dataBufferSize)
-		{
-			if($bcryptFormat)
-			{
-				$salt = substrb(base64_encode($dataBuffer), 0, $length);
-				$base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
-				$bcrypt64Chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
-				$salt = strtr($salt, $base64Chars, $bcrypt64Chars);
-			} else {
-				$salt = substrb(bin2hex($dataBuffer), 0, $length);
-			}
-		}
-		return $salt;
-	}
-	
-	// Create hash with random salt, bcrypt or sha256
-	function createHash($text, $algorithm, $cost = 0)
-	{
-		$hash = "";
-		switch($algorithm)
-		{
-			case "bcrypt":	$prefix = sprintf("$2y$%02d$", $cost);
-							$salt = $this->createSalt(22, true);
-							$hash = crypt($text, $prefix.$salt);
-							if(empty($salt) || strlenb($hash)!=60) $hash = "";
-							break;
-			case "sha256":	$prefix = "$5y$";
-							$salt = $this->createSalt(32);
-							$hash = "$prefix$salt".hash("sha256", $salt.$text);
-							if(empty($salt) || strlenb($hash)!=100) $hash = "";
-							break;
-		}
-		return $hash;
-	}
-	
-	// Verify that text matches hash
-	function verifyHash($text, $algorithm, $hash)
-	{
-		$hashCalculated = "";
-		switch($algorithm)
-		{
-			case "bcrypt":	if(substrb($hash, 0, 4)=="$2y$" || substrb($hash, 0, 4)=="$2a$")
-							{
-								$hashCalculated = crypt($text, $hash);
-							}
-							break;
-			case "sha256":	if(substrb($hash, 0, 4)=="$5y$")
-							{
-								$prefix = "$5y$";
-								$salt = substrb($hash, 4, 32);
-								$hashCalculated = "$prefix$salt".hash("sha256", $salt.$text);
-							}
-							break;
-		}
-		return $this->verifyToken($hashCalculated, $hash);
-	}
-	
-	// Verify that token is not empty and identical, timing attack safe text string comparison
-	function verifyToken($tokenExpected, $tokenReceived)
-	{
-		$ok = false;
-		$lengthExpected = strlenb($tokenExpected);
-		$lengthReceived = strlenb($tokenReceived);
-		if($lengthExpected!=0 && $lengthReceived!=0)
-		{
-			$ok = $lengthExpected==$lengthReceived;
-			for($i=0; $i<$lengthReceived; ++$i) $ok &= $tokenExpected[$i<$lengthExpected ? $i : 0]==$tokenReceived[$i];
-		}
-		return $ok;
-	}
-	
-	// Return meta data from raw data
-	function getMetaData($rawData, $key)
-	{
-		$value = "";
-		if(preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts))
-		{
-			$key = lcfirst($key);
-			foreach($this->getTextLines($parts[2]) as $line)
-			{
-				preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-				if(lcfirst($matches[1])==$key && !strempty($matches[2])) { $value = $matches[2]; break; }
-			}
-		}
-		return $value;
-	}
-	
-	// Set meta data in raw data
-	function setMetaData($rawData, $key, $value)
-	{
-		if(preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts))
-		{
-			$key = lcfirst($key);
-			foreach($this->getTextLines($parts[2]) as $line)
-			{
-				preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-				if(lcfirst($matches[1])==$key)
-				{
-					$rawDataNew .= "$matches[1]: $value\n";
-					$found = true;
-				} else {
-					$rawDataNew .= $line;
-				}
-			}
-			if(!$found) $rawDataNew .= ucfirst($key).": $value\n";
-			$rawDataNew = $parts[1]."---\n".$rawDataNew."---\n".$parts[3];
-		} else {
-			$rawDataNew = $rawData;
-		}
-		return $rawDataNew;
-	}
-	
-	// Detect web browser language
-	function detectBrowserLanguage($languages, $languageDefault)
-	{
-		$language = $languageDefault;
-		if(isset($_SERVER["HTTP_ACCEPT_LANGUAGE"]))
-		{
-			foreach(preg_split("/\s*,\s*/", $_SERVER["HTTP_ACCEPT_LANGUAGE"]) as $string)
-			{
-				$tokens = explode(';', $string);
-				if(in_array($tokens[0], $languages)) { $language = $tokens[0]; break; }
-			}
-		}
-		return $language;
-	}
-	
-	// Detect image dimensions and type for gif/jpg/png/svg
-	function detectImageInfo($fileName)
-	{
-		$width = $height = 0;
-		$type = "";
-		$fileHandle = @fopen($fileName, "rb");
-		if($fileHandle)
-		{
-			if(substru(strtoloweru($fileName), -3)=="gif")
-			{
-				$dataSignature = fread($fileHandle, 6);
-				$dataHeader = fread($fileHandle, 7);
-				if(!feof($fileHandle) && ($dataSignature=="GIF87a" || $dataSignature=="GIF89a"))
-				{
-					$width = (ord($dataHeader[1])<<8) + ord($dataHeader[0]);
-					$height = (ord($dataHeader[3])<<8) + ord($dataHeader[2]);
-					$type = "gif";
-				}
-			} else if(substru(strtoloweru($fileName), -3)=="jpg") {
-				$dataBufferSizeMax = filesize($fileName);
-				$dataBufferSize = min($dataBufferSizeMax, 4096);
-				if($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize);
-				$dataSignature = substrb($dataBuffer, 0, 4);
-				if(!feof($fileHandle) && ($dataSignature=="\xff\xd8\xff\xe0" || $dataSignature=="\xff\xd8\xff\xe1"))
-				{
-					for($pos=2; $pos+8<$dataBufferSize; $pos+=$length)
-					{
-						if($dataBuffer[$pos]!="\xff") break;
-						if($dataBuffer[$pos+1]=="\xc0" || $dataBuffer[$pos+1]=="\xc2")
-						{
-							$width = (ord($dataBuffer[$pos+7])<<8) + ord($dataBuffer[$pos+8]);
-							$height = (ord($dataBuffer[$pos+5])<<8) + ord($dataBuffer[$pos+6]);
-							$type = "jpg";
-							break;
-						}
-						$length = (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]) + 2;
-						while($pos+$length+8>=$dataBufferSize)
-						{
-							if($dataBufferSize==$dataBufferSizeMax) break;
-							$dataBufferDiff = min($dataBufferSizeMax, $dataBufferSize*2) - $dataBufferSize;
-							$dataBufferSize += $dataBufferDiff;
-							$dataBufferChunk = fread($fileHandle, $dataBufferDiff);
-							if(feof($fileHandle) || $dataBufferChunk===false) { $dataBufferSize = 0; break; }
-							$dataBuffer .= $dataBufferChunk;
-						}
-					}
-				}
-			} else if(substru(strtoloweru($fileName), -3)=="png") {
-				$dataSignature = fread($fileHandle, 8);
-				$dataHeader = fread($fileHandle, 16);
-				if(!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n")
-				{
-					$width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]);
-					$height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]);
-					$type = "png";
-				}
-			} else if(substru(strtoloweru($fileName), -3)=="svg") {
-				$dataBufferSizeMax = filesize($fileName);
-				$dataBufferSize = min($dataBufferSizeMax, 4096);
-				if($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize);
-				$dataSignature = substrb($dataBuffer, 0, 5);
-				if(!feof($fileHandle) && $dataSignature=="\x3csvg\x20")
-				{
-					$dataBuffer = ($pos = strposu($dataBuffer, '>')) ? substru($dataBuffer, 0, $pos) : $dataBuffer;
-					if(preg_match("/ width=\"(\d+)\"/", $dataBuffer, $matches)) $width = $matches[1];
-					if(preg_match("/ height=\"(\d+)\"/", $dataBuffer, $matches)) $height = $matches[1];
-					$type = "svg";
-				}
-			}
-			fclose($fileHandle);
-		}
-		return array($width, $height, $type);
-	}
-	
-	// Start timer
-	function timerStart(&$time)
-	{
-		$time = microtime(true);
-	}
-	
-	// Stop timer and calculate elapsed time in milliseconds
-	function timerStop(&$time)
-	{
-		$time = intval((microtime(true)-$time) * 1000);
-	}
+    // Create random text for cryptography
+    public function createSalt($length, $bcryptFormat = false) {
+        $dataBuffer = $salt = "";
+        $dataBufferSize = $bcryptFormat ? intval(ceil($length/4) * 3) : intval(ceil($length/2));
+        if (empty($dataBuffer) && function_exists("random_bytes")) {
+            $dataBuffer = @random_bytes($dataBufferSize);
+        }
+        if (empty($dataBuffer) && function_exists("mcrypt_create_iv")) {
+            $dataBuffer = @mcrypt_create_iv($dataBufferSize, MCRYPT_DEV_URANDOM);
+        }
+        if (empty($dataBuffer) && function_exists("openssl_random_pseudo_bytes")) {
+            $dataBuffer = @openssl_random_pseudo_bytes($dataBufferSize);
+        }
+        if (strlenb($dataBuffer)==$dataBufferSize) {
+            if ($bcryptFormat) {
+                $salt = substrb(base64_encode($dataBuffer), 0, $length);
+                $base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+                $bcrypt64Chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+                $salt = strtr($salt, $base64Chars, $bcrypt64Chars);
+            } else {
+                $salt = substrb(bin2hex($dataBuffer), 0, $length);
+            }
+        }
+        return $salt;
+    }
+    
+    // Create hash with random salt, bcrypt or sha256
+    public function createHash($text, $algorithm, $cost = 0) {
+        $hash = "";
+        switch ($algorithm) {
+            case "bcrypt":  $prefix = sprintf("$2y$%02d$", $cost);
+                            $salt = $this->createSalt(22, true);
+                            $hash = crypt($text, $prefix.$salt);
+                            if (empty($salt) || strlenb($hash)!=60) $hash = "";
+                            break;
+            case "sha256":  $prefix = "$5y$";
+                            $salt = $this->createSalt(32);
+                            $hash = "$prefix$salt".hash("sha256", $salt.$text);
+                            if (empty($salt) || strlenb($hash)!=100) $hash = "";
+                            break;
+        }
+        return $hash;
+    }
+    
+    // Verify that text matches hash
+    public function verifyHash($text, $algorithm, $hash) {
+        $hashCalculated = "";
+        switch ($algorithm) {
+            case "bcrypt":  if (substrb($hash, 0, 4)=="$2y$" || substrb($hash, 0, 4)=="$2a$") {
+                                $hashCalculated = crypt($text, $hash);
+                            }
+                            break;
+            case "sha256":  if (substrb($hash, 0, 4)=="$5y$") {
+                                $prefix = "$5y$";
+                                $salt = substrb($hash, 4, 32);
+                                $hashCalculated = "$prefix$salt".hash("sha256", $salt.$text);
+                            }
+                            break;
+        }
+        return $this->verifyToken($hashCalculated, $hash);
+    }
+    
+    // Verify that token is not empty and identical, timing attack safe text string comparison
+    public function verifyToken($tokenExpected, $tokenReceived) {
+        $ok = false;
+        $lengthExpected = strlenb($tokenExpected);
+        $lengthReceived = strlenb($tokenReceived);
+        if ($lengthExpected!=0 && $lengthReceived!=0) {
+            $ok = $lengthExpected==$lengthReceived;
+            for ($i=0; $i<$lengthReceived; ++$i) {
+                $ok &= $tokenExpected[$i<$lengthExpected ? $i : 0]==$tokenReceived[$i];
+            }
+        }
+        return $ok;
+    }
+    
+    // Return meta data from raw data
+    public function getMetaData($rawData, $key) {
+        $value = "";
+        if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) {
+            $key = lcfirst($key);
+            foreach ($this->getTextLines($parts[2]) as $line) {
+                preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+                if (lcfirst($matches[1])==$key && !strempty($matches[2])) {
+                    $value = $matches[2];
+                    break;
+                }
+            }
+        }
+        return $value;
+    }
+    
+    // Set meta data in raw data
+    public function setMetaData($rawData, $key, $value) {
+        if (preg_match("/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+(.+?)\-\-\-[\r\n]+(.*)$/s", $rawData, $parts)) {
+            $key = lcfirst($key);
+            foreach ($this->getTextLines($parts[2]) as $line) {
+                preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+                if (lcfirst($matches[1])==$key) {
+                    $rawDataNew .= "$matches[1]: $value\n";
+                    $found = true;
+                } else {
+                    $rawDataNew .= $line;
+                }
+            }
+            if (!$found) $rawDataNew .= ucfirst($key).": $value\n";
+            $rawDataNew = $parts[1]."---\n".$rawDataNew."---\n".$parts[3];
+        } else {
+            $rawDataNew = $rawData;
+        }
+        return $rawDataNew;
+    }
+    
+    // Detect web browser language
+    public function detectBrowserLanguage($languages, $languageDefault) {
+        $language = $languageDefault;
+        if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"])) {
+            foreach (preg_split("/\s*,\s*/", $_SERVER["HTTP_ACCEPT_LANGUAGE"]) as $string) {
+                $tokens = explode(";", $string);
+                if (in_array($tokens[0], $languages)) {
+                    $language = $tokens[0];
+                    break;
+                }
+            }
+        }
+        return $language;
+    }
+    
+    // Detect image dimensions and type for gif/jpg/png/svg
+    public function detectImageInfo($fileName) {
+        $width = $height = 0;
+        $type = "";
+        $fileHandle = @fopen($fileName, "rb");
+        if ($fileHandle) {
+            if (substru(strtoloweru($fileName), -3)=="gif") {
+                $dataSignature = fread($fileHandle, 6);
+                $dataHeader = fread($fileHandle, 7);
+                if (!feof($fileHandle) && ($dataSignature=="GIF87a" || $dataSignature=="GIF89a")) {
+                    $width = (ord($dataHeader[1])<<8) + ord($dataHeader[0]);
+                    $height = (ord($dataHeader[3])<<8) + ord($dataHeader[2]);
+                    $type = "gif";
+                }
+            } elseif (substru(strtoloweru($fileName), -3)=="jpg") {
+                $dataBufferSizeMax = filesize($fileName);
+                $dataBufferSize = min($dataBufferSizeMax, 4096);
+                if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize);
+                $dataSignature = substrb($dataBuffer, 0, 4);
+                if (!feof($fileHandle) && ($dataSignature=="\xff\xd8\xff\xe0" || $dataSignature=="\xff\xd8\xff\xe1")) {
+                    for ($pos=2; $pos+8<$dataBufferSize; $pos+=$length) {
+                        if ($dataBuffer[$pos]!="\xff") break;
+                        if ($dataBuffer[$pos+1]=="\xc0" || $dataBuffer[$pos+1]=="\xc2") {
+                            $width = (ord($dataBuffer[$pos+7])<<8) + ord($dataBuffer[$pos+8]);
+                            $height = (ord($dataBuffer[$pos+5])<<8) + ord($dataBuffer[$pos+6]);
+                            $type = "jpg";
+                            break;
+                        }
+                        $length = (ord($dataBuffer[$pos+2])<<8) + ord($dataBuffer[$pos+3]) + 2;
+                        while ($pos+$length+8>=$dataBufferSize) {
+                            if ($dataBufferSize==$dataBufferSizeMax) break;
+                            $dataBufferDiff = min($dataBufferSizeMax, $dataBufferSize*2) - $dataBufferSize;
+                            $dataBufferSize += $dataBufferDiff;
+                            $dataBufferChunk = fread($fileHandle, $dataBufferDiff);
+                            if (feof($fileHandle) || $dataBufferChunk===false) {
+                                $dataBufferSize = 0;
+                                break;
+                            }
+                            $dataBuffer .= $dataBufferChunk;
+                        }
+                    }
+                }
+            } elseif (substru(strtoloweru($fileName), -3)=="png") {
+                $dataSignature = fread($fileHandle, 8);
+                $dataHeader = fread($fileHandle, 16);
+                if (!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n") {
+                    $width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]);
+                    $height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]);
+                    $type = "png";
+                }
+            } elseif (substru(strtoloweru($fileName), -3)=="svg") {
+                $dataBufferSizeMax = filesize($fileName);
+                $dataBufferSize = min($dataBufferSizeMax, 4096);
+                if ($dataBufferSize) $dataBuffer = fread($fileHandle, $dataBufferSize);
+                $dataSignature = substrb($dataBuffer, 0, 5);
+                if (!feof($fileHandle) && $dataSignature=="\x3csvg\x20") {
+                    $dataBuffer = ($pos = strposu($dataBuffer, ">")) ? substru($dataBuffer, 0, $pos) : $dataBuffer;
+                    if (preg_match("/ width=\"(\d+)\"/", $dataBuffer, $matches)) $width = $matches[1];
+                    if (preg_match("/ height=\"(\d+)\"/", $dataBuffer, $matches)) $height = $matches[1];
+                    $type = "svg";
+                }
+            }
+            fclose($fileHandle);
+        }
+        return array($width, $height, $type);
+    }
+    
+    // Start timer
+    public function timerStart(&$time) {
+        $time = microtime(true);
+    }
+    
+    // Stop timer and calculate elapsed time in milliseconds
+    public function timerStop(&$time) {
+        $time = intval((microtime(true)-$time) * 1000);
+    }
 }
 
 // Unicode support for PHP
 mb_internal_encoding("UTF-8");
-function strempty($string) { return is_null($string) || $string===""; }
-function strencode($string) { return addcslashes($string, "\'\"\\\/"); }
-function strreplaceu() { return call_user_func_array("str_replace", func_get_args()); }
-function strtoloweru() { return call_user_func_array("mb_strtolower", func_get_args()); }
-function strtoupperu() { return call_user_func_array("mb_strtoupper", func_get_args()); }
-function strlenu() { return call_user_func_array("mb_strlen", func_get_args()); }
-function strlenb() { return call_user_func_array("strlen", func_get_args()); }
-function strposu() { return call_user_func_array("mb_strpos", func_get_args()); }
-function strposb() { return call_user_func_array("strpos", func_get_args()); }
-function strrposu() { return call_user_func_array("mb_strrpos", func_get_args()); }
-function strrposb() { return call_user_func_array("strrpos", func_get_args()); }
-function substru() { return call_user_func_array("mb_substr", func_get_args()); }
-function substrb() { return call_user_func_array("substr", func_get_args()); }
+function strempty($string) {
+    return is_null($string) || $string==="";
+}
+function strencode($string) {
+    return addcslashes($string, "\'\"\\\/");
+}
+function strreplaceu() {
+    return call_user_func_array("str_replace", func_get_args());
+}
+function strtoloweru() {
+    return call_user_func_array("mb_strtolower", func_get_args());
+}
+function strtoupperu() {
+    return call_user_func_array("mb_strtoupper", func_get_args());
+}
+function strlenu() {
+    return call_user_func_array("mb_strlen", func_get_args());
+}
+function strlenb() {
+    return call_user_func_array("strlen", func_get_args());
+}
+function strposu() {
+    return call_user_func_array("mb_strpos", func_get_args());
+}
+function strposb() {
+    return call_user_func_array("strpos", func_get_args());
+}
+function strrposu() {
+    return call_user_func_array("mb_strrpos", func_get_args());
+}
+function strrposb() {
+    return call_user_func_array("strrpos", func_get_args());
+}
+function substru() {
+    return call_user_func_array("mb_substr", func_get_args());
+}
+function substrb() {
+    return call_user_func_array("substr", func_get_args());
+}
 
 // Error reporting for PHP
 error_reporting(E_ALL ^ E_NOTICE);
-?>

+ 440 - 200
system/plugins/edit.css

@@ -2,312 +2,552 @@
 /* Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se */
 /* This file may be used and distributed under the terms of the public license. */
 
-.yellow-bar { position:relative; line-height:2em; margin-bottom:10px; }
-.yellow-bar-left { display:block; float:left; }
-.yellow-bar-right { display:block; float:right; }
-.yellow-bar-right a { margin-left:1em; }
-.yellow-bar-right #yellow-pane-create-link { padding:0 0.5em; }
-.yellow-bar-right #yellow-pane-delete-link { padding:0 0.5em; }
-.yellow-bar-banner { clear:both; }
-.yellow-body-modal-open { overflow:hidden; }
+.yellow-bar {
+    position: relative;
+    line-height: 2em;
+    margin-bottom: 10px;
+}
+.yellow-bar-left {
+    display: block;
+    float: left;
+}
+.yellow-bar-right {
+    display: block;
+    float: right;
+}
+.yellow-bar-right a {
+    margin-left: 1em;
+}
+.yellow-bar-right #yellow-pane-create-link {
+    padding: 0 0.5em;
+}
+.yellow-bar-right #yellow-pane-delete-link {
+    padding: 0 0.5em;
+}
+.yellow-bar-banner {
+    clear: both;
+}
+.yellow-body-modal-open {
+    overflow: hidden;
+}
 .yellow-pane {
-	position:absolute; display:none; z-index:100; padding:10px;
-	background-color:#fff; color:#000;
-	border:1px solid #bbb; 
-	border-radius:4px; box-shadow:2px 4px 10px rgba(0, 0, 0, 0.2);
-}
-.yellow-pane h1 { color:#000; font-size:2em; margin:0 1em; }
-.yellow-pane p { margin:0.5em; }
-.yellow-pane .yellow-status { margin-bottom:1em; }
-.yellow-pane .yellow-fields { width:15em; text-align:left; margin:0 auto; }
-.yellow-pane .yellow-form-control { width:15em; box-sizing:border-box; }
-.yellow-pane .yellow-fields .yellow-btn { width:15em; margin:1em 0 0.5em 0; }
-.yellow-pane .yellow-buttons .yellow-btn { width:15em; margin:0.5em 0; }
+    position: absolute;
+    display: none;
+    z-index: 100;
+    padding: 10px;
+    background-color: #fff;
+    color: #000;
+    border: 1px solid #bbb;
+    border-radius: 4px;
+    box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2);
+}
+.yellow-pane h1 {
+    color: #000;
+    font-size: 2em;
+    margin: 0 1em;
+}
+.yellow-pane p {
+    margin: 0.5em;
+}
+.yellow-pane .yellow-status {
+    margin-bottom: 1em;
+}
+.yellow-pane .yellow-fields {
+    width: 15em;
+    text-align: left;
+    margin: 0 auto;
+}
+.yellow-pane .yellow-form-control {
+    width: 15em;
+    box-sizing: border-box;
+}
+.yellow-pane .yellow-fields .yellow-btn {
+    width: 15em;
+    margin: 1em 0 0.5em 0;
+}
+.yellow-pane .yellow-buttons .yellow-btn {
+    width: 15em;
+    margin: 0.5em 0;
+}
 .yellow-close {
-	position:absolute;
-	top:0.8em; right:1em; cursor:pointer;
-	font-size:0.9em; color:#bbb; text-decoration:none;
-}
-.yellow-close:hover { color:#000; text-decoration:none; }
-.yellow-arrow { position:absolute; top:0; left:0; }
-.yellow-arrow:after, .yellow-arrow:before {
-	position:absolute;
-	pointer-events:none;
-	bottom:100%;
-	height:0; width:0;
-	border:solid transparent;
-	content:"";
+    position: absolute;
+    top: 0.8em;
+    right: 1em;
+    cursor: pointer;
+    font-size: 0.9em;
+    color: #bbb;
+    text-decoration: none;
+}
+.yellow-close:hover {
+    color: #000;
+    text-decoration: none;
+}
+.yellow-arrow {
+    position: absolute;
+    top: 0;
+    left: 0;
+}
+.yellow-arrow:after,
+.yellow-arrow:before {
+    position: absolute;
+    pointer-events: none;
+    bottom: 100%;
+    height: 0;
+    width: 0;
+    border: solid transparent;
+    content: "";
 }
 .yellow-arrow:after {
-	border-color:rgba(255, 255, 255, 0);
-	border-bottom-color:#fff;
-	border-width:10px;
-	margin-left:-10px;
+    border-color: rgba(255, 255, 255, 0);
+    border-bottom-color: #fff;
+    border-width: 10px;
+    margin-left: -10px;
 }
 .yellow-arrow:before {
-	border-color:rgba(187, 187, 187, 0);
-	border-bottom-color:#bbb;
-	border-width:11px;
-	margin-left:-11px;
+    border-color: rgba(187, 187, 187, 0);
+    border-bottom-color: #bbb;
+    border-width: 11px;
+    margin-left: -11px;
 }
 .yellow-popup {
-	position:absolute; display:none; z-index:200; padding:10px 0;
-	background-color:#fff; color:#000;
-	border:1px solid #bbb;
-	border-radius:4px; box-shadow:2px 4px 10px rgba(0, 0, 0, 0.2);
-}
-.yellow-dropdown { list-style:none; margin:0; padding:0; }
-.yellow-dropdown span { display:block; margin:0; padding:0.25em 1em; }
-.yellow-dropdown a { display:block; padding:0.2em 1em; text-decoration:none; }
-.yellow-dropdown a:hover { color:#fff; background-color:#18e; text-decoration:none; }
-.yellow-dropdown-menu a { color:#000; }
-.yellow-toolbar { list-style:none; margin:0; padding:0; }
-.yellow-toolbar-left { display:inline-block; float:left; }
-.yellow-toolbar-right { display:inline-block; float:right; }
-.yellow-toolbar-banner { clear:both; }
-.yellow-toolbar li { display:inline-block; vertical-align:top; }
+    position: absolute;
+    display: none;
+    z-index: 200;
+    padding: 10px 0;
+    background-color: #fff;
+    color: #000;
+    border: 1px solid #bbb;
+    border-radius: 4px;
+    box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2);
+}
+.yellow-dropdown {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+}
+.yellow-dropdown span {
+    display: block;
+    margin: 0;
+    padding: 0.25em 1em;
+}
+.yellow-dropdown a {
+    display: block;
+    padding: 0.2em 1em;
+    text-decoration: none;
+}
+.yellow-dropdown a:hover {
+    color: #fff;
+    background-color: #18e;
+    text-decoration: none;
+}
+.yellow-dropdown-menu a {
+    color: #000;
+}
+.yellow-toolbar {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+}
+.yellow-toolbar-left {
+    display: inline-block;
+    float: left;
+}
+.yellow-toolbar-right {
+    display: inline-block;
+    float: right;
+}
+.yellow-toolbar-banner {
+    clear: both;
+}
+.yellow-toolbar li {
+    display: inline-block;
+    vertical-align: top;
+}
 .yellow-toolbar a {
-	display:inline-block; padding:6px 16px; text-decoration:none;
-	background-color:#fff; color:#000;
-	font-size:0.9em; font-weight:normal;
-	border:1px solid #bbb;
-	border-radius:4px;
+    display: inline-block;
+    padding: 6px 16px;
+    text-decoration: none;
+    background-color: #fff;
+    color: #000;
+    font-size: 0.9em;
+    font-weight: normal;
+    border: 1px solid #bbb;
+    border-radius: 4px;
 }
 .yellow-toolbar a:hover {
-	background-color:#18e; background-image:none; border-color:#18e; color:#fff;
-	text-decoration:none;
+    background-color: #18e;
+    background-image: none;
+    border-color: #18e;
+    color: #fff;
+    text-decoration: none;
+}
+.yellow-toolbar-left a {
+    margin-right: 4px;
+    margin-bottom: 10px;
 }
-.yellow-toolbar-left a { margin-right:4px; margin-bottom:10px; }
-.yellow-toolbar-right a { margin-left:4px; margin-bottom:10px; }
-.yellow-toolbar .yellow-icon
-{
-	font-size:0.9em; min-width:1em; text-align:center;
+.yellow-toolbar-right a {
+    margin-left: 4px;
+    margin-bottom: 10px;
+}
+.yellow-toolbar .yellow-icon {
+    font-size: 0.9em;
+    min-width: 1em;
+    text-align: center;
 }
 .yellow-toolbar .yellow-toolbar-btn {
-	padding:6px 10px; min-width:4em; text-align:center;
+    padding: 6px 10px;
+    min-width: 4em;
+    text-align: center;
 }
 .yellow-toolbar .yellow-toolbar-btn-edit {
-	background-color:#29f; border-color:#29f; color:#fff;
+    background-color: #29f;
+    border-color: #29f;
+    color: #fff;
 }
 .yellow-toolbar .yellow-toolbar-btn-create {
-	background-color:#29f; border-color:#29f; color:#fff
+    background-color: #29f;
+    border-color: #29f;
+    color: #fff;
 }
 .yellow-toolbar .yellow-toolbar-btn-delete {
-	background-color:#e55; border-color:#e55; color:#fff
+    background-color: #e55;
+    border-color: #e55;
+    color: #fff;
 }
-.yellow-toolbar .yellow-toolbar-btn-delete:hover { background-color:#d44; border-color:#d44; }
-.yellow-toolbar .yellow-toolbar-btn-separator { visibility:hidden; padding:6px; }
-.yellow-toolbar .yellow-toolbar-checked { background-color:#666; border-color:#666; color:#fff; }
-.yellow-toolbar-tooltip { position:relative; }
-.yellow-toolbar-tooltip::after, .yellow-toolbar-tooltip::before {
-	position:absolute; z-index:300; display:none;
-	pointer-events:none;
+.yellow-toolbar .yellow-toolbar-btn-delete:hover {
+    background-color: #d44;
+    border-color: #d44;
 }
-.yellow-toolbar-tooltip::after {
-	padding:2px 9px;
-	font-weight:normal;
-	font-size:0.9em;
-	text-align:center;
-	white-space:nowrap;
-	content:attr(aria-label);
-	background-color:#111; color:#ddd;
-	border-radius:3px;
-	top:100%;
-	right:50%;
-	margin-top:6px;
-	transform:translateX(50%);
+.yellow-toolbar .yellow-toolbar-btn-separator {
+    visibility: hidden;
+    padding: 6px;
 }
+.yellow-toolbar .yellow-toolbar-checked {
+    background-color: #666;
+    border-color: #666;
+    color: #fff;
+}
+.yellow-toolbar-tooltip {
+    position: relative;
+}
+.yellow-toolbar-tooltip::after,
 .yellow-toolbar-tooltip::before {
-	width:0; height:0;
-	content:"";
-	border:4px solid transparent;
-	top:auto;
-	right:50%;
-	bottom:-6px;
-	margin-right:-4px;
-	border-bottom-color:#111;
+    position: absolute;
+    z-index: 300;
+    display: none;
+    pointer-events: none;
 }
-.yellow-toolbar-tooltip:hover::before, .yellow-toolbar-tooltip:hover::after {
-	display:inline-block;
+.yellow-toolbar-tooltip::after {
+    padding: 2px 9px;
+    font-weight: normal;
+    font-size: 0.9em;
+    text-align: center;
+    white-space: nowrap;
+    content: attr(aria-label);
+    background-color: #111;
+    color: #ddd;
+    border-radius: 3px;
+    top: 100%;
+    right: 50%;
+    margin-top: 6px;
+    transform: translateX(50%);
 }
-.yellow-toolbar-selected.yellow-toolbar-tooltip::before, .yellow-toolbar-selected.yellow-toolbar-tooltip::after {
-	display:none;
+.yellow-toolbar-tooltip::before {
+    width: 0;
+    height: 0;
+    content: "";
+    border: 4px solid transparent;
+    top: auto;
+    right: 50%;
+    bottom: -6px;
+    margin-right: -4px;
+    border-bottom-color: #111;
+}
+.yellow-toolbar-tooltip:hover::before,
+.yellow-toolbar-tooltip:hover::after {
+    display: inline-block;
+}
+.yellow-toolbar-selected.yellow-toolbar-tooltip::before,
+.yellow-toolbar-selected.yellow-toolbar-tooltip::after {
+    display: none;
 }
 .yellow-form-control {
-	margin:0; padding:2px 4px;
-	display:inline-block;
-	background-color:#fff; color:#000;
-	background-image:linear-gradient(to bottom, #fff, #fff);
-	border:1px solid #bbb;
-	border-radius:4px;
-	font-size:0.9em; font-family:inherit; font-weight:normal; line-height:normal;
+    margin: 0;
+    padding: 2px 4px;
+    display: inline-block;
+    background-color: #fff;
+    color: #000;
+    background-image: linear-gradient(to bottom, #fff, #fff);
+    border: 1px solid #bbb;
+    border-radius: 4px;
+    font-size: 0.9em;
+    font-family: inherit;
+    font-weight: normal;
+    line-height: normal;
 }
 .yellow-btn {
-	margin:0; padding:4px 22px;
-	display:inline-block; min-width:8em;
-	background-color:#eaeaea; color:#333333;
-	background-image:linear-gradient(to bottom, #f8f8f8, #e1e1e1);
-	border:1px solid #bbb;
-	border-color:#c1c1c1 #c1c1c1 #aaaaaa;
-	border-radius:4px;
-	outline-offset:-2px;
-	font-size:0.9em; font-family:inherit; font-weight:normal; line-height:1;
-	text-align:center; text-decoration:none;
-	box-sizing:border-box;
-}
-.yellow-btn:hover, .yellow-btn:focus, .yellow-btn:active {
-	color:#333333;
-	background-image:none;
-	text-decoration:none;
-}
-.yellow-btn:active { box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.1); }
+    margin: 0;
+    padding: 4px 22px;
+    display: inline-block;
+    min-width: 8em;
+    background-color: #eaeaea;
+    color: #333333;
+    background-image: linear-gradient(to bottom, #f8f8f8, #e1e1e1);
+    border: 1px solid #bbb;
+    border-color: #c1c1c1 #c1c1c1 #aaaaaa;
+    border-radius: 4px;
+    outline-offset: -2px;
+    font-size: 0.9em;
+    font-family: inherit;
+    font-weight: normal;
+    line-height: 1;
+    text-align: center;
+    text-decoration: none;
+    box-sizing: border-box;
+}
+.yellow-btn:hover,
+.yellow-btn:focus,
+.yellow-btn:active {
+    color: #333333;
+    background-image: none;
+    text-decoration: none;
+}
+.yellow-btn:active {
+    box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+/* Specific panes */
 
-#yellow-pane-login, #yellow-pane-signup, #yellow-pane-forgot, #yellow-pane-recover, #yellow-pane-settings, #yellow-pane-version, #yellow-pane-quit {
-	text-align:center;
-}
-#yellow-pane-edit-toolbar-title { margin:-5px 0 0 0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
-#yellow-pane-edit-text { padding:0 2px; outline:none; resize:none; border:none; }
-#yellow-pane-edit-preview { padding:0; overflow:auto; }
-#yellow-pane-edit-preview h1 { margin:0.67em 0; }
-#yellow-pane-edit-preview p { margin:1em 0; }
-#yellow-pane-edit-preview .content { margin:0; padding:0; }
-#yellow-pane-user { padding:10px 0; }
+#yellow-pane-login,
+#yellow-pane-signup,
+#yellow-pane-forgot,
+#yellow-pane-recover,
+#yellow-pane-settings,
+#yellow-pane-version,
+#yellow-pane-quit {
+    text-align: center;
+}
+#yellow-pane-edit-toolbar-title {
+    margin: -5px 0 0 0;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+#yellow-pane-edit-text {
+    padding: 0 2px;
+    outline: none;
+    resize: none;
+    border: none;
+}
+#yellow-pane-edit-preview {
+    padding: 0;
+    overflow: auto;
+}
+#yellow-pane-edit-preview h1 {
+    margin: 0.67em 0;
+}
+#yellow-pane-edit-preview p {
+    margin: 1em 0;
+}
+#yellow-pane-edit-preview .content {
+    margin: 0;
+    padding: 0;
+}
+#yellow-pane-user {
+    padding: 10px 0;
+}
 
-#yellow-popup-format, #yellow-popup-heading, #yellow-popup-list { width:16em; }
-#yellow-popup-format a, #yellow-popup-heading a { padding:0.25em 16px; }
-#yellow-popup-format #yellow-popup-format-h1, #yellow-popup-heading #yellow-popup-heading-h1 { font-size:2em; font-weight:bold; }
-#yellow-popup-format #yellow-popup-format-h2, #yellow-popup-heading #yellow-popup-heading-h2 { font-size:1.6em; font-weight:bold; }
-#yellow-popup-format #yellow-popup-format-h3, #yellow-popup-heading #yellow-popup-heading-h3 { font-size:1.3em; font-weight:bold; }
-#yellow-popup-format #yellow-popup-format-quote { font-style:italic; }
-#yellow-popup-format #yellow-popup-format-pre { font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace; font-size:0.9em; line-height:1.8; }
-#yellow-popup-emojiawesome { padding:10px; width:14em; }
-#yellow-popup-emojiawesome a { padding:0.2em; }
-#yellow-popup-emojiawesome .yellow-dropdown li { display:inline-block; }
+/* Specific popups */
+
+#yellow-popup-format,
+#yellow-popup-heading,
+#yellow-popup-list {
+    width: 16em;
+}
+#yellow-popup-format a,
+#yellow-popup-heading a {
+    padding: 0.25em 16px;
+}
+#yellow-popup-format #yellow-popup-format-h1,
+#yellow-popup-heading #yellow-popup-heading-h1 {
+    font-size: 2em;
+    font-weight: bold;
+}
+#yellow-popup-format #yellow-popup-format-h2,
+#yellow-popup-heading #yellow-popup-heading-h2 {
+    font-size: 1.6em;
+    font-weight: bold;
+}
+#yellow-popup-format #yellow-popup-format-h3,
+#yellow-popup-heading #yellow-popup-heading-h3 {
+    font-size: 1.3em;
+    font-weight: bold;
+}
+#yellow-popup-format #yellow-popup-format-quote {
+    font-style: italic;
+}
+#yellow-popup-format #yellow-popup-format-pre {
+    font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+    font-size: 0.9em;
+    line-height: 1.8;
+}
+#yellow-popup-emojiawesome {
+    padding: 10px;
+    width: 14em;
+}
+#yellow-popup-emojiawesome a {
+    padding: 0.2em;
+}
+#yellow-popup-emojiawesome .yellow-dropdown li {
+    display: inline-block;
+}
+#yellow-popup-fontawesome {
+    padding: 10px;
+    width: 13em;
+}
+#yellow-popup-fontawesome a {
+    padding: 0.18em 0.3em;
+    min-width: 1em;
+    text-align: center;
+}
+#yellow-popup-fontawesome .yellow-dropdown li {
+    display: inline-block;
+}
 
-#yellow-popup-fontawesome { padding:10px; width:13em; }
-#yellow-popup-fontawesome a { padding:0.18em 0.3em; min-width:1em; text-align:center; }
-#yellow-popup-fontawesome .yellow-dropdown li { display:inline-block; }
+/* Icons */
 
 @font-face {
-	font-family:'Edit';
-	font-weight:normal;
-	font-style:normal;
-	src:url('edit.woff') format('woff');
+    font-family: "Edit";
+    font-weight: normal;
+    font-style: normal;
+    src: url("edit.woff") format("woff");
 }
 .yellow-icon {
-	display:inline-block;
-	font-family:Edit;
-	font-style:normal;
-	font-weight:normal;
-	-webkit-font-smoothing:antialiased;
-	-moz-osx-font-smoothing:grayscale;
+    display: inline-block;
+    font-family: Edit;
+    font-style: normal;
+    font-weight: normal;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
 }
 .yellow-spin {
-	-webkit-animation:yellow-spin 1s infinite steps(16);
-	animation:yellow-spin 1s infinite steps(16);
+    -webkit-animation: yellow-spin 1s infinite steps(16);
+    animation: yellow-spin 1s infinite steps(16);
 }
 @-webkit-keyframes yellow-spin {
-	0% { -webkit-transform:rotate(0deg); transform:rotate(0deg); }
-	100% { -webkit-transform:rotate(359deg); transform:rotate(359deg); }
+    0% {
+        -webkit-transform: rotate(0deg);
+        transform: rotate(0deg);
+    }
+    100% {
+        -webkit-transform: rotate(359deg);
+        transform: rotate(359deg);
+    }
 }
 @keyframes yellow-spin {
-	0% { -webkit-transform:rotate(0deg); transform:rotate(0deg); }
-	100% { -webkit-transform:rotate(359deg); transform:rotate(359deg); }
+    0% {
+        -webkit-transform: rotate(0deg);
+        transform: rotate(0deg);
+    }
+    100% {
+        -webkit-transform: rotate(359deg);
+        transform: rotate(359deg);
+    }
 }
 
 .yellow-icon-preview:before {
-	content: "\f100";
+    content: "\f100";
 }
 .yellow-icon-format:before {
-	content: "\f101";
+    content: "\f101";
 }
 .yellow-icon-paragraph:before {
-	content: "\f101";
+    content: "\f101";
 }
 .yellow-icon-heading:before {
-	content: "\f102";
+    content: "\f102";
 }
 .yellow-icon-h1:before {
-	content: "\f103";
+    content: "\f103";
 }
 .yellow-icon-h2:before {
-	content: "\f104";
+    content: "\f104";
 }
 .yellow-icon-h3:before {
-	content: "\f105";
+    content: "\f105";
 }
 .yellow-icon-bold:before {
-	content: "\f106";
+    content: "\f106";
 }
 .yellow-icon-italic:before {
-	content: "\f0f7";
+    content: "\f0f7";
 }
 .yellow-icon-strikethrough:before {
-	content: "\f108";
+    content: "\f108";
 }
 .yellow-icon-quote:before {
-	content: "\f109";
+    content: "\f109";
 }
 .yellow-icon-code:before {
-	content: "\f10a";
+    content: "\f10a";
 }
 .yellow-icon-pre:before {
-	content: "\f10a";
+    content: "\f10a";
 }
 .yellow-icon-link:before {
-	content: "\f10b";
+    content: "\f10b";
 }
 .yellow-icon-file:before {
-	content: "\f10c";
+    content: "\f10c";
 }
 .yellow-icon-list:before {
-	content: "\f10d";
+    content: "\f10d";
 }
 .yellow-icon-ul:before {
-	content: "\f10d";
+    content: "\f10d";
 }
 .yellow-icon-ol:before {
-	content: "\f10e";
+    content: "\f10e";
 }
 .yellow-icon-tl:before {
-	content: "\f10f";
+    content: "\f10f";
 }
 .yellow-icon-hr:before {
-	content: "\f110";
+    content: "\f110";
 }
 .yellow-icon-table:before {
-	content: "\f111";
+    content: "\f111";
 }
 .yellow-icon-emojiawesome:before {
-	content: "\f112";
+    content: "\f112";
 }
 .yellow-icon-fontawesome:before {
-	content: "\f113";
+    content: "\f113";
 }
 .yellow-icon-draft:before {
-	content: "\f114";
+    content: "\f114";
 }
 .yellow-icon-undo:before {
-	content: "\f115";
+    content: "\f115";
 }
 .yellow-icon-redo:before {
-	content: "\f116";
+    content: "\f116";
 }
 .yellow-icon-spinner:before {
-	content: "\f200";
+    content: "\f200";
 }
 .yellow-icon-search:before {
-	content: "\f201";
+    content: "\f201";
 }
 .yellow-icon-close:before {
-	content: "\f202";
+    content: "\f202";
 }
 .yellow-icon-help:before {
-	content: "\f203";
+    content: "\f203";
 }
 .yellow-icon-markdown:before {
-	content: "\f203";
+    content: "\f203";
 }
 .yellow-icon-logo:before {
-	content: "\f8ff";
+    content: "\f8ff";
 }

+ 1287 - 1449
system/plugins/edit.js

@@ -2,1478 +2,1316 @@
 // Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
-var yellow =
-{
-	action: function(action, status, args) { yellow.edit.action(action, status, args); },
-	onLoad: function() { yellow.edit.load(); },
-	onClickAction: function(e) { yellow.edit.clickAction(e); },
-	onClick: function(e) { yellow.edit.click(e); },
-	onKeydown: function(e) { yellow.edit.keydown(e); },
-	onDrag: function(e) { yellow.edit.drag(e); },
-	onDrop: function(e) { yellow.edit.drop(e); },
-	onUpdate: function() { yellow.edit.updatePane(yellow.edit.paneId, yellow.edit.paneAction, yellow.edit.paneStatus); },
-	onResize: function() { yellow.edit.resizePane(yellow.edit.paneId, yellow.edit.paneAction, yellow.edit.paneStatus); }
+var yellow = {
+    
+    // Main event handlers
+    action: function(action, status, args) { yellow.edit.action(action, status, args); },
+    onLoad: function() { yellow.edit.load(); },
+    onClickAction: function(e) { yellow.edit.clickAction(e); },
+    onClick: function(e) { yellow.edit.click(e); },
+    onKeydown: function(e) { yellow.edit.keydown(e); },
+    onDrag: function(e) { yellow.edit.drag(e); },
+    onDrop: function(e) { yellow.edit.drop(e); },
+    onUpdate: function() { yellow.edit.updatePane(yellow.edit.paneId, yellow.edit.paneAction, yellow.edit.paneStatus); },
+    onResize: function() { yellow.edit.resizePane(yellow.edit.paneId, yellow.edit.paneAction, yellow.edit.paneStatus); }
 };
 
-yellow.edit =
-{
-	paneId: 0,			//visible pane ID
-	paneActionOld: 0,	//previous pane action
-	paneAction: 0,		//current pane action
-	paneStatus: 0,		//current pane status
-	popupId: 0,			//visible popup ID
-	intervalId: 0,		//timer interval ID
+yellow.edit = {
+    paneId: 0,          //visible pane ID
+    paneActionOld: 0,   //previous pane action
+    paneAction: 0,      //current pane action
+    paneStatus: 0,      //current pane status
+    popupId: 0,         //visible popup ID
+    intervalId: 0,      //timer interval ID
 
-	// Handle initialisation
-	load: function()
-	{
-		var body = document.getElementsByTagName("body")[0];
-		if(body && body.firstChild && !document.getElementById("yellow-bar"))
-		{
-			this.createBar("yellow-bar");
-			this.createPane("yellow-pane-edit", "none", "none");
-			this.action(yellow.page.action, yellow.page.status);
-			clearInterval(this.intervalId);
-		}
-	},
-	
-	// Handle action
-	action: function(action, status, args)
-	{
-		status = status ? status : "none";
-		args = args ? args : "none";
-		switch(action)
-		{
-			case "login":		this.showPane("yellow-pane-login", action, status); break;
-			case "logout":		this.sendPane("yellow-pane-logout", action); break;
-			case "signup":		this.showPane("yellow-pane-signup", action, status); break;
-			case "confirm":		this.showPane("yellow-pane-signup", action, status); break;
-			case "approve":		this.showPane("yellow-pane-signup", action, status); break;
-			case "forgot":		this.showPane("yellow-pane-forgot", action, status); break;
-			case "recover":		this.showPane("yellow-pane-recover", action, status); break;
-			case "reactivate":	this.showPane("yellow-pane-settings", action, status); break;
-			case "settings":	this.showPane("yellow-pane-settings", action, status); break;
-			case "verify":		this.showPane("yellow-pane-settings", action, status); break;
-			case "change":		this.showPane("yellow-pane-settings", action, status); break;
-			case "version":		this.showPane("yellow-pane-version", action, status); break;
-			case "update":		this.sendPane("yellow-pane-update", action, status, args); break;
-			case "quit":		this.showPane("yellow-pane-quit", action, status); break;
-			case "remove":		this.showPane("yellow-pane-quit", action, status); break;
-			case "create":		this.showPane("yellow-pane-edit", action, status, true); break;
-			case "edit":		this.showPane("yellow-pane-edit", action, status, true); break;
-			case "delete":		this.showPane("yellow-pane-edit", action, status, true); break;
-			case "user":		this.showPane("yellow-pane-user", action, status); break;
-			case "send":		this.sendPane(this.paneId, this.paneAction); break;
-			case "close":		this.hidePane(this.paneId); break;
-			case "toolbar":		this.processToolbar(status, args); break;
-			case "help":		this.processHelp(); break;
-		}
-	},
-	
-	// Handle action clicked
-	clickAction: function(e)
-	{
-		e.stopPropagation();
-		e.preventDefault();
-		var element = e.target;
-		for(; element; element=element.parentNode)
-		{
-			if(element.tagName=="A") break;
-		}
-		this.action(element.getAttribute("data-action"), element.getAttribute("data-status"), element.getAttribute("data-args"));
-	},
-	
-	// Handle mouse clicked
-	click: function(e)
-	{
-		if(this.popupId && !document.getElementById(this.popupId).contains(e.target)) this.hidePopup(this.popupId, true);
-		if(this.paneId && !document.getElementById(this.paneId).contains(e.target)) this.hidePane(this.paneId, true);
-	},
-	
-	// Handle keyboard
-	keydown: function(e)
-	{
-		if(this.paneId=="yellow-pane-edit") this.processShortcut(e);
-		if(this.paneId && e.keyCode==27) this.hidePane(this.paneId);
-	},
-	
-	// Handle drag
-	drag: function(e)
-	{
-		e.stopPropagation();
-		e.preventDefault();
-	},
-	
-	// Handle drop
-	drop: function(e)
-	{
-		e.stopPropagation();
-		e.preventDefault();
-		var elementText = document.getElementById("yellow-pane-edit-text");
-		var files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
-		for(var i=0; i<files.length; i++) this.uploadFile(elementText, files[i]);
-	},
-	
-	// Create bar
-	createBar: function(barId)
-	{
-		if(yellow.config.debug) console.log("yellow.edit.createBar id:"+barId);
-		var elementBar = document.createElement("div");
-		elementBar.className = "yellow-bar";
-		elementBar.setAttribute("id", barId);
-		if(barId=="yellow-bar")
-		{
-			yellow.toolbox.addEvent(document, "click", yellow.onClick);
-			yellow.toolbox.addEvent(document, "keydown", yellow.onKeydown);
-			yellow.toolbox.addEvent(window, "resize", yellow.onResize);
-		}
-		var elementDiv = document.createElement("div");
-		elementDiv.setAttribute("id", barId+"-content");
-		if(yellow.config.userName)
-		{
-			elementDiv.innerHTML =
-				"<div class=\"yellow-bar-left\">"+
-				"<a href=\"#\" id=\"yellow-pane-edit-link\" data-action=\"edit\">"+this.getText("Edit")+"</a>"+
-				"</div>"+
-				"<div class=\"yellow-bar-right\">"+
-				"<a href=\"#\" id=\"yellow-pane-create-link\" data-action=\"create\">"+this.getText("Create")+"</a>"+
-				"<a href=\"#\" id=\"yellow-pane-delete-link\" data-action=\"delete\">"+this.getText("Delete")+"</a>"+
-				"<a href=\"#\" id=\"yellow-pane-user-link\" data-action=\"user\">"+yellow.toolbox.encodeHtml(yellow.config.userName)+"</a>"+
-				"</div>"+
-				"<div class=\"yellow-bar-banner\"></div>";
-		}
-		elementBar.appendChild(elementDiv);
-		yellow.toolbox.insertBefore(elementBar, document.getElementsByTagName("body")[0].firstChild);
-		this.bindActions(elementBar);
-	},
-	
-	// Create pane
-	createPane: function(paneId, paneAction, paneStatus)
-	{
-		if(yellow.config.debug) console.log("yellow.edit.createPane id:"+paneId);
-		var elementPane = document.createElement("div");
-		elementPane.className = "yellow-pane";
-		elementPane.setAttribute("id", paneId);
-		elementPane.style.display = "none";
-		if(paneId=="yellow-pane-edit")
-		{
-			yellow.toolbox.addEvent(elementPane, "input", yellow.onUpdate);
-			yellow.toolbox.addEvent(elementPane, "dragenter", yellow.onDrag);
-			yellow.toolbox.addEvent(elementPane, "dragover", yellow.onDrag);
-			yellow.toolbox.addEvent(elementPane, "drop", yellow.onDrop);
-		}
-		if(paneId=="yellow-pane-edit" || paneId=="yellow-pane-user")
-		{
-			var elementArrow = document.createElement("span");
-			elementArrow.className = "yellow-arrow";
-			elementArrow.setAttribute("id", paneId+"-arrow");
-			elementPane.appendChild(elementArrow);
-		}
-		var elementDiv = document.createElement("div");
-		elementDiv.className = "yellow-content";
-		elementDiv.setAttribute("id", paneId+"-content");
-		switch(paneId)
-		{
-			case "yellow-pane-login":
-				elementDiv.innerHTML =
-				"<form method=\"post\">"+
-				"<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
-				"<div class=\"yellow-title\"><h1>"+this.getText("LoginTitle")+"</h1></div>"+
-				"<div class=\"yellow-fields\" id=\"yellow-pane-login-fields\">"+
-				"<input type=\"hidden\" name=\"action\" value=\"login\" />"+
-				"<p><label for=\"yellow-pane-login-email\">"+this.getText("LoginEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-login-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(yellow.config.editLoginEmail)+"\" /></p>"+
-				"<p><label for=\"yellow-pane-login-password\">"+this.getText("LoginPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-login-password\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(yellow.config.editLoginPassword)+"\" /></p>"+
-				"<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("LoginButton")+"\" /></p>"+
-				"</div>"+
-				"<div class=\"yellow-actions\" id=\"yellow-pane-login-actions\">"+
-				"<p><a href=\"#\" id=\"yellow-pane-login-forgot\" data-action=\"forgot\">"+this.getText("LoginForgot")+"</a><br /><a href=\"#\" id=\"yellow-pane-login-signup\" data-action=\"signup\">"+this.getText("LoginSignup")+"</a></p>"+
-				"</div>"+
-				"</form>";
-				break;
-			case "yellow-pane-signup":
-				elementDiv.innerHTML =
-				"<form method=\"post\">"+
-				"<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
-				"<div class=\"yellow-title\"><h1>"+this.getText("SignupTitle")+"</h1></div>"+
-				"<div class=\"yellow-status\"><p id=\"yellow-pane-signup-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+
-				"<div class=\"yellow-fields\" id=\"yellow-pane-signup-fields\">"+
-				"<input type=\"hidden\" name=\"action\" value=\"signup\" />"+
-				"<p><label for=\"yellow-pane-signup-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-signup-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+
-				"<p><label for=\"yellow-pane-signup-email\">"+this.getText("SignupEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-signup-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+
-				"<p><label for=\"yellow-pane-signup-password\">"+this.getText("SignupPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-signup-password\" maxlength=\"64\" value=\"\" /></p>"+
-				"<p><input type=\"checkbox\" name=\"consent\" value=\"consent\" id=\"consent\""+(this.getRequest("consent") ? " checked=\"checked\"" : "")+"> <label for=\"consent\">"+this.getText("SignupConsent")+"</label></p>"+
-				"<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("SignupButton")+"\" /></p>"+
-				"</div>"+
-				"<div class=\"yellow-buttons\" id=\"yellow-pane-signup-buttons\">"+
-				"<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
-				"</div>"+
-				"</form>";
-				break;
-			case "yellow-pane-forgot":
-				elementDiv.innerHTML =
-				"<form method=\"post\">"+
-				"<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
-				"<div class=\"yellow-title\"><h1>"+this.getText("ForgotTitle")+"</h1></div>"+
-				"<div class=\"yellow-status\"><p id=\"yellow-pane-forgot-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+
-				"<div class=\"yellow-fields\" id=\"yellow-pane-forgot-fields\">"+
-				"<input type=\"hidden\" name=\"action\" value=\"forgot\" />"+
-				"<p><label for=\"yellow-pane-forgot-email\">"+this.getText("ForgotEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-forgot-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+
-				"<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+
-				"</div>"+
-				"<div class=\"yellow-buttons\" id=\"yellow-pane-forgot-buttons\">"+
-				"<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
-				"</div>"+
-				"</form>";
-				break;
-			case "yellow-pane-recover":
-				elementDiv.innerHTML =
-				"<form method=\"post\">"+
-				"<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
-				"<div class=\"yellow-title\"><h1>"+this.getText("RecoverTitle")+"</h1></div>"+
-				"<div class=\"yellow-status\"><p id=\"yellow-pane-recover-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+
-				"<div class=\"yellow-fields\" id=\"yellow-pane-recover-fields\">"+
-				"<p><label for=\"yellow-pane-recover-password\">"+this.getText("RecoverPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-recover-password\" maxlength=\"64\" value=\"\" /></p>"+
-				"<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+
-				"</div>"+
-				"<div class=\"yellow-buttons\" id=\"yellow-pane-recover-buttons\">"+
-				"<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
-				"</div>"+
-				"</form>";
-				break;
-			case "yellow-pane-settings":
-				var rawDataLanguages = "";
-				if(yellow.config.serverLanguages && Object.keys(yellow.config.serverLanguages).length>1)
-				{
-					rawDataLanguages += "<p>";
-					for(var language in yellow.config.serverLanguages)
-					{
-						var checked = language==this.getRequest("language") ? " checked=\"checked\"" : "";
-						rawDataLanguages += "<label for=\"yellow-pane-settings-"+language+"\"><input type=\"radio\" name=\"language\" id=\"yellow-pane-settings-"+language+"\" value=\""+language+"\""+checked+"> "+yellow.toolbox.encodeHtml(yellow.config.serverLanguages[language])+"</label><br />";
-					}
-					rawDataLanguages += "</p>";
-				}
-				elementDiv.innerHTML =
-				"<form method=\"post\">"+
-				"<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
-				"<div class=\"yellow-title\"><h1 id=\"yellow-pane-settings-title\">"+this.getText("SettingsTitle")+"</h1></div>"+
-				"<div class=\"yellow-status\"><p id=\"yellow-pane-settings-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+
-				"<div class=\"yellow-fields\" id=\"yellow-pane-settings-fields\">"+
-				"<input type=\"hidden\" name=\"action\" value=\"settings\" />"+
-				"<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+
-				"<p><label for=\"yellow-pane-settings-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-settings-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+
-				"<p><label for=\"yellow-pane-settings-email\">"+this.getText("SignupEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-settings-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+
-				"<p><label for=\"yellow-pane-settings-password\">"+this.getText("SignupPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-settings-password\" maxlength=\"64\" value=\"\" /></p>"+rawDataLanguages+
-				"<p>"+this.getText("SettingsQuit")+" <a href=\"#\" data-action=\"quit\">"+this.getText("SettingsMore")+"</a></p>"+
-				"<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+
-				"</div>"+
-				"<div class=\"yellow-buttons\" id=\"yellow-pane-settings-buttons\">"+
-				"<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
-				"</div>"+
-				"</form>";
-				break;
-			case "yellow-pane-version":
-				elementDiv.innerHTML =
-				"<form method=\"post\">"+
-				"<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
-				"<div class=\"yellow-title\"><h1 id=\"yellow-pane-version-title\">"+yellow.toolbox.encodeHtml(yellow.config.serverVersion)+"</h1></div>"+
-				"<div class=\"yellow-status\"><p id=\"yellow-pane-version-status\" class=\""+paneStatus+"\">"+this.getText("VersionStatus", "", paneStatus)+"</p></div>"+
-				"<div class=\"yellow-output\" id=\"yellow-pane-version-output\">"+yellow.page.rawDataOutput+"</div>"+
-				"<div class=\"yellow-buttons\" id=\"yellow-pane-version-buttons\">"+
-				"<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
-				"</div>"+
-				"</form>";
-				break;
-			case "yellow-pane-quit":
-				elementDiv.innerHTML =
-				"<form method=\"post\">"+
-				"<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
-				"<div class=\"yellow-title\"><h1>"+this.getText("QuitTitle")+"</h1></div>"+
-				"<div class=\"yellow-status\"><p id=\"yellow-pane-quit-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+
-				"<div class=\"yellow-fields\" id=\"yellow-pane-quit-fields\">"+
-				"<input type=\"hidden\" name=\"action\" value=\"quit\" />"+
-				"<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+
-				"<p><label for=\"yellow-pane-quit-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-quit-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+
-				"<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("DeleteButton")+"\" /></p>"+
-				"</div>"+
-				"<div class=\"yellow-buttons\" id=\"yellow-pane-quit-buttons\">"+
-				"<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
-				"</div>"+
-				"</form>";
-				break;
-			case "yellow-pane-edit":
-				var rawDataButtons = "";
-				if(yellow.config.editToolbarButtons && yellow.config.editToolbarButtons!="none")
-				{
-					var tokens = yellow.config.editToolbarButtons.split(",");
-					for(var i=0; i<tokens.length; i++)
-					{
-						var token = tokens[i].trim();
-						if(token!="separator")
-						{
-							rawDataButtons += "<li><a href=\"#\" id=\"yellow-toolbar-"+yellow.toolbox.encodeHtml(token)+"\" class=\"yellow-toolbar-btn-icon yellow-toolbar-tooltip\" data-action=\"toolbar\" data-status=\""+yellow.toolbox.encodeHtml(token)+"\" aria-label=\""+this.getText("Toolbar", "", token)+"\"><i class=\"yellow-icon yellow-icon-"+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>";
-						} else {
-							rawDataButtons += "<li><a href=\"#\" class=\"yellow-toolbar-btn-separator\"></a></li>";
-						}
-					}
-					if(yellow.config.debug) console.log("yellow.edit.createPane buttons:"+yellow.config.editToolbarButtons);
-				}
-				elementDiv.innerHTML =
-				"<form method=\"post\">"+
-				"<div id=\"yellow-pane-edit-toolbar\">"+
-				"<h1 id=\"yellow-pane-edit-toolbar-title\" class=\"yellow-toolbar yellow-toolbar-left\">"+this.getText("Edit")+"</h1>"+
-				"<ul id=\"yellow-pane-edit-toolbar-buttons\" class=\"yellow-toolbar yellow-toolbar-left\">"+rawDataButtons+"</ul>"+
-				"<ul id=\"yellow-pane-edit-toolbar-main\" class=\"yellow-toolbar yellow-toolbar-right\">"+
-				"<li><a href=\"#\" id=\"yellow-pane-edit-cancel\" class=\"yellow-toolbar-btn\" data-action=\"close\">"+this.getText("CancelButton")+"</a></li>"+
-				"<li><a href=\"#\" id=\"yellow-pane-edit-send\" class=\"yellow-toolbar-btn\" data-action=\"send\">"+this.getText("EditButton")+"</a></li>"+
-				"</ul>"+
-				"<ul class=\"yellow-toolbar yellow-toolbar-banner\"></ul>"+
-				"</div>"+
-				"<textarea id=\"yellow-pane-edit-text\" class=\"yellow-form-control\"></textarea>"+
-				"<div id=\"yellow-pane-edit-preview\"></div>"+
-				"</form>";
-				break;
-			case "yellow-pane-user":
-				elementDiv.innerHTML =
-				"<ul class=\"yellow-dropdown\">"+
-				"<li><span>"+yellow.toolbox.encodeHtml(yellow.config.userEmail)+"</span></li>"+
-				"<li><a href=\"#\" data-action=\"settings\">"+this.getText("SettingsTitle")+"</a></li>" +
-				"<li><a href=\"#\" data-action=\"help\">"+this.getText("UserHelp")+"</a></li>" +
-				"<li><a href=\"#\" data-action=\"logout\">"+this.getText("UserLogout")+"</a></li>"+
-				"</ul>";
-				break;
-		}
-		elementPane.appendChild(elementDiv);
-		yellow.toolbox.insertAfter(elementPane, document.getElementsByTagName("body")[0].firstChild);
-		this.bindActions(elementPane);
-	},
+    // Handle initialisation
+    load: function() {
+        var body = document.getElementsByTagName("body")[0];
+        if (body && body.firstChild && !document.getElementById("yellow-bar")) {
+            this.createBar("yellow-bar");
+            this.createPane("yellow-pane-edit", "none", "none");
+            this.action(yellow.page.action, yellow.page.status);
+            clearInterval(this.intervalId);
+        }
+    },
+    
+    // Handle action
+    action: function(action, status, args) {
+        status = status ? status : "none";
+        args = args ? args : "none";
+        switch (action) {
+            case "login":       this.showPane("yellow-pane-login", action, status); break;
+            case "logout":      this.sendPane("yellow-pane-logout", action); break;
+            case "signup":      this.showPane("yellow-pane-signup", action, status); break;
+            case "confirm":     this.showPane("yellow-pane-signup", action, status); break;
+            case "approve":     this.showPane("yellow-pane-signup", action, status); break;
+            case "forgot":      this.showPane("yellow-pane-forgot", action, status); break;
+            case "recover":     this.showPane("yellow-pane-recover", action, status); break;
+            case "reactivate":  this.showPane("yellow-pane-settings", action, status); break;
+            case "settings":    this.showPane("yellow-pane-settings", action, status); break;
+            case "verify":      this.showPane("yellow-pane-settings", action, status); break;
+            case "change":      this.showPane("yellow-pane-settings", action, status); break;
+            case "version":     this.showPane("yellow-pane-version", action, status); break;
+            case "update":      this.sendPane("yellow-pane-update", action, status, args); break;
+            case "quit":        this.showPane("yellow-pane-quit", action, status); break;
+            case "remove":      this.showPane("yellow-pane-quit", action, status); break;
+            case "create":      this.showPane("yellow-pane-edit", action, status, true); break;
+            case "edit":        this.showPane("yellow-pane-edit", action, status, true); break;
+            case "delete":      this.showPane("yellow-pane-edit", action, status, true); break;
+            case "user":        this.showPane("yellow-pane-user", action, status); break;
+            case "send":        this.sendPane(this.paneId, this.paneAction); break;
+            case "close":       this.hidePane(this.paneId); break;
+            case "toolbar":     this.processToolbar(status, args); break;
+            case "help":        this.processHelp(); break;
+        }
+    },
+    
+    // Handle action clicked
+    clickAction: function(e) {
+        e.stopPropagation();
+        e.preventDefault();
+        var element = e.target;
+        for (; element; element=element.parentNode) {
+            if (element.tagName=="A") break;
+        }
+        this.action(element.getAttribute("data-action"), element.getAttribute("data-status"), element.getAttribute("data-args"));
+    },
+    
+    // Handle mouse clicked
+    click: function(e) {
+        if (this.popupId && !document.getElementById(this.popupId).contains(e.target)) this.hidePopup(this.popupId, true);
+        if (this.paneId && !document.getElementById(this.paneId).contains(e.target)) this.hidePane(this.paneId, true);
+    },
+    
+    // Handle keyboard
+    keydown: function(e) {
+        if (this.paneId=="yellow-pane-edit") this.processShortcut(e);
+        if (this.paneId && e.keyCode==27) this.hidePane(this.paneId);
+    },
+    
+    // Handle drag
+    drag: function(e) {
+        e.stopPropagation();
+        e.preventDefault();
+    },
+    
+    // Handle drop
+    drop: function(e) {
+        e.stopPropagation();
+        e.preventDefault();
+        var elementText = document.getElementById("yellow-pane-edit-text");
+        var files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
+        for (var i=0; i<files.length; i++) this.uploadFile(elementText, files[i]);
+    },
+    
+    // Create bar
+    createBar: function(barId) {
+        if (yellow.config.debug) console.log("yellow.edit.createBar id:"+barId);
+        var elementBar = document.createElement("div");
+        elementBar.className = "yellow-bar";
+        elementBar.setAttribute("id", barId);
+        if (barId=="yellow-bar") {
+            yellow.toolbox.addEvent(document, "click", yellow.onClick);
+            yellow.toolbox.addEvent(document, "keydown", yellow.onKeydown);
+            yellow.toolbox.addEvent(window, "resize", yellow.onResize);
+        }
+        var elementDiv = document.createElement("div");
+        elementDiv.setAttribute("id", barId+"-content");
+        if (yellow.config.userName) {
+            elementDiv.innerHTML =
+                "<div class=\"yellow-bar-left\">"+
+                "<a href=\"#\" id=\"yellow-pane-edit-link\" data-action=\"edit\">"+this.getText("Edit")+"</a>"+
+                "</div>"+
+                "<div class=\"yellow-bar-right\">"+
+                "<a href=\"#\" id=\"yellow-pane-create-link\" data-action=\"create\">"+this.getText("Create")+"</a>"+
+                "<a href=\"#\" id=\"yellow-pane-delete-link\" data-action=\"delete\">"+this.getText("Delete")+"</a>"+
+                "<a href=\"#\" id=\"yellow-pane-user-link\" data-action=\"user\">"+yellow.toolbox.encodeHtml(yellow.config.userName)+"</a>"+
+                "</div>"+
+                "<div class=\"yellow-bar-banner\"></div>";
+        }
+        elementBar.appendChild(elementDiv);
+        yellow.toolbox.insertBefore(elementBar, document.getElementsByTagName("body")[0].firstChild);
+        this.bindActions(elementBar);
+    },
+    
+    // Create pane
+    createPane: function(paneId, paneAction, paneStatus) {
+        if (yellow.config.debug) console.log("yellow.edit.createPane id:"+paneId);
+        var elementPane = document.createElement("div");
+        elementPane.className = "yellow-pane";
+        elementPane.setAttribute("id", paneId);
+        elementPane.style.display = "none";
+        if (paneId=="yellow-pane-edit") {
+            yellow.toolbox.addEvent(elementPane, "input", yellow.onUpdate);
+            yellow.toolbox.addEvent(elementPane, "dragenter", yellow.onDrag);
+            yellow.toolbox.addEvent(elementPane, "dragover", yellow.onDrag);
+            yellow.toolbox.addEvent(elementPane, "drop", yellow.onDrop);
+        }
+        if (paneId=="yellow-pane-edit" || paneId=="yellow-pane-user") {
+            var elementArrow = document.createElement("span");
+            elementArrow.className = "yellow-arrow";
+            elementArrow.setAttribute("id", paneId+"-arrow");
+            elementPane.appendChild(elementArrow);
+        }
+        var elementDiv = document.createElement("div");
+        elementDiv.className = "yellow-content";
+        elementDiv.setAttribute("id", paneId+"-content");
+        switch (paneId) {
+            case "yellow-pane-login":
+                elementDiv.innerHTML =
+                "<form method=\"post\">"+
+                "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
+                "<div class=\"yellow-title\"><h1>"+this.getText("LoginTitle")+"</h1></div>"+
+                "<div class=\"yellow-fields\" id=\"yellow-pane-login-fields\">"+
+                "<input type=\"hidden\" name=\"action\" value=\"login\" />"+
+                "<p><label for=\"yellow-pane-login-email\">"+this.getText("LoginEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-login-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(yellow.config.editLoginEmail)+"\" /></p>"+
+                "<p><label for=\"yellow-pane-login-password\">"+this.getText("LoginPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-login-password\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(yellow.config.editLoginPassword)+"\" /></p>"+
+                "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("LoginButton")+"\" /></p>"+
+                "</div>"+
+                "<div class=\"yellow-actions\" id=\"yellow-pane-login-actions\">"+
+                "<p><a href=\"#\" id=\"yellow-pane-login-forgot\" data-action=\"forgot\">"+this.getText("LoginForgot")+"</a><br /><a href=\"#\" id=\"yellow-pane-login-signup\" data-action=\"signup\">"+this.getText("LoginSignup")+"</a></p>"+
+                "</div>"+
+                "</form>";
+                break;
+            case "yellow-pane-signup":
+                elementDiv.innerHTML =
+                "<form method=\"post\">"+
+                "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
+                "<div class=\"yellow-title\"><h1>"+this.getText("SignupTitle")+"</h1></div>"+
+                "<div class=\"yellow-status\"><p id=\"yellow-pane-signup-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+
+                "<div class=\"yellow-fields\" id=\"yellow-pane-signup-fields\">"+
+                "<input type=\"hidden\" name=\"action\" value=\"signup\" />"+
+                "<p><label for=\"yellow-pane-signup-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-signup-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+
+                "<p><label for=\"yellow-pane-signup-email\">"+this.getText("SignupEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-signup-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+
+                "<p><label for=\"yellow-pane-signup-password\">"+this.getText("SignupPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-signup-password\" maxlength=\"64\" value=\"\" /></p>"+
+                "<p><input type=\"checkbox\" name=\"consent\" value=\"consent\" id=\"consent\""+(this.getRequest("consent") ? " checked=\"checked\"" : "")+"> <label for=\"consent\">"+this.getText("SignupConsent")+"</label></p>"+
+                "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("SignupButton")+"\" /></p>"+
+                "</div>"+
+                "<div class=\"yellow-buttons\" id=\"yellow-pane-signup-buttons\">"+
+                "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
+                "</div>"+
+                "</form>";
+                break;
+            case "yellow-pane-forgot":
+                elementDiv.innerHTML =
+                "<form method=\"post\">"+
+                "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
+                "<div class=\"yellow-title\"><h1>"+this.getText("ForgotTitle")+"</h1></div>"+
+                "<div class=\"yellow-status\"><p id=\"yellow-pane-forgot-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+
+                "<div class=\"yellow-fields\" id=\"yellow-pane-forgot-fields\">"+
+                "<input type=\"hidden\" name=\"action\" value=\"forgot\" />"+
+                "<p><label for=\"yellow-pane-forgot-email\">"+this.getText("ForgotEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-forgot-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+
+                "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+
+                "</div>"+
+                "<div class=\"yellow-buttons\" id=\"yellow-pane-forgot-buttons\">"+
+                "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
+                "</div>"+
+                "</form>";
+                break;
+            case "yellow-pane-recover":
+                elementDiv.innerHTML =
+                "<form method=\"post\">"+
+                "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
+                "<div class=\"yellow-title\"><h1>"+this.getText("RecoverTitle")+"</h1></div>"+
+                "<div class=\"yellow-status\"><p id=\"yellow-pane-recover-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+
+                "<div class=\"yellow-fields\" id=\"yellow-pane-recover-fields\">"+
+                "<p><label for=\"yellow-pane-recover-password\">"+this.getText("RecoverPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-recover-password\" maxlength=\"64\" value=\"\" /></p>"+
+                "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+
+                "</div>"+
+                "<div class=\"yellow-buttons\" id=\"yellow-pane-recover-buttons\">"+
+                "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
+                "</div>"+
+                "</form>";
+                break;
+            case "yellow-pane-settings":
+                var rawDataLanguages = "";
+                if (yellow.config.serverLanguages && Object.keys(yellow.config.serverLanguages).length>1) {
+                    rawDataLanguages += "<p>";
+                    for (var language in yellow.config.serverLanguages) {
+                        var checked = language==this.getRequest("language") ? " checked=\"checked\"" : "";
+                        rawDataLanguages += "<label for=\"yellow-pane-settings-"+language+"\"><input type=\"radio\" name=\"language\" id=\"yellow-pane-settings-"+language+"\" value=\""+language+"\""+checked+"> "+yellow.toolbox.encodeHtml(yellow.config.serverLanguages[language])+"</label><br />";
+                    }
+                    rawDataLanguages += "</p>";
+                }
+                elementDiv.innerHTML =
+                "<form method=\"post\">"+
+                "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
+                "<div class=\"yellow-title\"><h1 id=\"yellow-pane-settings-title\">"+this.getText("SettingsTitle")+"</h1></div>"+
+                "<div class=\"yellow-status\"><p id=\"yellow-pane-settings-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+
+                "<div class=\"yellow-fields\" id=\"yellow-pane-settings-fields\">"+
+                "<input type=\"hidden\" name=\"action\" value=\"settings\" />"+
+                "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+
+                "<p><label for=\"yellow-pane-settings-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-settings-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+
+                "<p><label for=\"yellow-pane-settings-email\">"+this.getText("SignupEmail")+"</label><br /><input class=\"yellow-form-control\" name=\"email\" id=\"yellow-pane-settings-email\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("email"))+"\" /></p>"+
+                "<p><label for=\"yellow-pane-settings-password\">"+this.getText("SignupPassword")+"</label><br /><input class=\"yellow-form-control\" type=\"password\" name=\"password\" id=\"yellow-pane-settings-password\" maxlength=\"64\" value=\"\" /></p>"+rawDataLanguages+
+                "<p>"+this.getText("SettingsQuit")+" <a href=\"#\" data-action=\"quit\">"+this.getText("SettingsMore")+"</a></p>"+
+                "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("OkButton")+"\" /></p>"+
+                "</div>"+
+                "<div class=\"yellow-buttons\" id=\"yellow-pane-settings-buttons\">"+
+                "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
+                "</div>"+
+                "</form>";
+                break;
+            case "yellow-pane-version":
+                elementDiv.innerHTML =
+                "<form method=\"post\">"+
+                "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
+                "<div class=\"yellow-title\"><h1 id=\"yellow-pane-version-title\">"+yellow.toolbox.encodeHtml(yellow.config.serverVersion)+"</h1></div>"+
+                "<div class=\"yellow-status\"><p id=\"yellow-pane-version-status\" class=\""+paneStatus+"\">"+this.getText("VersionStatus", "", paneStatus)+"</p></div>"+
+                "<div class=\"yellow-output\" id=\"yellow-pane-version-output\">"+yellow.page.rawDataOutput+"</div>"+
+                "<div class=\"yellow-buttons\" id=\"yellow-pane-version-buttons\">"+
+                "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
+                "</div>"+
+                "</form>";
+                break;
+            case "yellow-pane-quit":
+                elementDiv.innerHTML =
+                "<form method=\"post\">"+
+                "<a href=\"#\" class=\"yellow-close\" data-action=\"close\"><i class=\"yellow-icon yellow-icon-close\"></i></a>"+
+                "<div class=\"yellow-title\"><h1>"+this.getText("QuitTitle")+"</h1></div>"+
+                "<div class=\"yellow-status\"><p id=\"yellow-pane-quit-status\" class=\""+paneStatus+"\">"+this.getText(paneAction+"Status", "", paneStatus)+"</p></div>"+
+                "<div class=\"yellow-fields\" id=\"yellow-pane-quit-fields\">"+
+                "<input type=\"hidden\" name=\"action\" value=\"quit\" />"+
+                "<input type=\"hidden\" name=\"csrftoken\" value=\""+yellow.toolbox.encodeHtml(this.getCookie("csrftoken"))+"\" />"+
+                "<p><label for=\"yellow-pane-quit-name\">"+this.getText("SignupName")+"</label><br /><input class=\"yellow-form-control\" name=\"name\" id=\"yellow-pane-quit-name\" maxlength=\"64\" value=\""+yellow.toolbox.encodeHtml(this.getRequest("name"))+"\" /></p>"+
+                "<p><input class=\"yellow-btn\" type=\"submit\" value=\""+this.getText("DeleteButton")+"\" /></p>"+
+                "</div>"+
+                "<div class=\"yellow-buttons\" id=\"yellow-pane-quit-buttons\">"+
+                "<p><a href=\"#\" class=\"yellow-btn\" data-action=\"close\">"+this.getText("OkButton")+"</a></p>"+
+                "</div>"+
+                "</form>";
+                break;
+            case "yellow-pane-edit":
+                var rawDataButtons = "";
+                if (yellow.config.editToolbarButtons && yellow.config.editToolbarButtons!="none") {
+                    var tokens = yellow.config.editToolbarButtons.split(",");
+                    for (var i=0; i<tokens.length; i++) {
+                        var token = tokens[i].trim();
+                        if (token!="separator") {
+                            rawDataButtons += "<li><a href=\"#\" id=\"yellow-toolbar-"+yellow.toolbox.encodeHtml(token)+"\" class=\"yellow-toolbar-btn-icon yellow-toolbar-tooltip\" data-action=\"toolbar\" data-status=\""+yellow.toolbox.encodeHtml(token)+"\" aria-label=\""+this.getText("Toolbar", "", token)+"\"><i class=\"yellow-icon yellow-icon-"+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>";
+                        } else {
+                            rawDataButtons += "<li><a href=\"#\" class=\"yellow-toolbar-btn-separator\"></a></li>";
+                        }
+                    }
+                    if (yellow.config.debug) console.log("yellow.edit.createPane buttons:"+yellow.config.editToolbarButtons);
+                }
+                elementDiv.innerHTML =
+                "<form method=\"post\">"+
+                "<div id=\"yellow-pane-edit-toolbar\">"+
+                "<h1 id=\"yellow-pane-edit-toolbar-title\" class=\"yellow-toolbar yellow-toolbar-left\">"+this.getText("Edit")+"</h1>"+
+                "<ul id=\"yellow-pane-edit-toolbar-buttons\" class=\"yellow-toolbar yellow-toolbar-left\">"+rawDataButtons+"</ul>"+
+                "<ul id=\"yellow-pane-edit-toolbar-main\" class=\"yellow-toolbar yellow-toolbar-right\">"+
+                "<li><a href=\"#\" id=\"yellow-pane-edit-cancel\" class=\"yellow-toolbar-btn\" data-action=\"close\">"+this.getText("CancelButton")+"</a></li>"+
+                "<li><a href=\"#\" id=\"yellow-pane-edit-send\" class=\"yellow-toolbar-btn\" data-action=\"send\">"+this.getText("EditButton")+"</a></li>"+
+                "</ul>"+
+                "<ul class=\"yellow-toolbar yellow-toolbar-banner\"></ul>"+
+                "</div>"+
+                "<textarea id=\"yellow-pane-edit-text\" class=\"yellow-form-control\"></textarea>"+
+                "<div id=\"yellow-pane-edit-preview\"></div>"+
+                "</form>";
+                break;
+            case "yellow-pane-user":
+                elementDiv.innerHTML =
+                "<ul class=\"yellow-dropdown\">"+
+                "<li><span>"+yellow.toolbox.encodeHtml(yellow.config.userEmail)+"</span></li>"+
+                "<li><a href=\"#\" data-action=\"settings\">"+this.getText("SettingsTitle")+"</a></li>" +
+                "<li><a href=\"#\" data-action=\"help\">"+this.getText("UserHelp")+"</a></li>" +
+                "<li><a href=\"#\" data-action=\"logout\">"+this.getText("UserLogout")+"</a></li>"+
+                "</ul>";
+                break;
+        }
+        elementPane.appendChild(elementDiv);
+        yellow.toolbox.insertAfter(elementPane, document.getElementsByTagName("body")[0].firstChild);
+        this.bindActions(elementPane);
+    },
 
-	// Update pane
-	updatePane: function(paneId, paneAction, paneStatus, init)
-	{
-		if(yellow.config.debug) console.log("yellow.edit.updatePane id:"+paneId);
-		var showFields = paneStatus!="next" && paneStatus!="done";
-		switch(paneId)
-		{
-			case "yellow-pane-login":
-				if(yellow.config.editLoginRestrictions)
-				{
-					yellow.toolbox.setVisible(document.getElementById("yellow-pane-login-signup"), false);
-				}
-				break;
-			case "yellow-pane-signup":
-				yellow.toolbox.setVisible(document.getElementById("yellow-pane-signup-fields"), showFields);
-				yellow.toolbox.setVisible(document.getElementById("yellow-pane-signup-buttons"), !showFields);
-				break;
-			case "yellow-pane-forgot":
-				yellow.toolbox.setVisible(document.getElementById("yellow-pane-forgot-fields"), showFields);
-				yellow.toolbox.setVisible(document.getElementById("yellow-pane-forgot-buttons"), !showFields);
-				break;
-			case "yellow-pane-recover":
-				yellow.toolbox.setVisible(document.getElementById("yellow-pane-recover-fields"), showFields);
-				yellow.toolbox.setVisible(document.getElementById("yellow-pane-recover-buttons"), !showFields);
-				break;
-			case "yellow-pane-settings":
-				yellow.toolbox.setVisible(document.getElementById("yellow-pane-settings-fields"), showFields);
-				yellow.toolbox.setVisible(document.getElementById("yellow-pane-settings-buttons"), !showFields);
-				if(paneStatus=="none")
-				{
-					document.getElementById("yellow-pane-settings-status").innerHTML = "<a href=\"#\" data-action=\"version\">"+this.getText("VersionTitle")+"</a>";
-					document.getElementById("yellow-pane-settings-name").value = yellow.config.userName;
-					document.getElementById("yellow-pane-settings-email").value = yellow.config.userEmail;
-					document.getElementById("yellow-pane-settings-"+yellow.config.userLanguage).checked = true;
-				}
-				break;
-			case "yellow-pane-version":
-				if(paneStatus=="none" && this.isPlugin("update"))
-				{
-					document.getElementById("yellow-pane-version-status").innerHTML = this.getText("VersionStatusCheck");
-					document.getElementById("yellow-pane-version-output").innerHTML = "";
-					setTimeout("yellow.action('send');", 500);
-				}
-				if(paneStatus=="updates" && this.isPlugin("update"))
-				{
-					document.getElementById("yellow-pane-version-status").innerHTML = "<a href=\"#\" data-action=\"update\">"+this.getText("VersionStatusUpdates")+"</a>";
-				}
-				break;
-			case "yellow-pane-quit":
-				yellow.toolbox.setVisible(document.getElementById("yellow-pane-quit-fields"), showFields);
-				yellow.toolbox.setVisible(document.getElementById("yellow-pane-quit-buttons"), !showFields);
-				if(paneStatus=="none")
-				{
-					document.getElementById("yellow-pane-quit-status").innerHTML = this.getText("QuitStatusNone");
-					document.getElementById("yellow-pane-quit-name").value = "";
-				}
-				break;
-			case "yellow-pane-edit":
-				document.getElementById("yellow-pane-edit-text").focus();
-				if(init)
-				{
-					yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-text"), true);
-					yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-preview"), false);
-					document.getElementById("yellow-pane-edit-toolbar-title").innerHTML = yellow.toolbox.encodeHtml(yellow.page.title);
-					document.getElementById("yellow-pane-edit-text").value = paneAction=="create" ? yellow.page.rawDataNew : yellow.page.rawDataEdit;
-					var matches = document.getElementById("yellow-pane-edit-text").value.match(/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+/);
-					var position = document.getElementById("yellow-pane-edit-text").value.indexOf("\n", matches ? matches[0].length : 0);
-					document.getElementById("yellow-pane-edit-text").setSelectionRange(position, position);
-					if(yellow.config.editToolbarButtons!="none")
-					{
-						yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-toolbar-title"), false);
-						this.updateToolbar(0, "yellow-toolbar-checked");
-					}
-					if(yellow.config.userRestrictions)
-					{
-						yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-send"), false);
-						document.getElementById("yellow-pane-edit-text").readOnly = true;
-					}
-				}
-				if(!yellow.config.userRestrictions)
-				{
-					var key, className;
-					switch(this.getAction(paneId, paneAction))
-					{
-						case "create":	key = "CreateButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-create"; break;
-						case "edit":	key = "EditButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-edit"; break;
-						case "delete":	key = "DeleteButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-delete"; break;
-					}
-					if(document.getElementById("yellow-pane-edit-send").className != className)
-					{
-						document.getElementById("yellow-pane-edit-send").innerHTML = this.getText(key);
-						document.getElementById("yellow-pane-edit-send").className = className;
-						this.resizePane(paneId, paneAction, paneStatus);
-					}
-				}
-				break;
-		}
-		this.bindActions(document.getElementById(paneId));
-	},
+    // Update pane
+    updatePane: function(paneId, paneAction, paneStatus, init) {
+        if (yellow.config.debug) console.log("yellow.edit.updatePane id:"+paneId);
+        var showFields = paneStatus!="next" && paneStatus!="done";
+        switch (paneId) {
+            case "yellow-pane-login":
+                if (yellow.config.editLoginRestrictions) {
+                    yellow.toolbox.setVisible(document.getElementById("yellow-pane-login-signup"), false);
+                }
+                break;
+            case "yellow-pane-signup":
+                yellow.toolbox.setVisible(document.getElementById("yellow-pane-signup-fields"), showFields);
+                yellow.toolbox.setVisible(document.getElementById("yellow-pane-signup-buttons"), !showFields);
+                break;
+            case "yellow-pane-forgot":
+                yellow.toolbox.setVisible(document.getElementById("yellow-pane-forgot-fields"), showFields);
+                yellow.toolbox.setVisible(document.getElementById("yellow-pane-forgot-buttons"), !showFields);
+                break;
+            case "yellow-pane-recover":
+                yellow.toolbox.setVisible(document.getElementById("yellow-pane-recover-fields"), showFields);
+                yellow.toolbox.setVisible(document.getElementById("yellow-pane-recover-buttons"), !showFields);
+                break;
+            case "yellow-pane-settings":
+                yellow.toolbox.setVisible(document.getElementById("yellow-pane-settings-fields"), showFields);
+                yellow.toolbox.setVisible(document.getElementById("yellow-pane-settings-buttons"), !showFields);
+                if (paneStatus=="none") {
+                    document.getElementById("yellow-pane-settings-status").innerHTML = "<a href=\"#\" data-action=\"version\">"+this.getText("VersionTitle")+"</a>";
+                    document.getElementById("yellow-pane-settings-name").value = yellow.config.userName;
+                    document.getElementById("yellow-pane-settings-email").value = yellow.config.userEmail;
+                    document.getElementById("yellow-pane-settings-"+yellow.config.userLanguage).checked = true;
+                }
+                break;
+            case "yellow-pane-version":
+                if (paneStatus=="none" && this.isPlugin("update")) {
+                    document.getElementById("yellow-pane-version-status").innerHTML = this.getText("VersionStatusCheck");
+                    document.getElementById("yellow-pane-version-output").innerHTML = "";
+                    setTimeout("yellow.action('send');", 500);
+                }
+                if (paneStatus=="updates" && this.isPlugin("update")) {
+                    document.getElementById("yellow-pane-version-status").innerHTML = "<a href=\"#\" data-action=\"update\">"+this.getText("VersionStatusUpdates")+"</a>";
+                }
+                break;
+            case "yellow-pane-quit":
+                yellow.toolbox.setVisible(document.getElementById("yellow-pane-quit-fields"), showFields);
+                yellow.toolbox.setVisible(document.getElementById("yellow-pane-quit-buttons"), !showFields);
+                if (paneStatus=="none") {
+                    document.getElementById("yellow-pane-quit-status").innerHTML = this.getText("QuitStatusNone");
+                    document.getElementById("yellow-pane-quit-name").value = "";
+                }
+                break;
+            case "yellow-pane-edit":
+                document.getElementById("yellow-pane-edit-text").focus();
+                if (init) {
+                    yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-text"), true);
+                    yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-preview"), false);
+                    document.getElementById("yellow-pane-edit-toolbar-title").innerHTML = yellow.toolbox.encodeHtml(yellow.page.title);
+                    document.getElementById("yellow-pane-edit-text").value = paneAction=="create" ? yellow.page.rawDataNew : yellow.page.rawDataEdit;
+                    var matches = document.getElementById("yellow-pane-edit-text").value.match(/^(\xEF\xBB\xBF)?\-\-\-[\r\n]+/);
+                    var position = document.getElementById("yellow-pane-edit-text").value.indexOf("\n", matches ? matches[0].length : 0);
+                    document.getElementById("yellow-pane-edit-text").setSelectionRange(position, position);
+                    if (yellow.config.editToolbarButtons!="none") {
+                        yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-toolbar-title"), false);
+                        this.updateToolbar(0, "yellow-toolbar-checked");
+                    }
+                    if (yellow.config.userRestrictions) {
+                        yellow.toolbox.setVisible(document.getElementById("yellow-pane-edit-send"), false);
+                        document.getElementById("yellow-pane-edit-text").readOnly = true;
+                    }
+                }
+                if (!yellow.config.userRestrictions) {
+                    var key, className;
+                    switch (this.getAction(paneId, paneAction)) {
+                        case "create":    key = "CreateButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-create"; break;
+                        case "edit":    key = "EditButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-edit"; break;
+                        case "delete":    key = "DeleteButton"; className = "yellow-toolbar-btn yellow-toolbar-btn-delete"; break;
+                    }
+                    if (document.getElementById("yellow-pane-edit-send").className != className) {
+                        document.getElementById("yellow-pane-edit-send").innerHTML = this.getText(key);
+                        document.getElementById("yellow-pane-edit-send").className = className;
+                        this.resizePane(paneId, paneAction, paneStatus);
+                    }
+                }
+                break;
+        }
+        this.bindActions(document.getElementById(paneId));
+    },
 
-	// Resize pane
-	resizePane: function(paneId, paneAction, paneStatus)
-	{
-		var elementBar = document.getElementById("yellow-bar-content");
-		var paneLeft = yellow.toolbox.getOuterLeft(elementBar);
-		var paneTop = yellow.toolbox.getOuterTop(elementBar) + yellow.toolbox.getOuterHeight(elementBar) + 10;
-		var paneWidth = yellow.toolbox.getOuterWidth(elementBar);
-		var paneHeight = yellow.toolbox.getWindowHeight() - paneTop - Math.min(yellow.toolbox.getOuterHeight(elementBar) + 10, (yellow.toolbox.getWindowWidth()-yellow.toolbox.getOuterWidth(elementBar))/2);
-		switch(paneId)
-		{
-			case "yellow-pane-login":
-			case "yellow-pane-signup":
-			case "yellow-pane-forgot":
-			case "yellow-pane-recover":
-			case "yellow-pane-settings":
-			case "yellow-pane-version":
-			case "yellow-pane-quit":
-				yellow.toolbox.setOuterLeft(document.getElementById(paneId), paneLeft);
-				yellow.toolbox.setOuterTop(document.getElementById(paneId), paneTop);
-				yellow.toolbox.setOuterWidth(document.getElementById(paneId), paneWidth);
-				break;
-			case "yellow-pane-edit":
-				yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-edit"), paneLeft);
-				yellow.toolbox.setOuterTop(document.getElementById("yellow-pane-edit"), paneTop);
-				yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit"), paneHeight);
-				yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit"), paneWidth);
-				var elementWidth = yellow.toolbox.getWidth(document.getElementById("yellow-pane-edit"));
-				yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-text"), elementWidth);
-				yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-preview"), elementWidth);
-				var buttonsWidth = 0;
-				var buttonsWidthMax = yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-edit-toolbar")) -
-					yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-edit-toolbar-main")) - 1;
-				var element = document.getElementById("yellow-pane-edit-toolbar-buttons").firstChild;
-				for(; element; element=element.nextSibling)
-				{
-					element.removeAttribute("style");
-					buttonsWidth += yellow.toolbox.getOuterWidth(element);
-					if(buttonsWidth>buttonsWidthMax) yellow.toolbox.setVisible(element, false);
-				}
-				yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-toolbar-title"), buttonsWidthMax);
-				var height1 = yellow.toolbox.getHeight(document.getElementById("yellow-pane-edit"));
-				var height2 = yellow.toolbox.getOuterHeight(document.getElementById("yellow-pane-edit-toolbar"));
-				yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit-text"), height1 - height2);
-				yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit-preview"), height1 - height2);
-				var elementLink = document.getElementById("yellow-pane-"+paneAction+"-link");
-				var position = yellow.toolbox.getOuterLeft(elementLink) + yellow.toolbox.getOuterWidth(elementLink)/2;
-				position -= yellow.toolbox.getOuterLeft(document.getElementById("yellow-pane-edit")) + 1;
-				yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-edit-arrow"), position);
-				break;
-			case "yellow-pane-user":
-				yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-user"), paneLeft + paneWidth - yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-user")));
-				yellow.toolbox.setOuterTop(document.getElementById("yellow-pane-user"), paneTop);
-				var elementLink = document.getElementById("yellow-pane-user-link");
-				var position = yellow.toolbox.getOuterLeft(elementLink) + yellow.toolbox.getOuterWidth(elementLink)/2;
-				position -= yellow.toolbox.getOuterLeft(document.getElementById("yellow-pane-user"));
-				yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-user-arrow"), position);
-				break;
-		}
-	},
-	
-	// Show or hide pane
-	showPane: function(paneId, paneAction, paneStatus, modal)
-	{
-		if(this.paneId!=paneId || this.paneAction!=paneAction)
-		{
-			this.hidePane(this.paneId);
-			if(!document.getElementById(paneId)) this.createPane(paneId, paneAction, paneStatus);
-			var element = document.getElementById(paneId);
-			if(!yellow.toolbox.isVisible(element))
-			{
-				if(yellow.config.debug) console.log("yellow.edit.showPane id:"+paneId);
-				yellow.toolbox.setVisible(element, true);
-				if(modal)
-				{
-					yellow.toolbox.addClass(document.body, "yellow-body-modal-open");
-					yellow.toolbox.addValue("meta[name=viewport]", "content", ", maximum-scale=1, user-scalable=0");
-				}
-				this.paneId = paneId;
-				this.paneAction = paneAction;
-				this.paneStatus = paneStatus;
-				this.updatePane(paneId, paneAction, paneStatus, this.paneActionOld!=this.paneAction);
-				this.resizePane(paneId, paneAction, paneStatus);
-			}
-		} else {
-			this.hidePane(this.paneId, true);
-		}
-	},
+    // Resize pane
+    resizePane: function(paneId, paneAction, paneStatus) {
+        var elementBar = document.getElementById("yellow-bar-content");
+        var paneLeft = yellow.toolbox.getOuterLeft(elementBar);
+        var paneTop = yellow.toolbox.getOuterTop(elementBar) + yellow.toolbox.getOuterHeight(elementBar) + 10;
+        var paneWidth = yellow.toolbox.getOuterWidth(elementBar);
+        var paneHeight = yellow.toolbox.getWindowHeight() - paneTop - Math.min(yellow.toolbox.getOuterHeight(elementBar) + 10, (yellow.toolbox.getWindowWidth()-yellow.toolbox.getOuterWidth(elementBar))/2);
+        switch (paneId) {
+            case "yellow-pane-login":
+            case "yellow-pane-signup":
+            case "yellow-pane-forgot":
+            case "yellow-pane-recover":
+            case "yellow-pane-settings":
+            case "yellow-pane-version":
+            case "yellow-pane-quit":
+                yellow.toolbox.setOuterLeft(document.getElementById(paneId), paneLeft);
+                yellow.toolbox.setOuterTop(document.getElementById(paneId), paneTop);
+                yellow.toolbox.setOuterWidth(document.getElementById(paneId), paneWidth);
+                break;
+            case "yellow-pane-edit":
+                yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-edit"), paneLeft);
+                yellow.toolbox.setOuterTop(document.getElementById("yellow-pane-edit"), paneTop);
+                yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit"), paneHeight);
+                yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit"), paneWidth);
+                var elementWidth = yellow.toolbox.getWidth(document.getElementById("yellow-pane-edit"));
+                yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-text"), elementWidth);
+                yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-preview"), elementWidth);
+                var buttonsWidth = 0;
+                var buttonsWidthMax = yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-edit-toolbar")) -
+                    yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-edit-toolbar-main")) - 1;
+                var element = document.getElementById("yellow-pane-edit-toolbar-buttons").firstChild;
+                for (; element; element=element.nextSibling) {
+                    element.removeAttribute("style");
+                    buttonsWidth += yellow.toolbox.getOuterWidth(element);
+                    if (buttonsWidth>buttonsWidthMax) yellow.toolbox.setVisible(element, false);
+                }
+                yellow.toolbox.setOuterWidth(document.getElementById("yellow-pane-edit-toolbar-title"), buttonsWidthMax);
+                var height1 = yellow.toolbox.getHeight(document.getElementById("yellow-pane-edit"));
+                var height2 = yellow.toolbox.getOuterHeight(document.getElementById("yellow-pane-edit-toolbar"));
+                yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit-text"), height1 - height2);
+                yellow.toolbox.setOuterHeight(document.getElementById("yellow-pane-edit-preview"), height1 - height2);
+                var elementLink = document.getElementById("yellow-pane-"+paneAction+"-link");
+                var position = yellow.toolbox.getOuterLeft(elementLink) + yellow.toolbox.getOuterWidth(elementLink)/2;
+                position -= yellow.toolbox.getOuterLeft(document.getElementById("yellow-pane-edit")) + 1;
+                yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-edit-arrow"), position);
+                break;
+            case "yellow-pane-user":
+                yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-user"), paneLeft + paneWidth - yellow.toolbox.getOuterWidth(document.getElementById("yellow-pane-user")));
+                yellow.toolbox.setOuterTop(document.getElementById("yellow-pane-user"), paneTop);
+                var elementLink = document.getElementById("yellow-pane-user-link");
+                var position = yellow.toolbox.getOuterLeft(elementLink) + yellow.toolbox.getOuterWidth(elementLink)/2;
+                position -= yellow.toolbox.getOuterLeft(document.getElementById("yellow-pane-user"));
+                yellow.toolbox.setOuterLeft(document.getElementById("yellow-pane-user-arrow"), position);
+                break;
+        }
+    },
+    
+    // Show or hide pane
+    showPane: function(paneId, paneAction, paneStatus, modal) {
+        if (this.paneId!=paneId || this.paneAction!=paneAction) {
+            this.hidePane(this.paneId);
+            if (!document.getElementById(paneId)) this.createPane(paneId, paneAction, paneStatus);
+            var element = document.getElementById(paneId);
+            if (!yellow.toolbox.isVisible(element)) {
+                if (yellow.config.debug) console.log("yellow.edit.showPane id:"+paneId);
+                yellow.toolbox.setVisible(element, true);
+                if (modal) {
+                    yellow.toolbox.addClass(document.body, "yellow-body-modal-open");
+                    yellow.toolbox.addValue("meta[name=viewport]", "content", ", maximum-scale=1, user-scalable=0");
+                }
+                this.paneId = paneId;
+                this.paneAction = paneAction;
+                this.paneStatus = paneStatus;
+                this.updatePane(paneId, paneAction, paneStatus, this.paneActionOld!=this.paneAction);
+                this.resizePane(paneId, paneAction, paneStatus);
+            }
+        } else {
+            this.hidePane(this.paneId, true);
+        }
+    },
 
-	// Hide pane
-	hidePane: function(paneId, fadeout)
-	{
-		var element = document.getElementById(paneId);
-		if(yellow.toolbox.isVisible(element))
-		{
-			yellow.toolbox.removeClass(document.body, "yellow-body-modal-open");
-			yellow.toolbox.removeValue("meta[name=viewport]", "content", ", maximum-scale=1, user-scalable=0");
-			yellow.toolbox.setVisible(element, false, fadeout);
-			this.paneId = 0;
-			this.paneActionOld = this.paneAction;
-			this.paneAction = 0;
-			this.paneStatus = 0;
-		}
-		this.hidePopup(this.popupId);
-	},
+    // Hide pane
+    hidePane: function(paneId, fadeout) {
+        var element = document.getElementById(paneId);
+        if (yellow.toolbox.isVisible(element)) {
+            yellow.toolbox.removeClass(document.body, "yellow-body-modal-open");
+            yellow.toolbox.removeValue("meta[name=viewport]", "content", ", maximum-scale=1, user-scalable=0");
+            yellow.toolbox.setVisible(element, false, fadeout);
+            this.paneId = 0;
+            this.paneActionOld = this.paneAction;
+            this.paneAction = 0;
+            this.paneStatus = 0;
+        }
+        this.hidePopup(this.popupId);
+    },
 
-	// Send pane
-	sendPane: function(paneId, paneAction, paneStatus, paneArgs)
-	{
-		if(yellow.config.debug) console.log("yellow.edit.sendPane id:"+paneId);
-		var args = { "action":paneAction, "csrftoken":this.getCookie("csrftoken") };
-		if(paneId=="yellow-pane-edit")
-		{
-			args.action = this.getAction(paneId, paneAction);
-			args.rawdatasource = yellow.page.rawDataSource;
-			args.rawdataedit = document.getElementById("yellow-pane-edit-text").value;
-			args.rawdataendofline = yellow.page.rawDataEndOfLine;
-		}
-		if(paneArgs)
-		{
-			var tokens = paneArgs.split("/");
-			for(var i=0; i<tokens.length; i++)
-			{
-				var pair = tokens[i].split(/[:=]/);
-				if(!pair[0] || !pair[1]) continue;
-				args[pair[0]] = pair[1];
-			}
-		}
-		yellow.toolbox.submitForm(args);
-	},
-	
-	// Process help
-	processHelp: function()
-	{
-		this.hidePane(this.paneId);
-		window.open(this.getText("HelpUrl", "yellow"), "_self");
-	},
-	
-	// Process shortcut
-	processShortcut: function(e)
-	{
-		var shortcut = yellow.toolbox.getEventShortcut(e)
-		if(shortcut)
-		{
-			var tokens = yellow.config.editKeyboardShortcuts.split(",");
-			for(var i=0; i<tokens.length; i++)
-			{
-				var pair = tokens[i].trim().split(" ");
-				if(shortcut==pair[0] || shortcut.replace("meta+", "ctrl+")==pair[0])
-				{
-					e.stopPropagation();
-					e.preventDefault();
-					this.processToolbar(pair[1]);
-				}
-			}
-		}
-	},
-	
-	// Process toolbar
-	processToolbar: function(status, args)
-	{
-		if(yellow.config.debug) console.log("yellow.edit.processToolbar status:"+status);
-		var elementText = document.getElementById("yellow-pane-edit-text");
-		var elementPreview = document.getElementById("yellow-pane-edit-preview");
-		if(!yellow.config.userRestrictions && this.paneAction!="delete" && !yellow.toolbox.isVisible(elementPreview))
-		{
-			switch(status)
-			{
-				case "h1":			yellow.editor.setMarkdown(elementText, "# ", "insert-multiline-block", true); break;
-				case "h2":			yellow.editor.setMarkdown(elementText, "## ", "insert-multiline-block", true); break;
-				case "h3":			yellow.editor.setMarkdown(elementText, "### ", "insert-multiline-block", true); break;
-				case "paragraph":	yellow.editor.setMarkdown(elementText, "", "remove-multiline-block");
-									yellow.editor.setMarkdown(elementText, "", "remove-fenced-block"); break;
-				case "quote":		yellow.editor.setMarkdown(elementText, "> ", "insert-multiline-block", true); break;
-				case "pre":			yellow.editor.setMarkdown(elementText, "```\n", "insert-fenced-block", true); break;
-				case "bold":		yellow.editor.setMarkdown(elementText, "**", "insert-inline", true); break;
-				case "italic":		yellow.editor.setMarkdown(elementText, "*", "insert-inline", true); break;
-				case "strikethrough": yellow.editor.setMarkdown(elementText, "~~", "insert-inline", true); break;
-				case "code":		yellow.editor.setMarkdown(elementText, "`", "insert-autodetect", true); break;
-				case "ul":			yellow.editor.setMarkdown(elementText, "* ", "insert-multiline-block", true); break;
-				case "ol":			yellow.editor.setMarkdown(elementText, "1. ", "insert-multiline-block", true); break;
-				case "tl":			yellow.editor.setMarkdown(elementText, "- [ ] ", "insert-multiline-block", true); break;
-				case "link":		yellow.editor.setMarkdown(elementText, "[link](url)", "insert", false, yellow.editor.getMarkdownLink); break;
-				case "text":		yellow.editor.setMarkdown(elementText, args, "insert"); break;
-				case "draft":		yellow.editor.setMetaData(elementText, "status", "draft", true); break;
-				case "file":		this.showFileDialog(); break;
-				case "undo":		yellow.editor.undo(); break;
-				case "redo":		yellow.editor.redo(); break;
-			}
-		}
-		if(status=="preview") this.showPreview(elementText, elementPreview);
-		if(status=="save" && !yellow.config.userRestrictions && this.paneAction!="delete") this.action("send");
-		if(status=="help") window.open(this.getText("HelpUrl", "yellow"), "_blank");
-		if(status=="markdown") window.open(this.getText("MarkdownUrl", "yellow"), "_blank");
-		if(status=="format" || status=="heading" || status=="list" || status=="emojiawesome" || status=="fontawesome")
-		{
-			this.showPopup("yellow-popup-"+status, status);
-		} else {
-			this.hidePopup(this.popupId);
-		}
-	},
-	
-	// Update toolbar
-	updateToolbar: function(status, name)
-	{
-		if(status)
-		{
-			var element = document.getElementById("yellow-toolbar-"+status);
-			if(element) yellow.toolbox.addClass(element, name);
-		} else {
-			var elements = document.getElementsByClassName(name);
-			for(var i=0, l=elements.length; i<l; i++)
-			{
-				yellow.toolbox.removeClass(elements[i], name);
-			}
-		}
-	},
-	
-	// Create popup
-	createPopup: function(popupId)
-	{
-		if(yellow.config.debug) console.log("yellow.edit.createPopup id:"+popupId);
-		var elementPopup = document.createElement("div");
-		elementPopup.className = "yellow-popup";
-		elementPopup.setAttribute("id", popupId);
-		elementPopup.style.display = "none";
-		var elementDiv = document.createElement("div");
-		elementDiv.setAttribute("id", popupId+"-content");
-		switch(popupId)
-		{
-			case "yellow-popup-format":
-				elementDiv.innerHTML =
-				"<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+
-				"<li><a href=\"#\" id=\"yellow-popup-format-h1\" data-action=\"toolbar\" data-status=\"h1\">"+this.getText("ToolbarH1")+"</a></li>"+
-				"<li><a href=\"#\" id=\"yellow-popup-format-h2\" data-action=\"toolbar\" data-status=\"h2\">"+this.getText("ToolbarH2")+"</a></li>"+
-				"<li><a href=\"#\" id=\"yellow-popup-format-h3\" data-action=\"toolbar\" data-status=\"h3\">"+this.getText("ToolbarH3")+"</a></li>"+
-				"<li><a href=\"#\" id=\"yellow-popup-format-paragraph\" data-action=\"toolbar\" data-status=\"paragraph\">"+this.getText("ToolbarParagraph")+"</a></li>"+
-				"<li><a href=\"#\" id=\"yellow-popup-format-pre\" data-action=\"toolbar\" data-status=\"pre\">"+this.getText("ToolbarPre")+"</a></li>"+
-				"<li><a href=\"#\" id=\"yellow-popup-format-quote\" data-action=\"toolbar\" data-status=\"quote\">"+this.getText("ToolbarQuote")+"</a></li>"+
-				"</ul>";
-				break;
-			case "yellow-popup-heading":
-				elementDiv.innerHTML =
-				"<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+
-				"<li><a href=\"#\" id=\"yellow-popup-heading-h1\" data-action=\"toolbar\" data-status=\"h1\">"+this.getText("ToolbarH1")+"</a></li>"+
-				"<li><a href=\"#\" id=\"yellow-popup-heading-h2\" data-action=\"toolbar\" data-status=\"h2\">"+this.getText("ToolbarH2")+"</a></li>"+
-				"<li><a href=\"#\" id=\"yellow-popup-heading-h3\" data-action=\"toolbar\" data-status=\"h3\">"+this.getText("ToolbarH3")+"</a></li>"+
-				"</ul>";
-				break;
-			case "yellow-popup-list":
-				elementDiv.innerHTML =
-				"<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+
-				"<li><a href=\"#\" id=\"yellow-popup-list-ul\" data-action=\"toolbar\" data-status=\"ul\">"+this.getText("ToolbarUl")+"</a></li>"+
-				"<li><a href=\"#\" id=\"yellow-popup-list-ol\" data-action=\"toolbar\" data-status=\"ol\">"+this.getText("ToolbarOl")+"</a></li>"+
-				"</ul>";
-				break;
-			case "yellow-popup-emojiawesome":
-				var rawDataEmojis = "";
-				if(yellow.config.emojiawesomeToolbarButtons && yellow.config.emojiawesomeToolbarButtons!="none")
-				{
-					var tokens = yellow.config.emojiawesomeToolbarButtons.split(" ");
-					for(var i=0; i<tokens.length; i++)
-					{
-						var token = tokens[i].replace(/[\:]/g,"");
-						var className = token.replace("+1", "plus1").replace("-1", "minus1").replace(/_/g, "-");
-						rawDataEmojis += "<li><a href=\"#\" id=\"yellow-popup-list-"+yellow.toolbox.encodeHtml(token)+"\" data-action=\"toolbar\" data-status=\"text\" data-args=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"ea ea-"+yellow.toolbox.encodeHtml(className)+"\"></i></a></li>";
-					}
-				}
-				elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataEmojis+"</ul>";
-				break;
-			case "yellow-popup-fontawesome":
-				var rawDataIcons = "";
-				if(yellow.config.fontawesomeToolbarButtons && yellow.config.fontawesomeToolbarButtons!="none")
-				{
-					var tokens = yellow.config.fontawesomeToolbarButtons.split(" ");
-					for(var i=0; i<tokens.length; i++)
-					{
-						var token = tokens[i].replace(/[\:]/g,"");
-						rawDataIcons += "<li><a href=\"#\" id=\"yellow-popup-list-"+yellow.toolbox.encodeHtml(token)+"\" data-action=\"toolbar\" data-status=\"text\" data-args=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"fa "+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>";
-					}
-				}
-				elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataIcons+"</ul>";
-				break;
-		}
-		elementPopup.appendChild(elementDiv);
-		yellow.toolbox.insertAfter(elementPopup, document.getElementsByTagName("body")[0].firstChild);
-		this.bindActions(elementPopup);
-	},
-	
-	// Show or hide popup
-	showPopup: function(popupId, status)
-	{
-		if(this.popupId!=popupId)
-		{
-			this.hidePopup(this.popupId);
-			if(!document.getElementById(popupId)) this.createPopup(popupId);
-			var element = document.getElementById(popupId);
-			if(yellow.config.debug) console.log("yellow.edit.showPopup id:"+popupId);
-			yellow.toolbox.setVisible(element, true);
-			this.popupId = popupId;
-			this.updateToolbar(status, "yellow-toolbar-selected");
-			var elementParent = document.getElementById("yellow-toolbar-"+status);
-			var popupLeft = yellow.toolbox.getOuterLeft(elementParent);
-			var popupTop = yellow.toolbox.getOuterTop(elementParent) + yellow.toolbox.getOuterHeight(elementParent) - 1;
-			yellow.toolbox.setOuterLeft(document.getElementById(popupId), popupLeft);
-			yellow.toolbox.setOuterTop(document.getElementById(popupId), popupTop);
-		} else {
-			this.hidePopup(this.popupId, true);
-		}
-	},
-	
-	// Hide popup
-	hidePopup: function(popupId, fadeout)
-	{
-		var element = document.getElementById(popupId);
-		if(yellow.toolbox.isVisible(element))
-		{
-			yellow.toolbox.setVisible(element, false, fadeout);
-			this.popupId = 0;
-			this.updateToolbar(0, "yellow-toolbar-selected");
-		}
-	},
-	
-	// Show or hide preview
-	showPreview: function(elementText, elementPreview)
-	{
-		if(!yellow.toolbox.isVisible(elementPreview))
-		{
-			var thisObject = this;
-			var formData = new FormData();
-			formData.append("action", "preview");
-			formData.append("csrftoken", this.getCookie("csrftoken"));
-			formData.append("rawdataedit", elementText.value);
-			formData.append("rawdataendofline", yellow.page.rawDataEndOfLine);
-			var request = new XMLHttpRequest();
-			request.open("POST", window.location.pathname, true);
-			request.onload = function() { if(this.status==200) thisObject.showPreviewDone.call(thisObject, elementText, elementPreview, this.responseText); };
-			request.send(formData);
-		} else {
-			this.showPreviewDone(elementText, elementPreview, "");
-		}
-	},
-	
-	// Preview done
-	showPreviewDone: function(elementText, elementPreview, responseText)
-	{
-		var showPreview = responseText.length!=0;
-		yellow.toolbox.setVisible(elementText, !showPreview);
-		yellow.toolbox.setVisible(elementPreview, showPreview);
-		if(showPreview)
-		{
-			this.updateToolbar("preview", "yellow-toolbar-checked");
-			elementPreview.innerHTML = responseText;
-			dispatchEvent(new Event("load"));
-		} else {
-			this.updateToolbar(0, "yellow-toolbar-checked");
-			elementText.focus();
-		}
-	},
-	
-	// Show file dialog and trigger upload
-	showFileDialog: function()
-	{
-		var element = document.createElement("input");
-		element.setAttribute("id", "yellow-file-dialog");
-		element.setAttribute("type", "file");
-		element.setAttribute("accept", yellow.config.editUploadExtensions);
-		element.setAttribute("multiple", "multiple");
-		yellow.toolbox.addEvent(element, "change", yellow.onDrop);
-		element.click();
-	},
-	
-	// Upload file
-	uploadFile: function(elementText, file)
-	{
-		var extension = (file.name.lastIndexOf(".")!=-1 ? file.name.substring(file.name.lastIndexOf("."), file.name.length) : "").toLowerCase();
-		var extensions = yellow.config.editUploadExtensions.split(/\s*,\s*/);
-		if(file.size<=yellow.config.serverFileSizeMax && extensions.indexOf(extension)!=-1)
-		{
-			var text = this.getText("UploadProgress")+"\u200b";
-			yellow.editor.setMarkdown(elementText, text, "insert");
-			var thisObject = this;
-			var formData = new FormData();
-			formData.append("action", "upload");
-			formData.append("csrftoken", this.getCookie("csrftoken"));
-			formData.append("file", file);
-			var request = new XMLHttpRequest();
-			request.open("POST", window.location.pathname, true);
-			request.onload = function() { if(this.status==200) { thisObject.uploadFileDone.call(thisObject, elementText, this.responseText); } else { thisObject.uploadFileError.call(thisObject, elementText, this.responseText); } };
-			request.send(formData);
-		}
-	},
-	
-	// Upload done
-	uploadFileDone: function(elementText, responseText)
-	{
-		var result = JSON.parse(responseText);
-		if(result)
-		{
-			var textOld = this.getText("UploadProgress")+"\u200b";
-			var textNew;
-			if(result.location.substring(0, yellow.config.imageLocation.length)==yellow.config.imageLocation)
-			{
-				textNew = "[image "+result.location.substring(yellow.config.imageLocation.length)+"]";
-			} else {
-				textNew = "[link]("+result.location+")";
-			}
-			yellow.editor.replace(elementText, textOld, textNew);
-		}
-	},
-	
-	// Upload error
-	uploadFileError: function(elementText, responseText)
-	{
-		var result = JSON.parse(responseText);
-		if(result)
-		{
-			var textOld = this.getText("UploadProgress")+"\u200b";
-			var textNew = "["+result.error+"]";
-			yellow.editor.replace(elementText, textOld, textNew);
-		}
-	},
+    // Send pane
+    sendPane: function(paneId, paneAction, paneStatus, paneArgs) {
+        if (yellow.config.debug) console.log("yellow.edit.sendPane id:"+paneId);
+        var args = { "action":paneAction, "csrftoken":this.getCookie("csrftoken") };
+        if (paneId=="yellow-pane-edit") {
+            args.action = this.getAction(paneId, paneAction);
+            args.rawdatasource = yellow.page.rawDataSource;
+            args.rawdataedit = document.getElementById("yellow-pane-edit-text").value;
+            args.rawdataendofline = yellow.page.rawDataEndOfLine;
+        }
+        if (paneArgs) {
+            var tokens = paneArgs.split("/");
+            for (var i=0; i<tokens.length; i++) {
+                var pair = tokens[i].split(/[:=]/);
+                if (!pair[0] || !pair[1]) continue;
+                args[pair[0]] = pair[1];
+            }
+        }
+        yellow.toolbox.submitForm(args);
+    },
+    
+    // Process help
+    processHelp: function() {
+        this.hidePane(this.paneId);
+        window.open(this.getText("HelpUrl", "yellow"), "_self");
+    },
+    
+    // Process shortcut
+    processShortcut: function(e) {
+        var shortcut = yellow.toolbox.getEventShortcut(e);
+        if (shortcut) {
+            var tokens = yellow.config.editKeyboardShortcuts.split(",");
+            for (var i=0; i<tokens.length; i++) {
+                var pair = tokens[i].trim().split(" ");
+                if (shortcut==pair[0] || shortcut.replace("meta+", "ctrl+")==pair[0]) {
+                    e.stopPropagation();
+                    e.preventDefault();
+                    this.processToolbar(pair[1]);
+                }
+            }
+        }
+    },
+    
+    // Process toolbar
+    processToolbar: function(status, args) {
+        if (yellow.config.debug) console.log("yellow.edit.processToolbar status:"+status);
+        var elementText = document.getElementById("yellow-pane-edit-text");
+        var elementPreview = document.getElementById("yellow-pane-edit-preview");
+        if (!yellow.config.userRestrictions && this.paneAction!="delete" && !yellow.toolbox.isVisible(elementPreview)) {
+            switch (status) {
+                case "h1":              yellow.editor.setMarkdown(elementText, "# ", "insert-multiline-block", true); break;
+                case "h2":              yellow.editor.setMarkdown(elementText, "## ", "insert-multiline-block", true); break;
+                case "h3":              yellow.editor.setMarkdown(elementText, "### ", "insert-multiline-block", true); break;
+                case "paragraph":       yellow.editor.setMarkdown(elementText, "", "remove-multiline-block");
+                                        yellow.editor.setMarkdown(elementText, "", "remove-fenced-block"); break;
+                case "quote":           yellow.editor.setMarkdown(elementText, "> ", "insert-multiline-block", true); break;
+                case "pre":             yellow.editor.setMarkdown(elementText, "```\n", "insert-fenced-block", true); break;
+                case "bold":            yellow.editor.setMarkdown(elementText, "**", "insert-inline", true); break;
+                case "italic":          yellow.editor.setMarkdown(elementText, "*", "insert-inline", true); break;
+                case "strikethrough":   yellow.editor.setMarkdown(elementText, "~~", "insert-inline", true); break;
+                case "code":            yellow.editor.setMarkdown(elementText, "`", "insert-autodetect", true); break;
+                case "ul":              yellow.editor.setMarkdown(elementText, "* ", "insert-multiline-block", true); break;
+                case "ol":              yellow.editor.setMarkdown(elementText, "1. ", "insert-multiline-block", true); break;
+                case "tl":              yellow.editor.setMarkdown(elementText, "- [ ] ", "insert-multiline-block", true); break;
+                case "link":            yellow.editor.setMarkdown(elementText, "[link](url)", "insert", false, yellow.editor.getMarkdownLink); break;
+                case "text":            yellow.editor.setMarkdown(elementText, args, "insert"); break;
+                case "draft":           yellow.editor.setMetaData(elementText, "status", "draft", true); break;
+                case "file":            this.showFileDialog(); break;
+                case "undo":            yellow.editor.undo(); break;
+                case "redo":            yellow.editor.redo(); break;
+            }
+        }
+        if (status=="preview") this.showPreview(elementText, elementPreview);
+        if (status=="save" && !yellow.config.userRestrictions && this.paneAction!="delete") this.action("send");
+        if (status=="help") window.open(this.getText("HelpUrl", "yellow"), "_blank");
+        if (status=="markdown") window.open(this.getText("MarkdownUrl", "yellow"), "_blank");
+        if (status=="format" || status=="heading" || status=="list" || status=="emojiawesome" || status=="fontawesome") {
+            this.showPopup("yellow-popup-"+status, status);
+        } else {
+            this.hidePopup(this.popupId);
+        }
+    },
+    
+    // Update toolbar
+    updateToolbar: function(status, name) {
+        if (status) {
+            var element = document.getElementById("yellow-toolbar-"+status);
+            if (element) yellow.toolbox.addClass(element, name);
+        } else {
+            var elements = document.getElementsByClassName(name);
+            for (var i=0, l=elements.length; i<l; i++) {
+                yellow.toolbox.removeClass(elements[i], name);
+            }
+        }
+    },
+    
+    // Create popup
+    createPopup: function(popupId) {
+        if (yellow.config.debug) console.log("yellow.edit.createPopup id:"+popupId);
+        var elementPopup = document.createElement("div");
+        elementPopup.className = "yellow-popup";
+        elementPopup.setAttribute("id", popupId);
+        elementPopup.style.display = "none";
+        var elementDiv = document.createElement("div");
+        elementDiv.setAttribute("id", popupId+"-content");
+        switch (popupId) {
+            case "yellow-popup-format":
+                elementDiv.innerHTML =
+                "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+
+                "<li><a href=\"#\" id=\"yellow-popup-format-h1\" data-action=\"toolbar\" data-status=\"h1\">"+this.getText("ToolbarH1")+"</a></li>"+
+                "<li><a href=\"#\" id=\"yellow-popup-format-h2\" data-action=\"toolbar\" data-status=\"h2\">"+this.getText("ToolbarH2")+"</a></li>"+
+                "<li><a href=\"#\" id=\"yellow-popup-format-h3\" data-action=\"toolbar\" data-status=\"h3\">"+this.getText("ToolbarH3")+"</a></li>"+
+                "<li><a href=\"#\" id=\"yellow-popup-format-paragraph\" data-action=\"toolbar\" data-status=\"paragraph\">"+this.getText("ToolbarParagraph")+"</a></li>"+
+                "<li><a href=\"#\" id=\"yellow-popup-format-pre\" data-action=\"toolbar\" data-status=\"pre\">"+this.getText("ToolbarPre")+"</a></li>"+
+                "<li><a href=\"#\" id=\"yellow-popup-format-quote\" data-action=\"toolbar\" data-status=\"quote\">"+this.getText("ToolbarQuote")+"</a></li>"+
+                "</ul>";
+                break;
+            case "yellow-popup-heading":
+                elementDiv.innerHTML =
+                "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+
+                "<li><a href=\"#\" id=\"yellow-popup-heading-h1\" data-action=\"toolbar\" data-status=\"h1\">"+this.getText("ToolbarH1")+"</a></li>"+
+                "<li><a href=\"#\" id=\"yellow-popup-heading-h2\" data-action=\"toolbar\" data-status=\"h2\">"+this.getText("ToolbarH2")+"</a></li>"+
+                "<li><a href=\"#\" id=\"yellow-popup-heading-h3\" data-action=\"toolbar\" data-status=\"h3\">"+this.getText("ToolbarH3")+"</a></li>"+
+                "</ul>";
+                break;
+            case "yellow-popup-list":
+                elementDiv.innerHTML =
+                "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+
+                "<li><a href=\"#\" id=\"yellow-popup-list-ul\" data-action=\"toolbar\" data-status=\"ul\">"+this.getText("ToolbarUl")+"</a></li>"+
+                "<li><a href=\"#\" id=\"yellow-popup-list-ol\" data-action=\"toolbar\" data-status=\"ol\">"+this.getText("ToolbarOl")+"</a></li>"+
+                "</ul>";
+                break;
+            case "yellow-popup-emojiawesome":
+                var rawDataEmojis = "";
+                if (yellow.config.emojiawesomeToolbarButtons && yellow.config.emojiawesomeToolbarButtons!="none") {
+                    var tokens = yellow.config.emojiawesomeToolbarButtons.split(" ");
+                    for (var i=0; i<tokens.length; i++) {
+                        var token = tokens[i].replace(/[\:]/g,"");
+                        var className = token.replace("+1", "plus1").replace("-1", "minus1").replace(/_/g, "-");
+                        rawDataEmojis += "<li><a href=\"#\" id=\"yellow-popup-list-"+yellow.toolbox.encodeHtml(token)+"\" data-action=\"toolbar\" data-status=\"text\" data-args=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"ea ea-"+yellow.toolbox.encodeHtml(className)+"\"></i></a></li>";
+                    }
+                }
+                elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataEmojis+"</ul>";
+                break;
+            case "yellow-popup-fontawesome":
+                var rawDataIcons = "";
+                if (yellow.config.fontawesomeToolbarButtons && yellow.config.fontawesomeToolbarButtons!="none") {
+                    var tokens = yellow.config.fontawesomeToolbarButtons.split(" ");
+                    for (var i=0; i<tokens.length; i++) {
+                        var token = tokens[i].replace(/[\:]/g,"");
+                        rawDataIcons += "<li><a href=\"#\" id=\"yellow-popup-list-"+yellow.toolbox.encodeHtml(token)+"\" data-action=\"toolbar\" data-status=\"text\" data-args=\":"+yellow.toolbox.encodeHtml(token)+":\"><i class=\"fa "+yellow.toolbox.encodeHtml(token)+"\"></i></a></li>";
+                    }
+                }
+                elementDiv.innerHTML = "<ul class=\"yellow-dropdown yellow-dropdown-menu\">"+rawDataIcons+"</ul>";
+                break;
+        }
+        elementPopup.appendChild(elementDiv);
+        yellow.toolbox.insertAfter(elementPopup, document.getElementsByTagName("body")[0].firstChild);
+        this.bindActions(elementPopup);
+    },
+    
+    // Show or hide popup
+    showPopup: function(popupId, status) {
+        if (this.popupId!=popupId) {
+            this.hidePopup(this.popupId);
+            if (!document.getElementById(popupId)) this.createPopup(popupId);
+            var element = document.getElementById(popupId);
+            if (yellow.config.debug) console.log("yellow.edit.showPopup id:"+popupId);
+            yellow.toolbox.setVisible(element, true);
+            this.popupId = popupId;
+            this.updateToolbar(status, "yellow-toolbar-selected");
+            var elementParent = document.getElementById("yellow-toolbar-"+status);
+            var popupLeft = yellow.toolbox.getOuterLeft(elementParent);
+            var popupTop = yellow.toolbox.getOuterTop(elementParent) + yellow.toolbox.getOuterHeight(elementParent) - 1;
+            yellow.toolbox.setOuterLeft(document.getElementById(popupId), popupLeft);
+            yellow.toolbox.setOuterTop(document.getElementById(popupId), popupTop);
+        } else {
+            this.hidePopup(this.popupId, true);
+        }
+    },
+    
+    // Hide popup
+    hidePopup: function(popupId, fadeout) {
+        var element = document.getElementById(popupId);
+        if (yellow.toolbox.isVisible(element)) {
+            yellow.toolbox.setVisible(element, false, fadeout);
+            this.popupId = 0;
+            this.updateToolbar(0, "yellow-toolbar-selected");
+        }
+    },
+    
+    // Show or hide preview
+    showPreview: function(elementText, elementPreview) {
+        if (!yellow.toolbox.isVisible(elementPreview)) {
+            var thisObject = this;
+            var formData = new FormData();
+            formData.append("action", "preview");
+            formData.append("csrftoken", this.getCookie("csrftoken"));
+            formData.append("rawdataedit", elementText.value);
+            formData.append("rawdataendofline", yellow.page.rawDataEndOfLine);
+            var request = new XMLHttpRequest();
+            request.open("POST", window.location.pathname, true);
+            request.onload = function() { if (this.status==200) thisObject.showPreviewDone.call(thisObject, elementText, elementPreview, this.responseText); };
+            request.send(formData);
+        } else {
+            this.showPreviewDone(elementText, elementPreview, "");
+        }
+    },
+    
+    // Preview done
+    showPreviewDone: function(elementText, elementPreview, responseText) {
+        var showPreview = responseText.length!=0;
+        yellow.toolbox.setVisible(elementText, !showPreview);
+        yellow.toolbox.setVisible(elementPreview, showPreview);
+        if (showPreview) {
+            this.updateToolbar("preview", "yellow-toolbar-checked");
+            elementPreview.innerHTML = responseText;
+            dispatchEvent(new Event("load"));
+        } else {
+            this.updateToolbar(0, "yellow-toolbar-checked");
+            elementText.focus();
+        }
+    },
+    
+    // Show file dialog and trigger upload
+    showFileDialog: function() {
+        var element = document.createElement("input");
+        element.setAttribute("id", "yellow-file-dialog");
+        element.setAttribute("type", "file");
+        element.setAttribute("accept", yellow.config.editUploadExtensions);
+        element.setAttribute("multiple", "multiple");
+        yellow.toolbox.addEvent(element, "change", yellow.onDrop);
+        element.click();
+    },
+    
+    // Upload file
+    uploadFile: function(elementText, file) {
+        var extension = (file.name.lastIndexOf(".")!=-1 ? file.name.substring(file.name.lastIndexOf("."), file.name.length) : "").toLowerCase();
+        var extensions = yellow.config.editUploadExtensions.split(/\s*,\s*/);
+        if (file.size<=yellow.config.serverFileSizeMax && extensions.indexOf(extension)!=-1) {
+            var text = this.getText("UploadProgress")+"\u200b";
+            yellow.editor.setMarkdown(elementText, text, "insert");
+            var thisObject = this;
+            var formData = new FormData();
+            formData.append("action", "upload");
+            formData.append("csrftoken", this.getCookie("csrftoken"));
+            formData.append("file", file);
+            var request = new XMLHttpRequest();
+            request.open("POST", window.location.pathname, true);
+            request.onload = function() { if (this.status==200) { thisObject.uploadFileDone.call(thisObject, elementText, this.responseText); } else { thisObject.uploadFileError.call(thisObject, elementText, this.responseText); } };
+            request.send(formData);
+        }
+    },
+    
+    // Upload done
+    uploadFileDone: function(elementText, responseText) {
+        var result = JSON.parse(responseText);
+        if (result) {
+            var textOld = this.getText("UploadProgress")+"\u200b";
+            var textNew;
+            if (result.location.substring(0, yellow.config.imageLocation.length)==yellow.config.imageLocation) {
+                textNew = "[image "+result.location.substring(yellow.config.imageLocation.length)+"]";
+            } else {
+                textNew = "[link]("+result.location+")";
+            }
+            yellow.editor.replace(elementText, textOld, textNew);
+        }
+    },
+    
+    // Upload error
+    uploadFileError: function(elementText, responseText) {
+        var result = JSON.parse(responseText);
+        if (result) {
+            var textOld = this.getText("UploadProgress")+"\u200b";
+            var textNew = "["+result.error+"]";
+            yellow.editor.replace(elementText, textOld, textNew);
+        }
+    },
 
-	// Bind actions to links
-	bindActions: function(element)
-	{
-		var elements = element.getElementsByTagName("a");
-		for(var i=0, l=elements.length; i<l; i++)
-		{
-			if(elements[i].getAttribute("data-action")) elements[i].onclick = yellow.onClickAction;
-			if(elements[i].getAttribute("data-action")=="toolbar") elements[i].onmousedown = function(e) { e.preventDefault() };
-		}
-	},
-	
-	// Return action
-	getAction: function(paneId, paneAction)
-	{
-		var action = "";
-		if(paneId=="yellow-pane-edit")
-		{
-			switch(paneAction)
-			{
-				case "create":	action = "create"; break;
-				case "edit":	action = document.getElementById("yellow-pane-edit-text").value.length!=0 ? "edit" : "delete"; break;
-				case "delete":	action = "delete"; break;
-			}
-			if(yellow.page.statusCode==434 && paneAction!="delete") action = "create";
-		}
-		return action;
-	},
-	
-	// Return request string
-	getRequest: function(key, prefix)
-	{
-		if(!prefix) prefix = "request";
-		key = prefix + yellow.toolbox.toUpperFirst(key);
-		return (key in yellow.page) ? yellow.page[key] : "";
-	},
+    // Bind actions to links
+    bindActions: function(element) {
+        var elements = element.getElementsByTagName("a");
+        for (var i=0, l=elements.length; i<l; i++) {
+            if (elements[i].getAttribute("data-action")) elements[i].onclick = yellow.onClickAction;
+            if (elements[i].getAttribute("data-action")=="toolbar") elements[i].onmousedown = function(e) { e.preventDefault(); };
+        }
+    },
+    
+    // Return action
+    getAction: function(paneId, paneAction) {
+        var action = "";
+        if (paneId=="yellow-pane-edit") {
+            switch (paneAction) {
+                case "create":  action = "create"; break;
+                case "edit":    action = document.getElementById("yellow-pane-edit-text").value.length!=0 ? "edit" : "delete"; break;
+                case "delete":  action = "delete"; break;
+            }
+            if (yellow.page.statusCode==434 && paneAction!="delete") action = "create";
+        }
+        return action;
+    },
+    
+    // Return request string
+    getRequest: function(key, prefix) {
+        if (!prefix) prefix = "request";
+        key = prefix + yellow.toolbox.toUpperFirst(key);
+        return (key in yellow.page) ? yellow.page[key] : "";
+    },
 
-	// Return text string
-	getText: function(key, prefix, postfix)
-	{
-		if(!prefix) prefix = "edit";
-		if(!postfix) postfix = "";
-		key = prefix + yellow.toolbox.toUpperFirst(key) + yellow.toolbox.toUpperFirst(postfix);
-		return (key in yellow.text) ? yellow.text[key] : "["+key+"]";
-	},
+    // Return text string
+    getText: function(key, prefix, postfix) {
+        if (!prefix) prefix = "edit";
+        if (!postfix) postfix = "";
+        key = prefix + yellow.toolbox.toUpperFirst(key) + yellow.toolbox.toUpperFirst(postfix);
+        return (key in yellow.text) ? yellow.text[key] : "["+key+"]";
+    },
 
-	// Return cookie string
-	getCookie: function(name)
-	{
-		return yellow.toolbox.getCookie(name);
-	},
+    // Return cookie string
+    getCookie: function(name) {
+        return yellow.toolbox.getCookie(name);
+    },
 
-	// Check if plugin exists
-	isPlugin: function(name)
-	{
-		return name in yellow.config.serverPlugins;
-	}
+    // Check if plugin exists
+    isPlugin: function(name) {
+        return name in yellow.config.serverPlugins;
+    }
 };
 
-yellow.editor =
-{
-	// Set Markdown formatting
-	setMarkdown: function(element, prefix, type, toggle, callback)
-	{
-		var information = this.getMarkdownInformation(element, prefix, type);
-		var selectionStart = (information.type.indexOf("block")!=-1) ? information.top : information.start;
-		var selectionEnd = (information.type.indexOf("block")!=-1) ? information.bottom : information.end;
-		if(information.found && toggle) information.type = information.type.replace("insert", "remove");
-		if(information.type=="remove-fenced-block" || information.type=="remove-inline")
-		{
-			selectionStart -= information.prefix.length; selectionEnd += information.prefix.length;
-		}
-		var text = information.text;
-		var textSelectionBefore = text.substring(0, selectionStart);
-		var textSelection = text.substring(selectionStart, selectionEnd);
-		var textSelectionAfter = text.substring(selectionEnd, text.length);
-		var textSelectionNew, selectionStartNew, selectionEndNew;
-		switch(information.type)
-		{
-			case "insert-multiline-block":
-				textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information);
-				selectionStartNew = information.start + this.getMarkdownDifference(textSelection, textSelectionNew, true);
-				selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew);
-				if(information.start==information.top && information.start!=information.end) selectionStartNew = information.top;
-				if(information.end==information.top && information.start!=information.end) selectionEndNew = information.top;
-				break;
-			case "remove-multiline-block":
-				textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information);
-				selectionStartNew = information.start + this.getMarkdownDifference(textSelection, textSelectionNew, true);
-				selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew);
-				if(selectionStartNew<=information.top) selectionStartNew = information.top;
-				if(selectionEndNew<=information.top) selectionEndNew = information.top;
-				break;
-			case "insert-fenced-block":
-				textSelectionNew = this.getMarkdownFencedBlock(textSelection, information);
-				selectionStartNew = information.start + information.prefix.length;
-				selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew) - information.prefix.length;
-				break;
-			case "remove-fenced-block":
-				textSelectionNew = this.getMarkdownFencedBlock(textSelection, information);
-				selectionStartNew = information.start - information.prefix.length;
-				selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew) + information.prefix.length;
-				break;
-			case "insert-inline":
-				textSelectionNew = information.prefix + textSelection + information.prefix;
-				selectionStartNew = information.start + information.prefix.length;
-				selectionEndNew = information.end + information.prefix.length;
-				break;
-			case "remove-inline":
-				textSelectionNew = text.substring(information.start, information.end);
-				selectionStartNew = information.start - information.prefix.length;
-				selectionEndNew = information.end - information.prefix.length;
-				break;
-			case "insert":
-				textSelectionNew = callback ? callback(textSelection, information) : information.prefix;
-				selectionStartNew = information.start + textSelectionNew.length;
-				selectionEndNew = selectionStartNew;
-		}
-		if(textSelection!=textSelectionNew || selectionStart!=selectionStartNew || selectionEnd!=selectionEndNew)
-		{
-			element.focus();
-			element.setSelectionRange(selectionStart, selectionEnd);
-			document.execCommand("insertText", false, textSelectionNew);
-			element.value = textSelectionBefore + textSelectionNew + textSelectionAfter;
-			element.setSelectionRange(selectionStartNew, selectionEndNew);
-		}
-		if(yellow.config.debug) console.log("yellow.editor.setMarkdown type:"+information.type);
-	},
-	
-	// Return Markdown formatting information
-	getMarkdownInformation: function(element, prefix, type)
-	{
-		var text = element.value;
-		var start = element.selectionStart;
-		var end = element.selectionEnd;
-		var top = start, bottom = end;
-		while(text.charAt(top-1)!="\n" && top>0) top--;
-		if(bottom==top && bottom<text.length) bottom++;
-		while(text.charAt(bottom-1)!="\n" && bottom<text.length) bottom++;
-		if(type=="insert-autodetect")
-		{
-			if(text.substring(start, end).indexOf("\n")!=-1)
-			{
-				type = "insert-fenced-block"; prefix = "```\n";
-			} else {
-				type = "insert-inline"; prefix = "`";
-			}
-		}
-		var found = false;
-		if(type.indexOf("multiline-block")!=-1)
-		{
-			if(text.substring(top, top+prefix.length)==prefix) found = true;
-		} else if(type.indexOf("fenced-block")!=-1) {
-			if(text.substring(top-prefix.length, top)==prefix && text.substring(bottom, bottom+prefix.length)==prefix)
-			{
-				found = true;
-			}
-		} else {
-			if(text.substring(start-prefix.length, start)==prefix && text.substring(end, end+prefix.length)==prefix)
-			{
-				if(prefix=="*")
-				{
-					var lettersBefore = 0, lettersAfter = 0;
-					for(var index=start-1; text.charAt(index)=="*"; index--) lettersBefore++;
-					for(var index=end; text.charAt(index)=="*"; index++) lettersAfter++;
-					found = lettersBefore!=2 && lettersAfter!=2;
-				} else {
-					found = true;
-				}
-			}
-		}
-		return { "text":text, "prefix":prefix, "type":type, "start":start, "end":end, "top":top, "bottom":bottom, "found":found };
-	},
-	
-	// Return Markdown length difference
-	getMarkdownDifference: function(textSelection, textSelectionNew, firstTextLine)
-	{
-		var textSelectionLength, textSelectionLengthNew;
-		if(firstTextLine)
-		{
-			var position = textSelection.indexOf("\n");
-			var positionNew = textSelectionNew.indexOf("\n");
-			textSelectionLength = position!=-1 ? position+1 : textSelection.length+1;
-			textSelectionLengthNew = positionNew!=-1 ? positionNew+1 : textSelectionNew.length+1;
-		} else {
-			var position = textSelection.indexOf("\n");
-			var positionNew = textSelectionNew.indexOf("\n");
-			textSelectionLength = position!=-1 ? textSelection.length : textSelection.length+1;
-			textSelectionLengthNew = positionNew!=-1 ? textSelectionNew.length : textSelectionNew.length+1;
-		}
-		return textSelectionLengthNew - textSelectionLength;
-	},
-	
-	// Return Markdown for multiline block
-	getMarkdownMultilineBlock: function(textSelection, information)
-	{
-		var textSelectionNew = "";
-		var lines = yellow.toolbox.getTextLines(textSelection);
-		for(var i=0; i<lines.length; i++)
-		{
-			var matches = lines[i].match(/^(\s*[\#\*\-\>\s]+)?(\s+\[.\]|\s*\d+\.)?[ \t]+/);
-			if(matches)
-			{
-				textSelectionNew += lines[i].substring(matches[0].length);
-			} else {
-				textSelectionNew += lines[i];
-			}
-		}
-		textSelection = textSelectionNew;
-		if(information.type.indexOf("remove")==-1)
-		{
-			textSelectionNew = "";
-			var linePrefix = information.prefix;
-			lines = yellow.toolbox.getTextLines(textSelection.length!=0 ? textSelection : "\n");
-			for(var i=0; i<lines.length; i++)
-			{
-				textSelectionNew += linePrefix+lines[i];
-				if(information.prefix=="1. ")
-				{
-					var matches = linePrefix.match(/^(\d+)\.\s/);
-					if(matches) linePrefix = (parseInt(matches[1])+1)+". ";
-				}
-			}
-			textSelection = textSelectionNew;
-		}
-		return textSelection;
-	},
-	
-	// Return Markdown for fenced block
-	getMarkdownFencedBlock: function(textSelection, information)
-	{
-		var textSelectionNew = "";
-		var lines = yellow.toolbox.getTextLines(textSelection);
-		for(var i=0; i<lines.length; i++)
-		{
-			var matches = lines[i].match(/^```/);
-			if(!matches) textSelectionNew += lines[i];
-		}
-		textSelection = textSelectionNew;
-		if(information.type.indexOf("remove")==-1)
-		{
-			if(textSelection.length==0) textSelection = "\n";
-			textSelection = information.prefix + textSelection + information.prefix;
-		}
-		return textSelection;
-	},
-	
-	// Return Markdown for link
-	getMarkdownLink: function(textSelection, information)
-	{
-		return textSelection.length!=0 ? information.prefix.replace("link", textSelection) : information.prefix;
-	},
-	
-	// Set meta data
-	setMetaData: function(element, key, value, toggle)
-	{
-		var information = this.getMetaDataInformation(element, key);
-		if(information.bottom!=0)
-		{
-			var selectionStart = information.found ? information.start : information.bottom;
-			var selectionEnd = information.found ? information.end : information.bottom;
-			var text = information.text;
-			var textSelectionBefore = text.substring(0, selectionStart);
-			var textSelection = text.substring(selectionStart, selectionEnd);
-			var textSelectionAfter = text.substring(selectionEnd, text.length);
-			var textSelectionNew = yellow.toolbox.toUpperFirst(key)+": "+value+"\n";
-			if(information.found && information.value==value && toggle) textSelectionNew = "";
-			var selectionStartNew = selectionStart;
-			var selectionEndNew = selectionStart + textSelectionNew.trim().length;
-			element.focus();
-			element.setSelectionRange(selectionStart, selectionEnd);
-			document.execCommand("insertText", false, textSelectionNew);
-			element.value = textSelectionBefore + textSelectionNew + textSelectionAfter;
-			element.setSelectionRange(selectionStartNew, selectionEndNew);
-			element.scrollTop = 0;
-			if(yellow.config.debug) console.log("yellow.editor.setMetaData key:"+key);
-		}
-	},
-	
-	// Return meta data information
-	getMetaDataInformation: function(element, key)
-	{
-		var text = element.value;
-		var value = "";
-		var start = 0, end = 0, top = 0, bottom = 0;
-		var found = false;
-		var parts = text.match(/^(\xEF\xBB\xBF)?(\-\-\-[\r\n]+)([\s\S]+?)\-\-\-[\r\n]+/);
-		if(parts)
-		{
-			key = yellow.toolbox.toLowerFirst(key);
-			start = end = top = ((parts[1] ? parts[1] : "")+parts[2]).length;
-			bottom = ((parts[1] ? parts[1] : "")+parts[2]+parts[3]).length;
-			var lines = yellow.toolbox.getTextLines(parts[3]);
-			for(var i=0; i<lines.length; i++)
-			{
-				var matches = lines[i].match(/^\s*(.*?)\s*:\s*(.*?)\s*$/);
-				if(matches && yellow.toolbox.toLowerFirst(matches[1])==key && matches[2].length!=0)
-				{
-					value = matches[2];
-					end = start + lines[i].length;
-					found = true;
-					break;
-				}
-				start = end = start + lines[i].length;
-			}
-		}
-		return { "text":text, "value":value, "start":start, "end":end, "top":top, "bottom":bottom, "found":found };
-	},
-	
-	// Replace text
-	replace: function(element, textOld, textNew)
-	{
-		var text = element.value;
-		var selectionStart = element.selectionStart;
-		var selectionEnd = element.selectionEnd;
-		var selectionStartFound = text.indexOf(textOld);
-		var selectionEndFound = selectionStartFound + textOld.length;
-		if(selectionStartFound!=-1)
-		{
-			var selectionStartNew = selectionStart<selectionStartFound ? selectionStart : selectionStart+textNew.length-textOld.length;
-			var selectionEndNew = selectionEnd<selectionEndFound ? selectionEnd : selectionEnd+textNew.length-textOld.length;
-			var textBefore = text.substring(0, selectionStartFound);
-			var textAfter = text.substring(selectionEndFound, text.length);
-			if(textOld!=textNew)
-			{
-				element.focus();
-				element.setSelectionRange(selectionStartFound, selectionEndFound);
-				document.execCommand("insertText", false, textNew);
-				element.value = textBefore + textNew + textAfter;
-				element.setSelectionRange(selectionStartNew, selectionEndNew);
-			}
-		}
-	},
-	
-	// Undo changes
-	undo: function()
-	{
-		document.execCommand("undo");
-	},
+yellow.editor = {
 
-	// Redo changes
-	redo: function()
-	{
-		document.execCommand("redo");
-	}
+    // Set Markdown formatting
+    setMarkdown: function(element, prefix, type, toggle, callback) {
+        var information = this.getMarkdownInformation(element, prefix, type);
+        var selectionStart = (information.type.indexOf("block")!=-1) ? information.top : information.start;
+        var selectionEnd = (information.type.indexOf("block")!=-1) ? information.bottom : information.end;
+        if (information.found && toggle) information.type = information.type.replace("insert", "remove");
+        if (information.type=="remove-fenced-block" || information.type=="remove-inline") {
+            selectionStart -= information.prefix.length; selectionEnd += information.prefix.length;
+        }
+        var text = information.text;
+        var textSelectionBefore = text.substring(0, selectionStart);
+        var textSelection = text.substring(selectionStart, selectionEnd);
+        var textSelectionAfter = text.substring(selectionEnd, text.length);
+        var textSelectionNew, selectionStartNew, selectionEndNew;
+        switch (information.type) {
+            case "insert-multiline-block":
+                textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information);
+                selectionStartNew = information.start + this.getMarkdownDifference(textSelection, textSelectionNew, true);
+                selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew);
+                if (information.start==information.top && information.start!=information.end) selectionStartNew = information.top;
+                if (information.end==information.top && information.start!=information.end) selectionEndNew = information.top;
+                break;
+            case "remove-multiline-block":
+                textSelectionNew = this.getMarkdownMultilineBlock(textSelection, information);
+                selectionStartNew = information.start + this.getMarkdownDifference(textSelection, textSelectionNew, true);
+                selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew);
+                if (selectionStartNew<=information.top) selectionStartNew = information.top;
+                if (selectionEndNew<=information.top) selectionEndNew = information.top;
+                break;
+            case "insert-fenced-block":
+                textSelectionNew = this.getMarkdownFencedBlock(textSelection, information);
+                selectionStartNew = information.start + information.prefix.length;
+                selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew) - information.prefix.length;
+                break;
+            case "remove-fenced-block":
+                textSelectionNew = this.getMarkdownFencedBlock(textSelection, information);
+                selectionStartNew = information.start - information.prefix.length;
+                selectionEndNew = information.end + this.getMarkdownDifference(textSelection, textSelectionNew) + information.prefix.length;
+                break;
+            case "insert-inline":
+                textSelectionNew = information.prefix + textSelection + information.prefix;
+                selectionStartNew = information.start + information.prefix.length;
+                selectionEndNew = information.end + information.prefix.length;
+                break;
+            case "remove-inline":
+                textSelectionNew = text.substring(information.start, information.end);
+                selectionStartNew = information.start - information.prefix.length;
+                selectionEndNew = information.end - information.prefix.length;
+                break;
+            case "insert":
+                textSelectionNew = callback ? callback(textSelection, information) : information.prefix;
+                selectionStartNew = information.start + textSelectionNew.length;
+                selectionEndNew = selectionStartNew;
+        }
+        if (textSelection!=textSelectionNew || selectionStart!=selectionStartNew || selectionEnd!=selectionEndNew) {
+            element.focus();
+            element.setSelectionRange(selectionStart, selectionEnd);
+            document.execCommand("insertText", false, textSelectionNew);
+            element.value = textSelectionBefore + textSelectionNew + textSelectionAfter;
+            element.setSelectionRange(selectionStartNew, selectionEndNew);
+        }
+        if (yellow.config.debug) console.log("yellow.editor.setMarkdown type:"+information.type);
+    },
+    
+    // Return Markdown formatting information
+    getMarkdownInformation: function(element, prefix, type) {
+        var text = element.value;
+        var start = element.selectionStart;
+        var end = element.selectionEnd;
+        var top = start, bottom = end;
+        while (text.charAt(top-1)!="\n" && top>0) top--;
+        if (bottom==top && bottom<text.length) bottom++;
+        while (text.charAt(bottom-1)!="\n" && bottom<text.length) bottom++;
+        if (type=="insert-autodetect") {
+            if (text.substring(start, end).indexOf("\n")!=-1) {
+                type = "insert-fenced-block"; prefix = "```\n";
+            } else {
+                type = "insert-inline"; prefix = "`";
+            }
+        }
+        var found = false;
+        if (type.indexOf("multiline-block")!=-1) {
+            if (text.substring(top, top+prefix.length)==prefix) found = true;
+        } else if (type.indexOf("fenced-block")!=-1) {
+            if (text.substring(top-prefix.length, top)==prefix && text.substring(bottom, bottom+prefix.length)==prefix) {
+                found = true;
+            }
+        } else {
+            if (text.substring(start-prefix.length, start)==prefix && text.substring(end, end+prefix.length)==prefix) {
+                if (prefix=="*") {
+                    var lettersBefore = 0, lettersAfter = 0;
+                    for (var index=start-1; text.charAt(index)=="*"; index--) lettersBefore++;
+                    for (var index=end; text.charAt(index)=="*"; index++) lettersAfter++;
+                    found = lettersBefore!=2 && lettersAfter!=2;
+                } else {
+                    found = true;
+                }
+            }
+        }
+        return { "text":text, "prefix":prefix, "type":type, "start":start, "end":end, "top":top, "bottom":bottom, "found":found };
+    },
+    
+    // Return Markdown length difference
+    getMarkdownDifference: function(textSelection, textSelectionNew, firstTextLine) {
+        var textSelectionLength, textSelectionLengthNew;
+        if (firstTextLine) {
+            var position = textSelection.indexOf("\n");
+            var positionNew = textSelectionNew.indexOf("\n");
+            textSelectionLength = position!=-1 ? position+1 : textSelection.length+1;
+            textSelectionLengthNew = positionNew!=-1 ? positionNew+1 : textSelectionNew.length+1;
+        } else {
+            var position = textSelection.indexOf("\n");
+            var positionNew = textSelectionNew.indexOf("\n");
+            textSelectionLength = position!=-1 ? textSelection.length : textSelection.length+1;
+            textSelectionLengthNew = positionNew!=-1 ? textSelectionNew.length : textSelectionNew.length+1;
+        }
+        return textSelectionLengthNew - textSelectionLength;
+    },
+    
+    // Return Markdown for multiline block
+    getMarkdownMultilineBlock: function(textSelection, information) {
+        var textSelectionNew = "";
+        var lines = yellow.toolbox.getTextLines(textSelection);
+        for (var i=0; i<lines.length; i++) {
+            var matches = lines[i].match(/^(\s*[\#\*\-\>\s]+)?(\s+\[.\]|\s*\d+\.)?[ \t]+/);
+            if (matches) {
+                textSelectionNew += lines[i].substring(matches[0].length);
+            } else {
+                textSelectionNew += lines[i];
+            }
+        }
+        textSelection = textSelectionNew;
+        if (information.type.indexOf("remove")==-1) {
+            textSelectionNew = "";
+            var linePrefix = information.prefix;
+            lines = yellow.toolbox.getTextLines(textSelection.length!=0 ? textSelection : "\n");
+            for (var i=0; i<lines.length; i++) {
+                textSelectionNew += linePrefix+lines[i];
+                if (information.prefix=="1. ") {
+                    var matches = linePrefix.match(/^(\d+)\.\s/);
+                    if (matches) linePrefix = (parseInt(matches[1])+1)+". ";
+                }
+            }
+            textSelection = textSelectionNew;
+        }
+        return textSelection;
+    },
+    
+    // Return Markdown for fenced block
+    getMarkdownFencedBlock: function(textSelection, information) {
+        var textSelectionNew = "";
+        var lines = yellow.toolbox.getTextLines(textSelection);
+        for (var i=0; i<lines.length; i++) {
+            var matches = lines[i].match(/^```/);
+            if (!matches) textSelectionNew += lines[i];
+        }
+        textSelection = textSelectionNew;
+        if (information.type.indexOf("remove")==-1) {
+            if (textSelection.length==0) textSelection = "\n";
+            textSelection = information.prefix + textSelection + information.prefix;
+        }
+        return textSelection;
+    },
+    
+    // Return Markdown for link
+    getMarkdownLink: function(textSelection, information) {
+        return textSelection.length!=0 ? information.prefix.replace("link", textSelection) : information.prefix;
+    },
+    
+    // Set meta data
+    setMetaData: function(element, key, value, toggle) {
+        var information = this.getMetaDataInformation(element, key);
+        if (information.bottom!=0) {
+            var selectionStart = information.found ? information.start : information.bottom;
+            var selectionEnd = information.found ? information.end : information.bottom;
+            var text = information.text;
+            var textSelectionBefore = text.substring(0, selectionStart);
+            var textSelection = text.substring(selectionStart, selectionEnd);
+            var textSelectionAfter = text.substring(selectionEnd, text.length);
+            var textSelectionNew = yellow.toolbox.toUpperFirst(key)+": "+value+"\n";
+            if (information.found && information.value==value && toggle) textSelectionNew = "";
+            var selectionStartNew = selectionStart;
+            var selectionEndNew = selectionStart + textSelectionNew.trim().length;
+            element.focus();
+            element.setSelectionRange(selectionStart, selectionEnd);
+            document.execCommand("insertText", false, textSelectionNew);
+            element.value = textSelectionBefore + textSelectionNew + textSelectionAfter;
+            element.setSelectionRange(selectionStartNew, selectionEndNew);
+            element.scrollTop = 0;
+            if (yellow.config.debug) console.log("yellow.editor.setMetaData key:"+key);
+        }
+    },
+    
+    // Return meta data information
+    getMetaDataInformation: function(element, key) {
+        var text = element.value;
+        var value = "";
+        var start = 0, end = 0, top = 0, bottom = 0;
+        var found = false;
+        var parts = text.match(/^(\xEF\xBB\xBF)?(\-\-\-[\r\n]+)([\s\S]+?)\-\-\-[\r\n]+/);
+        if (parts) {
+            key = yellow.toolbox.toLowerFirst(key);
+            start = end = top = ((parts[1] ? parts[1] : "")+parts[2]).length;
+            bottom = ((parts[1] ? parts[1] : "")+parts[2]+parts[3]).length;
+            var lines = yellow.toolbox.getTextLines(parts[3]);
+            for (var i=0; i<lines.length; i++) {
+                var matches = lines[i].match(/^\s*(.*?)\s*:\s*(.*?)\s*$/);
+                if (matches && yellow.toolbox.toLowerFirst(matches[1])==key && matches[2].length!=0) {
+                    value = matches[2];
+                    end = start + lines[i].length;
+                    found = true;
+                    break;
+                }
+                start = end = start + lines[i].length;
+            }
+        }
+        return { "text":text, "value":value, "start":start, "end":end, "top":top, "bottom":bottom, "found":found };
+    },
+    
+    // Replace text
+    replace: function(element, textOld, textNew) {
+        var text = element.value;
+        var selectionStart = element.selectionStart;
+        var selectionEnd = element.selectionEnd;
+        var selectionStartFound = text.indexOf(textOld);
+        var selectionEndFound = selectionStartFound + textOld.length;
+        if (selectionStartFound!=-1) {
+            var selectionStartNew = selectionStart<selectionStartFound ? selectionStart : selectionStart+textNew.length-textOld.length;
+            var selectionEndNew = selectionEnd<selectionEndFound ? selectionEnd : selectionEnd+textNew.length-textOld.length;
+            var textBefore = text.substring(0, selectionStartFound);
+            var textAfter = text.substring(selectionEndFound, text.length);
+            if (textOld!=textNew) {
+                element.focus();
+                element.setSelectionRange(selectionStartFound, selectionEndFound);
+                document.execCommand("insertText", false, textNew);
+                element.value = textBefore + textNew + textAfter;
+                element.setSelectionRange(selectionStartNew, selectionEndNew);
+            }
+        }
+    },
+    
+    // Undo changes
+    undo: function() {
+        document.execCommand("undo");
+    },
+
+    // Redo changes
+    redo: function() {
+        document.execCommand("redo");
+    }
 };
 
-yellow.toolbox =
-{
-	// Insert element before reference element
-	insertBefore: function(element, elementReference)
-	{
-		elementReference.parentNode.insertBefore(element, elementReference);
-	},
+yellow.toolbox = {
+
+    // Insert element before reference element
+    insertBefore: function(element, elementReference) {
+        elementReference.parentNode.insertBefore(element, elementReference);
+    },
 
-	// Insert element after reference element
-	insertAfter: function(element, elementReference)
-	{
-		elementReference.parentNode.insertBefore(element, elementReference.nextSibling);
-	},
+    // Insert element after reference element
+    insertAfter: function(element, elementReference) {
+        elementReference.parentNode.insertBefore(element, elementReference.nextSibling);
+    },
 
-	// Add element class
-	addClass: function(element, name)
-	{
-		element.classList.add(name);
-	},
-	
-	// Remove element class
-	removeClass: function(element, name)
-	{
-		element.classList.remove(name);
-	},
+    // Add element class
+    addClass: function(element, name) {
+        element.classList.add(name);
+    },
+    
+    // Remove element class
+    removeClass: function(element, name) {
+        element.classList.remove(name);
+    },
 
-	// Add attribute information
-	addValue: function(selector, name, value)
-	{
-		var element = document.querySelector(selector);
-		element.setAttribute(name, element.getAttribute(name) + value);
-	},
+    // Add attribute information
+    addValue: function(selector, name, value) {
+        var element = document.querySelector(selector);
+        element.setAttribute(name, element.getAttribute(name) + value);
+    },
 
-	// Remove attribute information
-	removeValue: function(selector, name, value)
-	{
-		var element = document.querySelector(selector);
-		element.setAttribute(name, element.getAttribute(name).replace(value, ""));
-	},
-	
-	// Add event handler
-	addEvent: function(element, type, handler)
-	{
-		element.addEventListener(type, handler, false);
-	},
-	
-	// Remove event handler
-	removeEvent: function(element, type, handler)
-	{
-		element.removeEventListener(type, handler, false);
-	},
-	
-	// Return shortcut from keyboard event, alphanumeric only
-	getEventShortcut: function(e)
-	{
-		var shortcut = "";
-		if(e.keyCode>=48 && e.keyCode<=90)
-		{
-			shortcut += (e.ctrlKey ? "ctrl+" : "")+(e.metaKey ? "meta+" : "")+(e.altKey ? "alt+" : "")+(e.shiftKey ? "shift+" : "");
-			shortcut += String.fromCharCode(e.keyCode).toLowerCase();
-		}
-		return shortcut;
-	},
-	
-	// Return element width in pixel
-	getWidth: function(element)
-	{
-		return element.offsetWidth - this.getBoxSize(element).width;
-	},
-	
-	// Return element height in pixel
-	getHeight: function(element)
-	{
-		return element.offsetHeight - this.getBoxSize(element).height;
-	},
-	
-	// Set element width in pixel, including padding and border
-	setOuterWidth: function(element, width)
-	{
-		element.style.width = Math.max(0, width - this.getBoxSize(element).width) + "px";
-	},
-	
-	// Set element height in pixel, including padding and border
-	setOuterHeight: function(element, height)
-	{
-		element.style.height = Math.max(0, height - this.getBoxSize(element).height) + "px";
-	},
-	
-	// Return element width in pixel, including padding and border
-	getOuterWidth: function(element, includeMargin)
-	{
-		var width = element.offsetWidth;
-		if(includeMargin) width += this.getMarginSize(element).width;
-		return width;
-	},
+    // Remove attribute information
+    removeValue: function(selector, name, value) {
+        var element = document.querySelector(selector);
+        element.setAttribute(name, element.getAttribute(name).replace(value, ""));
+    },
+    
+    // Add event handler
+    addEvent: function(element, type, handler) {
+        element.addEventListener(type, handler, false);
+    },
+    
+    // Remove event handler
+    removeEvent: function(element, type, handler) {
+        element.removeEventListener(type, handler, false);
+    },
+    
+    // Return shortcut from keyboard event, alphanumeric only
+    getEventShortcut: function(e) {
+        var shortcut = "";
+        if (e.keyCode>=48 && e.keyCode<=90) {
+            shortcut += (e.ctrlKey ? "ctrl+" : "")+(e.metaKey ? "meta+" : "")+(e.altKey ? "alt+" : "")+(e.shiftKey ? "shift+" : "");
+            shortcut += String.fromCharCode(e.keyCode).toLowerCase();
+        }
+        return shortcut;
+    },
+    
+    // Return element width in pixel
+    getWidth: function(element) {
+        return element.offsetWidth - this.getBoxSize(element).width;
+    },
+    
+    // Return element height in pixel
+    getHeight: function(element) {
+        return element.offsetHeight - this.getBoxSize(element).height;
+    },
+    
+    // Set element width in pixel, including padding and border
+    setOuterWidth: function(element, width) {
+        element.style.width = Math.max(0, width - this.getBoxSize(element).width) + "px";
+    },
+    
+    // Set element height in pixel, including padding and border
+    setOuterHeight: function(element, height) {
+        element.style.height = Math.max(0, height - this.getBoxSize(element).height) + "px";
+    },
+    
+    // Return element width in pixel, including padding and border
+    getOuterWidth: function(element, includeMargin) {
+        var width = element.offsetWidth;
+        if (includeMargin) width += this.getMarginSize(element).width;
+        return width;
+    },
 
-	// Return element height in pixel, including padding and border
-	getOuterHeight: function(element, includeMargin)
-	{
-		var height = element.offsetHeight;
-		if(includeMargin) height += this.getMarginSize(element).height;
-		return height;
-	},
-	
-	// Set element left position in pixel
-	setOuterLeft: function(element, left)
-	{
-		element.style.left = Math.max(0, left) + "px";
-	},
-	
-	// Set element top position in pixel
-	setOuterTop: function(element, top)
-	{
-		element.style.top = Math.max(0, top) + "px";
-	},
-	
-	// Return element left position in pixel
-	getOuterLeft: function(element)
-	{
-		return element.getBoundingClientRect().left + window.pageXOffset;
-	},
-	
-	// Return element top position in pixel
-	getOuterTop: function(element)
-	{
-		return element.getBoundingClientRect().top + window.pageYOffset;
-	},
-	
-	// Return window width in pixel
-	getWindowWidth: function()
-	{
-		return window.innerWidth;
-	},
-	
-	// Return window height in pixel
-	getWindowHeight: function()
-	{
-		return window.innerHeight;
-	},
-	
-	// Return element CSS property
-	getStyle: function(element, property)
-	{
-		return window.getComputedStyle(element).getPropertyValue(property);
-	},
-	
-	// Return element CSS padding and border
-	getBoxSize: function(element)
-	{
-		var paddingLeft = parseFloat(this.getStyle(element, "padding-left")) || 0;
-		var paddingRight = parseFloat(this.getStyle(element, "padding-right")) || 0;
-		var borderLeft = parseFloat(this.getStyle(element, "border-left-width")) || 0;
-		var borderRight = parseFloat(this.getStyle(element, "border-right-width")) || 0;
-		var width = paddingLeft + paddingRight + borderLeft + borderRight;
-		var paddingTop = parseFloat(this.getStyle(element, "padding-top")) || 0;
-		var paddingBottom = parseFloat(this.getStyle(element, "padding-bottom")) || 0;
-		var borderTop = parseFloat(this.getStyle(element, "border-top-width")) || 0;
-		var borderBottom = parseFloat(this.getStyle(element, "border-bottom-width")) || 0;
-		var height = paddingTop + paddingBottom + borderTop + borderBottom;
-		return { "width":width, "height":height };
-	},
-	
-	// Return element CSS margin
-	getMarginSize: function(element)
-	{
-		var marginLeft = parseFloat(this.getStyle(element, "margin-left")) || 0;
-		var marginRight = parseFloat(this.getStyle(element, "margin-right")) || 0;
-		var width = marginLeft + marginRight;
-		var marginTop = parseFloat(this.getStyle(element, "margin-top")) || 0;
-		var marginBottom = parseFloat(this.getStyle(element, "margin-bottom")) || 0;
-		var height = marginTop + marginBottom;
-		return { "width":width, "height":height };
-	},
-	
-	// Set element visibility
-	setVisible: function(element, show, fadeout)
-	{
-		if(fadeout && !show)
-		{
-			var opacity = 1;
-			function renderFrame()
-			{
-				opacity -= .1;
-				if(opacity<=0)
-				{
-					element.style.opacity = "initial";
-					element.style.display = "none";
-				} else {
-					element.style.opacity = opacity;
-					requestAnimationFrame(renderFrame);
-				}
-			}
-			renderFrame();
-		} else {
-			element.style.display = show ? "block" : "none";
-		}
-	},
+    // Return element height in pixel, including padding and border
+    getOuterHeight: function(element, includeMargin) {
+        var height = element.offsetHeight;
+        if (includeMargin) height += this.getMarginSize(element).height;
+        return height;
+    },
+    
+    // Set element left position in pixel
+    setOuterLeft: function(element, left) {
+        element.style.left = Math.max(0, left) + "px";
+    },
+    
+    // Set element top position in pixel
+    setOuterTop: function(element, top) {
+        element.style.top = Math.max(0, top) + "px";
+    },
+    
+    // Return element left position in pixel
+    getOuterLeft: function(element) {
+        return element.getBoundingClientRect().left + window.pageXOffset;
+    },
+    
+    // Return element top position in pixel
+    getOuterTop: function(element) {
+        return element.getBoundingClientRect().top + window.pageYOffset;
+    },
+    
+    // Return window width in pixel
+    getWindowWidth: function() {
+        return window.innerWidth;
+    },
+    
+    // Return window height in pixel
+    getWindowHeight: function() {
+        return window.innerHeight;
+    },
+    
+    // Return element CSS property
+    getStyle: function(element, property) {
+        return window.getComputedStyle(element).getPropertyValue(property);
+    },
+    
+    // Return element CSS padding and border
+    getBoxSize: function(element) {
+        var paddingLeft = parseFloat(this.getStyle(element, "padding-left")) || 0;
+        var paddingRight = parseFloat(this.getStyle(element, "padding-right")) || 0;
+        var borderLeft = parseFloat(this.getStyle(element, "border-left-width")) || 0;
+        var borderRight = parseFloat(this.getStyle(element, "border-right-width")) || 0;
+        var width = paddingLeft + paddingRight + borderLeft + borderRight;
+        var paddingTop = parseFloat(this.getStyle(element, "padding-top")) || 0;
+        var paddingBottom = parseFloat(this.getStyle(element, "padding-bottom")) || 0;
+        var borderTop = parseFloat(this.getStyle(element, "border-top-width")) || 0;
+        var borderBottom = parseFloat(this.getStyle(element, "border-bottom-width")) || 0;
+        var height = paddingTop + paddingBottom + borderTop + borderBottom;
+        return { "width":width, "height":height };
+    },
+    
+    // Return element CSS margin
+    getMarginSize: function(element) {
+        var marginLeft = parseFloat(this.getStyle(element, "margin-left")) || 0;
+        var marginRight = parseFloat(this.getStyle(element, "margin-right")) || 0;
+        var width = marginLeft + marginRight;
+        var marginTop = parseFloat(this.getStyle(element, "margin-top")) || 0;
+        var marginBottom = parseFloat(this.getStyle(element, "margin-bottom")) || 0;
+        var height = marginTop + marginBottom;
+        return { "width":width, "height":height };
+    },
+    
+    // Set element visibility
+    setVisible: function(element, show, fadeout) {
+        if (fadeout && !show) {
+            var opacity = 1;
+            function renderFrame() {
+                opacity -= .1;
+                if (opacity<=0) {
+                    element.style.opacity = "initial";
+                    element.style.display = "none";
+                } else {
+                    element.style.opacity = opacity;
+                    requestAnimationFrame(renderFrame);
+                }
+            }
+            renderFrame();
+        } else {
+            element.style.display = show ? "block" : "none";
+        }
+    },
 
-	// Check if element exists and is visible
-	isVisible: function(element)
-	{
-		return element && element.style.display!="none";
-	},
-	
-	// Convert first letter to lowercase
-	toLowerFirst: function(string)
-	{
-		return string.charAt(0).toLowerCase()+string.slice(1);
-	},
+    // Check if element exists and is visible
+    isVisible: function(element) {
+        return element && element.style.display!="none";
+    },
+    
+    // Convert first letter to lowercase
+    toLowerFirst: function(string) {
+        return string.charAt(0).toLowerCase()+string.slice(1);
+    },
 
-	// Convert first letter to uppercase
-	toUpperFirst: function(string)
-	{
-		return string.charAt(0).toUpperCase()+string.slice(1);
-	},
-	
-	// Return lines from text string, including newline
-	getTextLines: function(string)
-	{
-		var lines = string.split("\n");
-		for(var i=0; i<lines.length; i++) lines[i] = lines[i]+"\n";
-		if(string.length==0 || string.charAt(string.length-1)=="\n") lines.pop();
-		return lines;
-	},
-	
-	// Return cookie string
-	getCookie: function(name)
-	{
-		var matches = document.cookie.match("(^|; )"+name+"=([^;]+)");
-		return matches ? unescape(matches[2]) : "";
-	},
-	
-	// Encode HTML special characters
-	encodeHtml: function(string)
-	{
-		return string
-			.replace(/&/g, "&amp;")
-			.replace(/</g, "&lt;")
-			.replace(/>/g, "&gt;")
-			.replace(/"/g, "&quot;");
-	},
-	
-	// Submit form with post method
-	submitForm: function(args)
-	{
-		var elementForm = document.createElement("form");
-		elementForm.setAttribute("method", "post");
-		for(var key in args)
-		{
-			if(!args.hasOwnProperty(key)) continue;
-			var elementInput = document.createElement("input");
-			elementInput.setAttribute("type", "hidden");
-			elementInput.setAttribute("name", key);
-			elementInput.setAttribute("value", args[key]);
-			elementForm.appendChild(elementInput);
-		}
-		document.body.appendChild(elementForm);
-		elementForm.submit();
-	}
+    // Convert first letter to uppercase
+    toUpperFirst: function(string) {
+        return string.charAt(0).toUpperCase()+string.slice(1);
+    },
+    
+    // Return lines from text string, including newline
+    getTextLines: function(string) {
+        var lines = string.split("\n");
+        for (var i=0; i<lines.length; i++) lines[i] = lines[i]+"\n";
+        if (string.length==0 || string.charAt(string.length-1)=="\n") lines.pop();
+        return lines;
+    },
+    
+    // Return cookie string
+    getCookie: function(name) {
+        var matches = document.cookie.match("(^|; )"+name+"=([^;]+)");
+        return matches ? unescape(matches[2]) : "";
+    },
+    
+    // Encode HTML special characters
+    encodeHtml: function(string) {
+        return string
+            .replace(/&/g, "&amp;")
+            .replace(/</g, "&lt;")
+            .replace(/>/g, "&gt;")
+            .replace(/"/g, "&quot;");
+    },
+    
+    // Submit form with post method
+    submitForm: function(args) {
+        var elementForm = document.createElement("form");
+        elementForm.setAttribute("method", "post");
+        for (var key in args) {
+            if (!args.hasOwnProperty(key)) continue;
+            var elementInput = document.createElement("input");
+            elementInput.setAttribute("type", "hidden");
+            elementInput.setAttribute("name", key);
+            elementInput.setAttribute("value", args[key]);
+            elementForm.appendChild(elementInput);
+        }
+        document.body.appendChild(elementForm);
+        elementForm.submit();
+    }
 };
 
 yellow.edit.intervalId = setInterval("yellow.onLoad()", 1);

+ 1741 - 1999
system/plugins/edit.php

@@ -3,2035 +3,1777 @@
 // Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
-class YellowEdit
-{
-	const VERSION = "0.7.27";
-	var $yellow;			//access to API
-	var $response;			//web response
-	var $users;				//user accounts
-	var $merge;				//text merge
+class YellowEdit {
+    const VERSION = "0.7.27";
+    public $yellow;         //access to API
+    public $response;       //web response
+    public $users;          //user accounts
+    public $merge;          //text merge
 
-	// Handle initialisation
-	function onLoad($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->response = new YellowResponse($yellow);
-		$this->users = new YellowUsers($yellow);
-		$this->merge = new YellowMerge($yellow);
-		$this->yellow->config->setDefault("editLocation", "/edit/");
-		$this->yellow->config->setDefault("editUploadNewLocation", "/media/@group/@filename");
-		$this->yellow->config->setDefault("editUploadExtensions", ".gif, .jpg, .pdf, .png, .svg, .tgz, .zip");
-		$this->yellow->config->setDefault("editKeyboardShortcuts", "ctrl+b bold, ctrl+i italic, ctrl+e code, ctrl+k link, ctrl+s save, ctrl+shift+p preview");
-		$this->yellow->config->setDefault("editToolbarButtons", "auto");
-		$this->yellow->config->setDefault("editEndOfLine", "auto");
-		$this->yellow->config->setDefault("editUserFile", "user.ini");
-		$this->yellow->config->setDefault("editUserPasswordMinLength", "8");
-		$this->yellow->config->setDefault("editUserHashAlgorithm", "bcrypt");
-		$this->yellow->config->setDefault("editUserHashCost", "10");
-		$this->yellow->config->setDefault("editUserHome", "/");
-		$this->yellow->config->setDefault("editLoginRestrictions", "0");
-		$this->yellow->config->setDefault("editLoginSessionTimeout", "2592000");
-		$this->yellow->config->setDefault("editBruteForceProtection", "25");
-		$this->users->load($this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"));
-	}
+    // Handle initialisation
+    public function onLoad($yellow) {
+        $this->yellow = $yellow;
+        $this->response = new YellowResponse($yellow);
+        $this->users = new YellowUsers($yellow);
+        $this->merge = new YellowMerge($yellow);
+        $this->yellow->config->setDefault("editLocation", "/edit/");
+        $this->yellow->config->setDefault("editUploadNewLocation", "/media/@group/@filename");
+        $this->yellow->config->setDefault("editUploadExtensions", ".gif, .jpg, .pdf, .png, .svg, .tgz, .zip");
+        $this->yellow->config->setDefault("editKeyboardShortcuts", "ctrl+b bold, ctrl+i italic, ctrl+e code, ctrl+k link, ctrl+s save, ctrl+shift+p preview");
+        $this->yellow->config->setDefault("editToolbarButtons", "auto");
+        $this->yellow->config->setDefault("editEndOfLine", "auto");
+        $this->yellow->config->setDefault("editUserFile", "user.ini");
+        $this->yellow->config->setDefault("editUserPasswordMinLength", "8");
+        $this->yellow->config->setDefault("editUserHashAlgorithm", "bcrypt");
+        $this->yellow->config->setDefault("editUserHashCost", "10");
+        $this->yellow->config->setDefault("editUserHome", "/");
+        $this->yellow->config->setDefault("editLoginRestrictions", "0");
+        $this->yellow->config->setDefault("editLoginSessionTimeout", "2592000");
+        $this->yellow->config->setDefault("editBruteForceProtection", "25");
+        $this->users->load($this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile"));
+    }
 
-	// Handle startup
-	function onStartup($update)
-	{
-		if($update)
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$fileData = $this->yellow->toolbox->readFile($fileNameUser);
-			foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-			{
-				preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-				if(!empty($matches[1]) && !empty($matches[2]) && $matches[1][0]!='#')
-				{
-					list($hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) = explode(',', $matches[2]);
-					if($errors=="none") { $home=$pending; $pending=$errors; $errors=$modified; $modified=$stamp; $stamp=""; } //TODO: remove later
-					if(strlenb($stamp)!=20) $stamp=$this->users->createStamp(); //TODO: remove later, converts old file format
-					if($status!="active" && $status!="inactive") { unset($this->users->users[$matches[1]]); continue; }
-					$pending = "none";
-					$this->users->set($matches[1], $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home);
-					$fileDataNew .= "$matches[1]: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n";
-				} else {
-					$fileDataNew .= $line;
-				}
-			}
-			if($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileNameUser, $fileDataNew);
-		}
-	}
-	
-	// Handle request
-	function onRequest($scheme, $address, $base, $location, $fileName)
-	{
-		$statusCode = 0;
-		if($this->checkRequest($location))
-		{
-			$scheme = $this->yellow->config->get("serverScheme");
-			$address = $this->yellow->config->get("serverAddress");
-			$base = rtrim($this->yellow->config->get("serverBase").$this->yellow->config->get("editLocation"), '/');
-			list($scheme, $address, $base, $location, $fileName) = $this->yellow->getRequestInformation($scheme, $address, $base);
-			$this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName);
-			$statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName);
-		}
-		return $statusCode;
-	}
-	
-	// Handle page meta data parsing
-	function onParseMeta($page)
-	{
-		if($page==$this->yellow->page && $this->response->isActive())
-		{
-			if($this->response->isUser())
-			{
-				if(empty($this->response->rawDataSource)) $this->response->rawDataSource = $page->rawData;
-				if(empty($this->response->rawDataEdit)) $this->response->rawDataEdit = $page->rawData;
-				if(empty($this->response->rawDataEndOfLine)) $this->response->rawDataEndOfLine = $this->response->getEndOfLine($page->rawData);
-				if($page->statusCode==434) $this->response->rawDataEdit = $this->response->getRawDataNew($page->location);
-			}
-			if(empty($this->response->language)) $this->response->language = $page->get("language");
-			if(empty($this->response->action)) $this->response->action = $this->response->isUser() ? "none" : "login";
-			if(empty($this->response->status)) $this->response->status = "none";
-			if($this->response->status=="error") $this->response->action = "error";
-		}
-	}
-	
-	// Handle page content parsing of custom block
-	function onParseContentBlock($page, $name, $text, $shortcut)
-	{
-		$output = null;
-		if($name=="edit" && $shortcut)
-		{
-			$editText = "$name $text";
-			if(substru($text, 0, 2)=="- ") $editText = trim(substru($text, 2));
-			$output = "<a href=\"".$page->get("pageEdit")."\">".htmlspecialchars($editText)."</a>";
-		}
-		return $output;
-	}
-	
-	// Handle page extra HTML data
-	function onExtra($name)
-	{
-		$output = null;
-		if($name=="header" && $this->response->isActive())
-		{
-			$pluginLocation = $this->yellow->config->get("serverBase").$this->yellow->config->get("pluginLocation");
-			$output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"{$pluginLocation}edit.css\" />\n";
-			$output .= "<script type=\"text/javascript\" src=\"{$pluginLocation}edit.js\"></script>\n";
-			$output .= "<script type=\"text/javascript\">\n";
-			$output .= "// <![CDATA[\n";
-			$output .= "yellow.page = ".json_encode($this->response->getPageData()).";\n";
-			$output .= "yellow.config = ".json_encode($this->response->getConfigData()).";\n";
-			$output .= "yellow.text = ".json_encode($this->response->getTextData()).";\n";
-			$output .= "// ]]>\n";
-			$output .= "</script>\n";
-		}
-		return $output;
-	}
-	
-	// Handle command
-	function onCommand($args)
-	{
-		list($command) = $args;
-		switch($command)
-		{
-			case "user":	$statusCode = $this->userCommand($args); break;
-			default:		$statusCode = 0;
-		}
-		return $statusCode;
-	}
-	
-	// Handle command help
-	function onCommandHelp()
-	{
-		return "user [option email password name]\n";
-	}
+    // Handle startup
+    public function onStartup($update) {
+        if ($update) {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $fileData = $this->yellow->toolbox->readFile($fileNameUser);
+            foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+                preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+                if (!empty($matches[1]) && !empty($matches[2]) && $matches[1][0]!="#") {
+                    list($hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) = explode(",", $matches[2]);
+                    if ($errors=="none") { $home=$pending; $pending=$errors; $errors=$modified; $modified=$stamp; $stamp=""; } //TODO: remove later
+                    if (strlenb($stamp)!=20) $stamp=$this->users->createStamp(); //TODO: remove later, converts old file format
+                    if ($status!="active" && $status!="inactive") {
+                        unset($this->users->users[$matches[1]]);
+                        continue;
+                    }
+                    $pending = "none";
+                    $this->users->set($matches[1], $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home);
+                    $fileDataNew .= "$matches[1]: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n";
+                } else {
+                    $fileDataNew .= $line;
+                }
+            }
+            if ($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileNameUser, $fileDataNew);
+        }
+    }
+    
+    // Handle request
+    public function onRequest($scheme, $address, $base, $location, $fileName) {
+        $statusCode = 0;
+        if ($this->checkRequest($location)) {
+            $scheme = $this->yellow->config->get("serverScheme");
+            $address = $this->yellow->config->get("serverAddress");
+            $base = rtrim($this->yellow->config->get("serverBase").$this->yellow->config->get("editLocation"), "/");
+            list($scheme, $address, $base, $location, $fileName) = $this->yellow->getRequestInformation($scheme, $address, $base);
+            $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName);
+            $statusCode = $this->processRequest($scheme, $address, $base, $location, $fileName);
+        }
+        return $statusCode;
+    }
+    
+    // Handle page meta data parsing
+    public function onParseMeta($page) {
+        if ($page==$this->yellow->page && $this->response->isActive()) {
+            if ($this->response->isUser()) {
+                if (empty($this->response->rawDataSource)) $this->response->rawDataSource = $page->rawData;
+                if (empty($this->response->rawDataEdit)) $this->response->rawDataEdit = $page->rawData;
+                if (empty($this->response->rawDataEndOfLine)) $this->response->rawDataEndOfLine = $this->response->getEndOfLine($page->rawData);
+                if ($page->statusCode==434) $this->response->rawDataEdit = $this->response->getRawDataNew($page->location);
+            }
+            if (empty($this->response->language)) $this->response->language = $page->get("language");
+            if (empty($this->response->action)) $this->response->action = $this->response->isUser() ? "none" : "login";
+            if (empty($this->response->status)) $this->response->status = "none";
+            if ($this->response->status=="error") $this->response->action = "error";
+        }
+    }
+    
+    // Handle page content parsing of custom block
+    public function onParseContentBlock($page, $name, $text, $shortcut) {
+        $output = null;
+        if ($name=="edit" && $shortcut) {
+            $editText = "$name $text";
+            if (substru($text, 0, 2)=="- ") $editText = trim(substru($text, 2));
+            $output = "<a href=\"".$page->get("pageEdit")."\">".htmlspecialchars($editText)."</a>";
+        }
+        return $output;
+    }
+    
+    // Handle page extra HTML data
+    public function onExtra($name) {
+        $output = null;
+        if ($name=="header" && $this->response->isActive()) {
+            $pluginLocation = $this->yellow->config->get("serverBase").$this->yellow->config->get("pluginLocation");
+            $output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"{$pluginLocation}edit.css\" />\n";
+            $output .= "<script type=\"text/javascript\" src=\"{$pluginLocation}edit.js\"></script>\n";
+            $output .= "<script type=\"text/javascript\">\n";
+            $output .= "// <![CDATA[\n";
+            $output .= "yellow.page = ".json_encode($this->response->getPageData()).";\n";
+            $output .= "yellow.config = ".json_encode($this->response->getConfigData()).";\n";
+            $output .= "yellow.text = ".json_encode($this->response->getTextData()).";\n";
+            $output .= "// ]]>\n";
+            $output .= "</script>\n";
+        }
+        return $output;
+    }
+    
+    // Handle command
+    public function onCommand($args) {
+        list($command) = $args;
+        switch ($command) {
+            case "user":    $statusCode = $this->userCommand($args); break;
+            default:        $statusCode = 0;
+        }
+        return $statusCode;
+    }
+    
+    // Handle command help
+    public function onCommandHelp() {
+        return "user [option email password name]\n";
+    }
 
-	// Update user account
-	function userCommand($args)
-	{
-		list($command, $option) = $args;
-		switch($option)
-		{
-			case "":		$statusCode = $this->userShow($args); break;
-			case "add": 	$statusCode = $this->userAdd($args); break;
-			case "change":	$statusCode = $this->userChange($args); break;
-			case "remove":	$statusCode = $this->userRemove($args); break;
-			default:		$statusCode = 400; echo "Yellow $command: Invalid arguments\n";
-		}
-		return $statusCode;
-	}
-	
-	// Show user accounts
-	function userShow($args)
-	{
-		list($command) = $args;
-		foreach($this->users->getData() as $line) echo "$line\n";
-		if(!$this->users->getNumber()) echo "Yellow $command: No user accounts\n";
-		return 200;
-	}
-	
-	// Add user account
-	function userAdd($args)
-	{
-		$status = "ok";
-		list($command, $option, $email, $password, $name) = $args;
-		if(empty($email) || empty($password)) $status = $this->response->status = "incomplete";
-		if($status=="ok") $status = $this->getUserAccount($email, $password, "add");
-		if($status=="ok" && $this->users->isTaken($email)) $status = "taken";
-		switch($status)
-		{
-			case "incomplete": echo "ERROR updating configuration: Please enter email and password!\n"; break;
-			case "invalid":	echo "ERROR updating configuration: Please enter a valid email!\n"; break;
-			case "taken": echo "ERROR updating configuration: Please enter a different email!\n"; break;
-			case "weak": echo "ERROR updating configuration: Please enter a different password!\n"; break;
-		}
-		if($status=="ok")
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$status = $this->users->save($fileNameUser, $email, $password, $name, "", "active") ? "ok" : "error";
-			if($status=="error") echo "ERROR updating configuration: Can't write file '$fileNameUser'!\n";
-		}
-		if($status=="ok")
-		{
-			$algorithm = $this->yellow->config->get("editUserHashAlgorithm");
-			$status = substru($this->users->getHash($email), 0, 10)!="error-hash" ? "ok" : "error";
-			if($status=="error") echo "ERROR updating configuration: Hash algorithm '$algorithm' not supported!\n";
-		}
-		$statusCode = $status=="ok" ? 200 : 500;
-		echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."added\n";
-		return $statusCode;
-	}
-	
-	// Change user account
-	function userChange($args)
-	{
-		$status = "ok";
-		list($command, $option, $email, $password, $name) = $args;
-		if(empty($email)) $status = $this->response->status = "invalid";
-		if($status=="ok") $status = $this->getUserAccount($email, $password, "change");
-		if($status=="ok" && !$this->users->isExisting($email)) $status = "unknown";
-		switch($status)
-		{
-			case "invalid":	echo "ERROR updating configuration: Please enter a valid email!\n"; break;
-			case "unknown": echo "ERROR updating configuration: Can't find email '$email'!\n"; break;
-			case "weak": echo "ERROR updating configuration: Please enter a different password!\n"; break;
-		}
-		if($status=="ok")
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$status = $this->users->save($fileNameUser, $email, $password, $name) ? "ok" : "error";
-			if($status=="error") echo "ERROR updating configuration: Can't write file '$fileNameUser'!\n";
-		}
-		$statusCode = $status=="ok" ? 200 : 500;
-		echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."changed\n";
-		return $statusCode;
-	}
+    // Update user account
+    public function userCommand($args) {
+        list($command, $option) = $args;
+        switch ($option) {
+            case "":        $statusCode = $this->userShow($args); break;
+            case "add":     $statusCode = $this->userAdd($args); break;
+            case "change":  $statusCode = $this->userChange($args); break;
+            case "remove":  $statusCode = $this->userRemove($args); break;
+            default:        $statusCode = 400; echo "Yellow $command: Invalid arguments\n";
+        }
+        return $statusCode;
+    }
+    
+    // Show user accounts
+    public function userShow($args) {
+        list($command) = $args;
+        foreach ($this->users->getData() as $line) {
+            echo "$line\n";
+        }
+        if (!$this->users->getNumber()) echo "Yellow $command: No user accounts\n";
+        return 200;
+    }
+    
+    // Add user account
+    public function userAdd($args) {
+        $status = "ok";
+        list($command, $option, $email, $password, $name) = $args;
+        if (empty($email) || empty($password)) $status = $this->response->status = "incomplete";
+        if ($status=="ok") $status = $this->getUserAccount($email, $password, "add");
+        if ($status=="ok" && $this->users->isTaken($email)) $status = "taken";
+        switch ($status) {
+            case "incomplete":  echo "ERROR updating configuration: Please enter email and password!\n"; break;
+            case "invalid":     echo "ERROR updating configuration: Please enter a valid email!\n"; break;
+            case "taken":       echo "ERROR updating configuration: Please enter a different email!\n"; break;
+            case "weak":        echo "ERROR updating configuration: Please enter a different password!\n"; break;
+        }
+        if ($status=="ok") {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $status = $this->users->save($fileNameUser, $email, $password, $name, "", "active") ? "ok" : "error";
+            if ($status=="error") echo "ERROR updating configuration: Can't write file '$fileNameUser'!\n";
+        }
+        if ($status=="ok") {
+            $algorithm = $this->yellow->config->get("editUserHashAlgorithm");
+            $status = substru($this->users->getHash($email), 0, 10)!="error-hash" ? "ok" : "error";
+            if ($status=="error") echo "ERROR updating configuration: Hash algorithm '$algorithm' not supported!\n";
+        }
+        $statusCode = $status=="ok" ? 200 : 500;
+        echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."added\n";
+        return $statusCode;
+    }
+    
+    // Change user account
+    public function userChange($args) {
+        $status = "ok";
+        list($command, $option, $email, $password, $name) = $args;
+        if (empty($email)) $status = $this->response->status = "invalid";
+        if ($status=="ok") $status = $this->getUserAccount($email, $password, "change");
+        if ($status=="ok" && !$this->users->isExisting($email)) $status = "unknown";
+        switch ($status) {
+            case "invalid": echo "ERROR updating configuration: Please enter a valid email!\n"; break;
+            case "unknown": echo "ERROR updating configuration: Can't find email '$email'!\n"; break;
+            case "weak":    echo "ERROR updating configuration: Please enter a different password!\n"; break;
+        }
+        if ($status=="ok") {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $status = $this->users->save($fileNameUser, $email, $password, $name) ? "ok" : "error";
+            if ($status=="error") echo "ERROR updating configuration: Can't write file '$fileNameUser'!\n";
+        }
+        $statusCode = $status=="ok" ? 200 : 500;
+        echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."changed\n";
+        return $statusCode;
+    }
 
-	// Remove user account
-	function userRemove($args)
-	{
-		$status = "ok";
-		list($command, $option, $email) = $args;
-		if(empty($email)) $status = $this->response->status = "invalid";
-		if($status=="ok") $status = $this->getUserAccount($email, "", "remove");
-		if($status=="ok" && !$this->users->isExisting($email)) $status = "unknown";
-		switch($status)
-		{
-			case "invalid":	echo "ERROR updating configuration: Please enter a valid email!\n"; break;
-			case "unknown": echo "ERROR updating configuration: Can't find email '$email'!\n"; break;
-		}
-		if($status=="ok")
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$status = $this->users->remove($fileNameUser, $email) ? "ok" : "error";
-			if($status=="error") echo "ERROR updating configuration: Can't write file '$fileNameUser'!\n";
-		}
-		$statusCode = $status=="ok" ? 200 : 500;
-		echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."removed\n";
-		return $statusCode;
-	}
-	
-	// Process request
-	function processRequest($scheme, $address, $base, $location, $fileName)
-	{
-		$statusCode = 0;
-		if($this->checkUserAuth($scheme, $address, $base, $location, $fileName))
-		{
-			switch($_REQUEST["action"])
-			{
-				case "":			$statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break;
-				case "login":		$statusCode = $this->processRequestLogin($scheme, $address, $base, $location, $fileName); break;
-				case "logout":		$statusCode = $this->processRequestLogout($scheme, $address, $base, $location, $fileName); break;
-				case "settings":	$statusCode = $this->processRequestSettings($scheme, $address, $base, $location, $fileName); break;
-				case "version":		$statusCode = $this->processRequestVersion($scheme, $address, $base, $location, $fileName); break;
-				case "update":		$statusCode = $this->processRequestUpdate($scheme, $address, $base, $location, $fileName); break;
-				case "quit":		$statusCode = $this->processRequestQuit($scheme, $address, $base, $location, $fileName); break;
-				case "create":		$statusCode = $this->processRequestCreate($scheme, $address, $base, $location, $fileName); break;
-				case "edit":		$statusCode = $this->processRequestEdit($scheme, $address, $base, $location, $fileName); break;
-				case "delete":		$statusCode = $this->processRequestDelete($scheme, $address, $base, $location, $fileName); break;
-				case "preview":		$statusCode = $this->processRequestPreview($scheme, $address, $base, $location, $fileName); break;
-				case "upload":		$statusCode = $this->processRequestUpload($scheme, $address, $base, $location, $fileName); break;
-			}
-		} else if($this->checkUserUnauth($scheme, $address, $base, $location, $fileName)) {
-			$this->yellow->lookup->requestHandler = "core";
-			switch($_REQUEST["action"])
-			{
-				case "":			$statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break;
-				case "signup":		$statusCode = $this->processRequestSignup($scheme, $address, $base, $location, $fileName); break;
-				case "forgot":		$statusCode = $this->processRequestForgot($scheme, $address, $base, $location, $fileName); break;
-				case "confirm":		$statusCode = $this->processRequestConfirm($scheme, $address, $base, $location, $fileName); break;
-				case "approve":		$statusCode = $this->processRequestApprove($scheme, $address, $base, $location, $fileName); break;
-				case "recover":		$statusCode = $this->processRequestRecover($scheme, $address, $base, $location, $fileName); break;
-				case "reactivate":	$statusCode = $this->processRequestReactivate($scheme, $address, $base, $location, $fileName); break;
-				case "verify":		$statusCode = $this->processRequestVerify($scheme, $address, $base, $location, $fileName); break;
-				case "change":		$statusCode = $this->processRequestChange($scheme, $address, $base, $location, $fileName); break;
-				case "remove":		$statusCode = $this->processRequestRemove($scheme, $address, $base, $location, $fileName); break;
-			}
-		}
-		$this->checkUserFailed($scheme, $address, $base, $location, $fileName);
-		return $statusCode;
-	}
-	
-	// Process request to show file
-	function processRequestShow($scheme, $address, $base, $location, $fileName)
-	{
-		$statusCode = 0;
-		if(is_readable($fileName))
-		{
-			$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		} else {
-			if($this->yellow->lookup->isRedirectLocation($location))
-			{
-				$location = $this->yellow->lookup->isFileLocation($location) ? "$location/" : "/".$this->yellow->getRequestLanguage()."/";
-				$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
-				$statusCode = $this->yellow->sendStatus(301, $location);
-			} else {
-				$this->yellow->page->error($this->response->isUserRestrictions() ? 404 : 434);
-				$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-			}
-		}
-		return $statusCode;
-	}
+    // Remove user account
+    public function userRemove($args) {
+        $status = "ok";
+        list($command, $option, $email) = $args;
+        if (empty($email)) $status = $this->response->status = "invalid";
+        if ($status=="ok") $status = $this->getUserAccount($email, "", "remove");
+        if ($status=="ok" && !$this->users->isExisting($email)) $status = "unknown";
+        switch ($status) {
+            case "invalid": echo "ERROR updating configuration: Please enter a valid email!\n"; break;
+            case "unknown": echo "ERROR updating configuration: Can't find email '$email'!\n"; break;
+        }
+        if ($status=="ok") {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $status = $this->users->remove($fileNameUser, $email) ? "ok" : "error";
+            if ($status=="error") echo "ERROR updating configuration: Can't write file '$fileNameUser'!\n";
+        }
+        $statusCode = $status=="ok" ? 200 : 500;
+        echo "Yellow $command: User account ".($statusCode!=200 ? "not " : "")."removed\n";
+        return $statusCode;
+    }
+    
+    // Process request
+    public function processRequest($scheme, $address, $base, $location, $fileName) {
+        $statusCode = 0;
+        if ($this->checkUserAuth($scheme, $address, $base, $location, $fileName)) {
+            switch ($_REQUEST["action"]) {
+                case "":            $statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break;
+                case "login":       $statusCode = $this->processRequestLogin($scheme, $address, $base, $location, $fileName); break;
+                case "logout":      $statusCode = $this->processRequestLogout($scheme, $address, $base, $location, $fileName); break;
+                case "settings":    $statusCode = $this->processRequestSettings($scheme, $address, $base, $location, $fileName); break;
+                case "version":     $statusCode = $this->processRequestVersion($scheme, $address, $base, $location, $fileName); break;
+                case "update":      $statusCode = $this->processRequestUpdate($scheme, $address, $base, $location, $fileName); break;
+                case "quit":        $statusCode = $this->processRequestQuit($scheme, $address, $base, $location, $fileName); break;
+                case "create":      $statusCode = $this->processRequestCreate($scheme, $address, $base, $location, $fileName); break;
+                case "edit":        $statusCode = $this->processRequestEdit($scheme, $address, $base, $location, $fileName); break;
+                case "delete":      $statusCode = $this->processRequestDelete($scheme, $address, $base, $location, $fileName); break;
+                case "preview":     $statusCode = $this->processRequestPreview($scheme, $address, $base, $location, $fileName); break;
+                case "upload":      $statusCode = $this->processRequestUpload($scheme, $address, $base, $location, $fileName); break;
+            }
+        } elseif ($this->checkUserUnauth($scheme, $address, $base, $location, $fileName)) {
+            $this->yellow->lookup->requestHandler = "core";
+            switch ($_REQUEST["action"]) {
+                case "":            $statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break;
+                case "signup":      $statusCode = $this->processRequestSignup($scheme, $address, $base, $location, $fileName); break;
+                case "forgot":      $statusCode = $this->processRequestForgot($scheme, $address, $base, $location, $fileName); break;
+                case "confirm":     $statusCode = $this->processRequestConfirm($scheme, $address, $base, $location, $fileName); break;
+                case "approve":     $statusCode = $this->processRequestApprove($scheme, $address, $base, $location, $fileName); break;
+                case "recover":     $statusCode = $this->processRequestRecover($scheme, $address, $base, $location, $fileName); break;
+                case "reactivate":  $statusCode = $this->processRequestReactivate($scheme, $address, $base, $location, $fileName); break;
+                case "verify":      $statusCode = $this->processRequestVerify($scheme, $address, $base, $location, $fileName); break;
+                case "change":      $statusCode = $this->processRequestChange($scheme, $address, $base, $location, $fileName); break;
+                case "remove":      $statusCode = $this->processRequestRemove($scheme, $address, $base, $location, $fileName); break;
+            }
+        }
+        $this->checkUserFailed($scheme, $address, $base, $location, $fileName);
+        return $statusCode;
+    }
+    
+    // Process request to show file
+    public function processRequestShow($scheme, $address, $base, $location, $fileName) {
+        $statusCode = 0;
+        if (is_readable($fileName)) {
+            $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        } else {
+            if ($this->yellow->lookup->isRedirectLocation($location)) {
+                $location = $this->yellow->lookup->isFileLocation($location) ? "$location/" : "/".$this->yellow->getRequestLanguage()."/";
+                $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+                $statusCode = $this->yellow->sendStatus(301, $location);
+            } else {
+                $this->yellow->page->error($this->response->isUserRestrictions() ? 404 : 434);
+                $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+            }
+        }
+        return $statusCode;
+    }
 
-	// Process request for user login
-	function processRequestLogin($scheme, $address, $base, $location, $fileName)
-	{
-		$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-		if($this->users->save($fileNameUser, $this->response->userEmail))
-		{
-			$home = $this->users->getHome($this->response->userEmail);
-			if(substru($location, 0, strlenu($home))==$home)
-			{
-				$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
-				$statusCode = $this->yellow->sendStatus(303, $location);
-			} else {
-				$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $home);
-				$statusCode = $this->yellow->sendStatus(302, $location);
-			}
-		} else {
-			$this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-			$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		}
-		return $statusCode;
-	}
-	
-	// Process request for user logout
-	function processRequestLogout($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->userEmail = "";
-		$this->response->destroyCookies($scheme, $address, $base);
-		$location = $this->yellow->lookup->normaliseUrl(
-			$this->yellow->config->get("serverScheme"),
-			$this->yellow->config->get("serverAddress"),
-			$this->yellow->config->get("serverBase"), $location);
-		$statusCode = $this->yellow->sendStatus(302, $location);
-		return $statusCode;
-	}
+    // Process request for user login
+    public function processRequestLogin($scheme, $address, $base, $location, $fileName) {
+        $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+        if ($this->users->save($fileNameUser, $this->response->userEmail)) {
+            $home = $this->users->getHome($this->response->userEmail);
+            if (substru($location, 0, strlenu($home))==$home) {
+                $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+                $statusCode = $this->yellow->sendStatus(303, $location);
+            } else {
+                $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $home);
+                $statusCode = $this->yellow->sendStatus(302, $location);
+            }
+        } else {
+            $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+            $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        }
+        return $statusCode;
+    }
+    
+    // Process request for user logout
+    public function processRequestLogout($scheme, $address, $base, $location, $fileName) {
+        $this->response->userEmail = "";
+        $this->response->destroyCookies($scheme, $address, $base);
+        $location = $this->yellow->lookup->normaliseUrl(
+            $this->yellow->config->get("serverScheme"),
+            $this->yellow->config->get("serverAddress"),
+            $this->yellow->config->get("serverBase"),
+            $location);
+        $statusCode = $this->yellow->sendStatus(302, $location);
+        return $statusCode;
+    }
 
-	// Process request for user signup
-	function processRequestSignup($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "signup";
-		$this->response->status = "ok";
-		$name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"]));
-		$email = trim($_REQUEST["email"]);
-		$password = trim($_REQUEST["password"]);
-		$consent = trim($_REQUEST["consent"]);
-		if(empty($name) || empty($email) || empty($password) || empty($consent)) $this->response->status = "incomplete";
-		if($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, $password, $this->response->action);
-		if($this->response->status=="ok" && $this->response->isLoginRestrictions()) $this->response->status = "next";
-		if($this->response->status=="ok" && $this->users->isTaken($email)) $this->response->status = "next";
-		if($this->response->status=="ok")
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$this->response->status = $this->users->save($fileNameUser, $email, $password, $name, "", "unconfirmed") ? "ok" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-		}
-		if($this->response->status=="ok")
-		{
-			$algorithm = $this->yellow->config->get("editUserHashAlgorithm");
-			$this->response->status = substru($this->users->getHash($email), 0, 10)!="error-hash" ? "ok" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Hash algorithm '$algorithm' not supported!");
-		}
-		if($this->response->status=="ok")
-		{
-			$this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "confirm") ? "next" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
-	
-	// Process request to confirm user signup
-	function processRequestConfirm($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "confirm";
-		$this->response->status = "ok";
-		$email = $_REQUEST["email"];
-		$this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
-		if($this->response->status=="ok")
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "unapproved") ? "ok" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-		}
-		if($this->response->status=="ok")
-		{
-			$this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "approve") ? "done" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
-	
-	// Process request to approve user signup
-	function processRequestApprove($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "approve";
-		$this->response->status = "ok";
-		$email = $_REQUEST["email"];
-		$this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
-		if($this->response->status=="ok")
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "ok" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-		}
-		if($this->response->status=="ok")
-		{
-			$this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "welcome") ? "done" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
+    // Process request for user signup
+    public function processRequestSignup($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "signup";
+        $this->response->status = "ok";
+        $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"]));
+        $email = trim($_REQUEST["email"]);
+        $password = trim($_REQUEST["password"]);
+        $consent = trim($_REQUEST["consent"]);
+        if (empty($name) || empty($email) || empty($password) || empty($consent)) $this->response->status = "incomplete";
+        if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, $password, $this->response->action);
+        if ($this->response->status=="ok" && $this->response->isLoginRestrictions()) $this->response->status = "next";
+        if ($this->response->status=="ok" && $this->users->isTaken($email)) $this->response->status = "next";
+        if ($this->response->status=="ok") {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $this->response->status = $this->users->save($fileNameUser, $email, $password, $name, "", "unconfirmed") ? "ok" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+        }
+        if ($this->response->status=="ok") {
+            $algorithm = $this->yellow->config->get("editUserHashAlgorithm");
+            $this->response->status = substru($this->users->getHash($email), 0, 10)!="error-hash" ? "ok" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Hash algorithm '$algorithm' not supported!");
+        }
+        if ($this->response->status=="ok") {
+            $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "confirm") ? "next" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
+    
+    // Process request to confirm user signup
+    public function processRequestConfirm($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "confirm";
+        $this->response->status = "ok";
+        $email = $_REQUEST["email"];
+        $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
+        if ($this->response->status=="ok") {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "unapproved") ? "ok" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+        }
+        if ($this->response->status=="ok") {
+            $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "approve") ? "done" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
+    
+    // Process request to approve user signup
+    public function processRequestApprove($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "approve";
+        $this->response->status = "ok";
+        $email = $_REQUEST["email"];
+        $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
+        if ($this->response->status=="ok") {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "ok" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+        }
+        if ($this->response->status=="ok") {
+            $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "welcome") ? "done" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
 
-	// Process request for forgotten password
-	function processRequestForgot($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "forgot";
-		$this->response->status = "ok";
-		$email = trim($_REQUEST["email"]);
-		if(!filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid";
-		if($this->response->status=="ok" && !$this->users->isExisting($email)) $this->response->status = "next";
-		if($this->response->status=="ok")
-		{
-			$this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "recover") ? "next" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
-	
-	// Process request to recover password
-	function processRequestRecover($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "recover";
-		$this->response->status = "ok";
-		$email = trim($_REQUEST["email"]);
-		$password = trim($_REQUEST["password"]);
-		$this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
-		if($this->response->status=="ok")
-		{
-			if(empty($password)) $this->response->status = "password";
-			if($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, $password, $this->response->action);
-			if($this->response->status=="ok")
-			{
-				$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-				$this->response->status = $this->users->save($fileNameUser, $email, $password) ? "ok" : "error";
-				if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-			}
-			if($this->response->status=="ok")
-			{
-				$this->response->destroyCookies($scheme, $address, $base);
-				$this->response->status = "done";
-			}
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
-	
-	// Process request to reactivate account
-	function processRequestReactivate($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "reactivate";
-		$this->response->status = "ok";
-		$email = $_REQUEST["email"];
-		$this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
-		if($this->response->status=="ok")
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "done" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
-	
-	// Process request to change settings
-	function processRequestSettings($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "settings";
-		$this->response->status = "ok";
-		$email = trim($_REQUEST["email"]);
-		$emailSource = $this->response->userEmail;
-		$password = trim($_REQUEST["password"]);
-		$name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"]));
-		$language = trim($_REQUEST["language"]);
-		if($email!=$emailSource || !empty($password))
-		{
-			if(empty($email)) $this->response->status = "invalid";
-			if($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, $password, $this->response->action);
-			if($this->response->status=="ok" && $email!=$emailSource && $this->users->isTaken($email)) $this->response->status = "taken";
-			if($this->response->status=="ok" && $email!=$emailSource)
-			{
-				$pending = $emailSource;
-				$home = $this->users->getHome($emailSource);
-				$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-				$this->response->status = $this->users->save($fileNameUser, $email, "no", $name, $language, "unverified", "", "", "", $pending, $home) ? "ok" : "error";
-				if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-			}
-			if($this->response->status=="ok")
-			{
-				$pending = $email.':'.(empty($password) ? $this->users->getHash($emailSource) : $this->users->createHash($password));
-				$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-				$this->response->status = $this->users->save($fileNameUser, $emailSource, "", $name, $language, "", "", "", "", $pending) ? "ok" : "error";
-				if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-			}
-			if($this->response->status=="ok")
-			{
-				$action = $email!=$emailSource ? "verify" : "change";
-				$this->response->status = $this->response->sendMail($scheme, $address, $base, $email, $action) ? "next" : "error";
-				if($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
-			}
-		} else {
-			if($this->response->status=="ok")
-			{
-				$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-				$this->response->status = $this->users->save($fileNameUser, $email, "", $name, $language) ? "done" : "error";
-				if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-			}
-		}
-		if($this->response->status=="done")
-		{
-			$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
-			$statusCode = $this->yellow->sendStatus(303, $location);
-		} else {
-			$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		}
-		return $statusCode;
-	}
+    // Process request for forgotten password
+    public function processRequestForgot($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "forgot";
+        $this->response->status = "ok";
+        $email = trim($_REQUEST["email"]);
+        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $this->response->status = "invalid";
+        if ($this->response->status=="ok" && !$this->users->isExisting($email)) $this->response->status = "next";
+        if ($this->response->status=="ok") {
+            $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "recover") ? "next" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
+    
+    // Process request to recover password
+    public function processRequestRecover($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "recover";
+        $this->response->status = "ok";
+        $email = trim($_REQUEST["email"]);
+        $password = trim($_REQUEST["password"]);
+        $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
+        if ($this->response->status=="ok") {
+            if (empty($password)) $this->response->status = "password";
+            if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, $password, $this->response->action);
+            if ($this->response->status=="ok") {
+                $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+                $this->response->status = $this->users->save($fileNameUser, $email, $password) ? "ok" : "error";
+                if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+            }
+            if ($this->response->status=="ok") {
+                $this->response->destroyCookies($scheme, $address, $base);
+                $this->response->status = "done";
+            }
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
+    
+    // Process request to reactivate account
+    public function processRequestReactivate($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "reactivate";
+        $this->response->status = "ok";
+        $email = $_REQUEST["email"];
+        $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
+        if ($this->response->status=="ok") {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "done" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
+    
+    // Process request to change settings
+    public function processRequestSettings($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "settings";
+        $this->response->status = "ok";
+        $email = trim($_REQUEST["email"]);
+        $emailSource = $this->response->userEmail;
+        $password = trim($_REQUEST["password"]);
+        $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"]));
+        $language = trim($_REQUEST["language"]);
+        if ($email!=$emailSource || !empty($password)) {
+            if (empty($email)) $this->response->status = "invalid";
+            if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, $password, $this->response->action);
+            if ($this->response->status=="ok" && $email!=$emailSource && $this->users->isTaken($email)) $this->response->status = "taken";
+            if ($this->response->status=="ok" && $email!=$emailSource) {
+                $pending = $emailSource;
+                $home = $this->users->getHome($emailSource);
+                $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+                $this->response->status = $this->users->save($fileNameUser, $email, "no", $name, $language, "unverified", "", "", "", $pending, $home) ? "ok" : "error";
+                if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+            }
+            if ($this->response->status=="ok") {
+                $pending = $email.":".(empty($password) ? $this->users->getHash($emailSource) : $this->users->createHash($password));
+                $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+                $this->response->status = $this->users->save($fileNameUser, $emailSource, "", $name, $language, "", "", "", "", $pending) ? "ok" : "error";
+                if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+            }
+            if ($this->response->status=="ok") {
+                $action = $email!=$emailSource ? "verify" : "change";
+                $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, $action) ? "next" : "error";
+                if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
+            }
+        } else {
+            if ($this->response->status=="ok") {
+                $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+                $this->response->status = $this->users->save($fileNameUser, $email, "", $name, $language) ? "done" : "error";
+                if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+            }
+        }
+        if ($this->response->status=="done") {
+            $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+            $statusCode = $this->yellow->sendStatus(303, $location);
+        } else {
+            $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        }
+        return $statusCode;
+    }
 
-	// Process request to verify email
-	function processRequestVerify($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "verify";
-		$this->response->status = "ok";
-		$email = $emailSource = $_REQUEST["email"];
-		$this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
-		if($this->response->status=="ok")
-		{
-			$emailSource = $this->users->getPending($email);
-			if($this->users->getStatus($emailSource)!="active") $this->response->status = "done";
-		}
-		if($this->response->status=="ok")
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "unchanged") ? "ok" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-		}
-		if($this->response->status=="ok")
-		{
-			$this->response->status = $this->response->sendMail($scheme, $address, $base, $emailSource, "change") ? "done" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
-	
-	// Process request to change email or password
-	function processRequestChange($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "change";
-		$this->response->status = "ok";
-		$email = $emailSource = trim($_REQUEST["email"]);
-		$this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
-		if($this->response->status=="ok")
-		{
-			list($email, $hash) = explode(':', $this->users->getPending($email), 2);
-			if(!$this->users->isExisting($email) || empty($hash)) $this->response->status = "done";
-		}
-		if($this->response->status=="ok")
-		{
-			$this->users->users[$email]["hash"] = $hash;
-			$this->users->users[$email]["pending"] = "none";
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "ok" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-		}
-		if($this->response->status=="ok" && $email!=$emailSource)
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$this->response->status = $this->users->remove($fileNameUser, $emailSource) ? "ok" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-		}
-		if($this->response->status=="ok")
-		{
-			$this->response->destroyCookies($scheme, $address, $base);
-			$this->response->status = "done";
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
-	
-	// Process request to show software version
-	function processRequestVersion($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "version";
-		$this->response->status = "ok";
-		if($this->yellow->plugins->isExisting("update"))
-		{
-			list($statusCodeCurrent, $dataCurrent) = $this->yellow->plugins->get("update")->getSoftwareVersion();
-			list($statusCodeLatest, $dataLatest) = $this->yellow->plugins->get("update")->getSoftwareVersion(true);
-			list($statusCodeModified, $dataModified) = $this->yellow->plugins->get("update")->getSoftwareModified();
-			$statusCode = max($statusCodeCurrent, $statusCodeLatest, $statusCodeModified);
-			if($this->response->isUserWebmaster())
-			{
-				foreach($dataCurrent as $key=>$value)
-				{
-					if(strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0)
-					{
-						++$updates;
-						$rawData = htmlspecialchars("$key $dataLatest[$key]")."<br />\n";
-						$this->response->rawDataOutput .= $rawData;
-					}
-				}
-				if($updates==0)
-				{
-					foreach($dataCurrent as $key=>$value)
-					{
-						if(!is_null($dataModified[$key]) && !is_null($dataLatest[$key]))
-						{
-							$rawData = $this->yellow->text->getTextHtml("editVersionUpdateModified", $this->response->language)." - <a href=\"#\" data-action=\"update\" data-status=\"update\" data-args=\"".$this->yellow->toolbox->normaliseArgs("option:force/feature:$key")."\">".$this->yellow->text->getTextHtml("editVersionUpdateForce", $this->response->language)."</a><br />\n";
-							$rawData = preg_replace("/@software/i", htmlspecialchars("$key $dataLatest[$key]"), $rawData);
-							$this->response->rawDataOutput .= $rawData;
-						}
-					}
-				}
-				$this->response->status = $updates ? "updates" : "done";
-			} else {
-				foreach($dataCurrent as $key=>$value)
-				{
-					if(strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0) ++$updates;
-				}
-				$this->response->status = $updates ? "warning" : "done";
-			}
-			if($statusCode!=200) $this->response->status = "error";
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
-	
-	// Process request to update website
-	function processRequestUpdate($scheme, $address, $base, $location, $fileName)
-	{
-		$statusCode = 0;
-		if($this->yellow->plugins->isExisting("update") && $this->response->isUserWebmaster())
-		{
-			$option = trim($_REQUEST["option"]);
-			$feature = trim($_REQUEST["feature"]);
-			$statusCode = $this->yellow->command("update", $option, $feature);
-			if($statusCode==200)
-			{
-				$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
-				$statusCode = $this->yellow->sendStatus(303, $location);
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Process request to quit account
-	function processRequestQuit($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "quit";
-		$this->response->status = "ok";
-		$name = trim($_REQUEST["name"]);
-		$email = $this->response->userEmail;
-		if(empty($name)) $this->response->status = "none";
-		if($this->response->status=="ok" && $name!=$this->users->getName($email)) $this->response->status = "mismatch";
-		if($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, "", $this->response->action);
-		if($this->response->status=="ok")
-		{
-			$this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "remove") ? "next" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
-	
-	// Process request to remove account
-	function processRequestRemove($scheme, $address, $base, $location, $fileName)
-	{
-		$this->response->action = "remove";
-		$this->response->status = "ok";
-		$email = $_REQUEST["email"];
-		$this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
-		if($this->response->status=="ok")
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "removed") ? "ok" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-		}
-		if($this->response->status=="ok")
-		{
-			$this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "goodbye") ? "ok" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
-		}
-		if($this->response->status=="ok")
-		{
-			$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-			$this->response->status = $this->users->remove($fileNameUser, $email) ? "ok" : "error";
-			if($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-		}
-		if($this->response->status=="ok")
-		{
-			$this->response->destroyCookies($scheme, $address, $base);
-			$this->response->status = "done";
-		}
-		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-		return $statusCode;
-	}
-	
-	// Process request to create page
-	function processRequestCreate($scheme, $address, $base, $location, $fileName)
-	{
-		$statusCode = 0;
-		if(!$this->response->isUserRestrictions() && !empty($_REQUEST["rawdataedit"]))
-		{
-			$this->response->rawDataSource = $_REQUEST["rawdatasource"];
-			$this->response->rawDataEdit = $_REQUEST["rawdatasource"];
-			$this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"];
-			$rawData = $_REQUEST["rawdataedit"];
-			$page = $this->response->getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $this->response->getEndOfLine());
-			if(!$page->isError())
-			{
-				if($this->yellow->toolbox->createFile($page->fileName, $page->rawData, true))
-				{
-					$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location);
-					$statusCode = $this->yellow->sendStatus(303, $location);
-				} else {
-					$this->yellow->page->error(500, "Can't write file '$page->fileName'!");
-					$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-				}
-			} else {
-				$this->yellow->page->error(500, $page->get("pageError"));
-				$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Process request to edit page
-	function processRequestEdit($scheme, $address, $base, $location, $fileName)
-	{
-		$statusCode = 0;
-		if(!$this->response->isUserRestrictions() && !empty($_REQUEST["rawdataedit"]))
-		{
-			$this->response->rawDataSource = $_REQUEST["rawdatasource"];
-			$this->response->rawDataEdit = $_REQUEST["rawdataedit"];
-			$this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"];
-			$rawDataFile = $this->yellow->toolbox->readFile($fileName);
-			$page = $this->response->getPageEdit($scheme, $address, $base, $location, $fileName,
-				$this->response->rawDataSource, $this->response->rawDataEdit, $rawDataFile, $this->response->rawDataEndOfLine);
-			if(!$page->isError())
-			{
-				if($this->yellow->toolbox->renameFile($fileName, $page->fileName, true) &&
-				   $this->yellow->toolbox->createFile($page->fileName, $page->rawData))
-				{
-					$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location);
-					$statusCode = $this->yellow->sendStatus(303, $location);
-				} else {
-					$this->yellow->page->error(500, "Can't write file '$page->fileName'!");
-					$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-				}
-			} else {
-				$this->yellow->page->error(500, $page->get("pageError"));
-				$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-			}
-		}
-		return $statusCode;
-	}
+    // Process request to verify email
+    public function processRequestVerify($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "verify";
+        $this->response->status = "ok";
+        $email = $emailSource = $_REQUEST["email"];
+        $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
+        if ($this->response->status=="ok") {
+            $emailSource = $this->users->getPending($email);
+            if ($this->users->getStatus($emailSource)!="active") $this->response->status = "done";
+        }
+        if ($this->response->status=="ok") {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "unchanged") ? "ok" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+        }
+        if ($this->response->status=="ok") {
+            $this->response->status = $this->response->sendMail($scheme, $address, $base, $emailSource, "change") ? "done" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
+    
+    // Process request to change email or password
+    public function processRequestChange($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "change";
+        $this->response->status = "ok";
+        $email = $emailSource = trim($_REQUEST["email"]);
+        $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
+        if ($this->response->status=="ok") {
+            list($email, $hash) = explode(":", $this->users->getPending($email), 2);
+            if (!$this->users->isExisting($email) || empty($hash)) $this->response->status = "done";
+        }
+        if ($this->response->status=="ok") {
+            $this->users->users[$email]["hash"] = $hash;
+            $this->users->users[$email]["pending"] = "none";
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "active") ? "ok" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+        }
+        if ($this->response->status=="ok" && $email!=$emailSource) {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $this->response->status = $this->users->remove($fileNameUser, $emailSource) ? "ok" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+        }
+        if ($this->response->status=="ok") {
+            $this->response->destroyCookies($scheme, $address, $base);
+            $this->response->status = "done";
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
+    
+    // Process request to show software version
+    public function processRequestVersion($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "version";
+        $this->response->status = "ok";
+        if ($this->yellow->plugins->isExisting("update")) {
+            list($statusCodeCurrent, $dataCurrent) = $this->yellow->plugins->get("update")->getSoftwareVersion();
+            list($statusCodeLatest, $dataLatest) = $this->yellow->plugins->get("update")->getSoftwareVersion(true);
+            list($statusCodeModified, $dataModified) = $this->yellow->plugins->get("update")->getSoftwareModified();
+            $statusCode = max($statusCodeCurrent, $statusCodeLatest, $statusCodeModified);
+            if ($this->response->isUserWebmaster()) {
+                foreach ($dataCurrent as $key=>$value) {
+                    if (strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0) {
+                        ++$updates;
+                        $rawData = htmlspecialchars("$key $dataLatest[$key]")."<br />\n";
+                        $this->response->rawDataOutput .= $rawData;
+                    }
+                }
+                if ($updates==0) {
+                    foreach ($dataCurrent as $key=>$value) {
+                        if (!is_null($dataModified[$key]) && !is_null($dataLatest[$key])) {
+                            $rawData = $this->yellow->text->getTextHtml("editVersionUpdateModified", $this->response->language)." - <a href=\"#\" data-action=\"update\" data-status=\"update\" data-args=\"".$this->yellow->toolbox->normaliseArgs("option:force/feature:$key")."\">".$this->yellow->text->getTextHtml("editVersionUpdateForce", $this->response->language)."</a><br />\n";
+                            $rawData = preg_replace("/@software/i", htmlspecialchars("$key $dataLatest[$key]"), $rawData);
+                            $this->response->rawDataOutput .= $rawData;
+                        }
+                    }
+                }
+                $this->response->status = $updates ? "updates" : "done";
+            } else {
+                foreach ($dataCurrent as $key=>$value) {
+                    if (strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0) ++$updates;
+                }
+                $this->response->status = $updates ? "warning" : "done";
+            }
+            if ($statusCode!=200) $this->response->status = "error";
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
+    
+    // Process request to update website
+    public function processRequestUpdate($scheme, $address, $base, $location, $fileName) {
+        $statusCode = 0;
+        if ($this->yellow->plugins->isExisting("update") && $this->response->isUserWebmaster()) {
+            $option = trim($_REQUEST["option"]);
+            $feature = trim($_REQUEST["feature"]);
+            $statusCode = $this->yellow->command("update", $option, $feature);
+            if ($statusCode==200) {
+                $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+                $statusCode = $this->yellow->sendStatus(303, $location);
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Process request to quit account
+    public function processRequestQuit($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "quit";
+        $this->response->status = "ok";
+        $name = trim($_REQUEST["name"]);
+        $email = $this->response->userEmail;
+        if (empty($name)) $this->response->status = "none";
+        if ($this->response->status=="ok" && $name!=$this->users->getName($email)) $this->response->status = "mismatch";
+        if ($this->response->status=="ok") $this->response->status = $this->getUserAccount($email, "", $this->response->action);
+        if ($this->response->status=="ok") {
+            $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "remove") ? "next" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
+    
+    // Process request to remove account
+    public function processRequestRemove($scheme, $address, $base, $location, $fileName) {
+        $this->response->action = "remove";
+        $this->response->status = "ok";
+        $email = $_REQUEST["email"];
+        $this->response->status = $this->getUserStatus($email, $_REQUEST["action"]);
+        if ($this->response->status=="ok") {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $this->response->status = $this->users->save($fileNameUser, $email, "", "", "", "removed") ? "ok" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+        }
+        if ($this->response->status=="ok") {
+            $this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "goodbye") ? "ok" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
+        }
+        if ($this->response->status=="ok") {
+            $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+            $this->response->status = $this->users->remove($fileNameUser, $email) ? "ok" : "error";
+            if ($this->response->status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+        }
+        if ($this->response->status=="ok") {
+            $this->response->destroyCookies($scheme, $address, $base);
+            $this->response->status = "done";
+        }
+        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+        return $statusCode;
+    }
+    
+    // Process request to create page
+    public function processRequestCreate($scheme, $address, $base, $location, $fileName) {
+        $statusCode = 0;
+        if (!$this->response->isUserRestrictions() && !empty($_REQUEST["rawdataedit"])) {
+            $this->response->rawDataSource = $_REQUEST["rawdatasource"];
+            $this->response->rawDataEdit = $_REQUEST["rawdatasource"];
+            $this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"];
+            $rawData = $_REQUEST["rawdataedit"];
+            $page = $this->response->getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $this->response->getEndOfLine());
+            if (!$page->isError()) {
+                if ($this->yellow->toolbox->createFile($page->fileName, $page->rawData, true)) {
+                    $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location);
+                    $statusCode = $this->yellow->sendStatus(303, $location);
+                } else {
+                    $this->yellow->page->error(500, "Can't write file '$page->fileName'!");
+                    $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+                }
+            } else {
+                $this->yellow->page->error(500, $page->get("pageError"));
+                $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Process request to edit page
+    public function processRequestEdit($scheme, $address, $base, $location, $fileName) {
+        $statusCode = 0;
+        if (!$this->response->isUserRestrictions() && !empty($_REQUEST["rawdataedit"])) {
+            $this->response->rawDataSource = $_REQUEST["rawdatasource"];
+            $this->response->rawDataEdit = $_REQUEST["rawdataedit"];
+            $this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"];
+            $rawDataFile = $this->yellow->toolbox->readFile($fileName);
+            $page = $this->response->getPageEdit($scheme, $address, $base, $location, $fileName,
+                $this->response->rawDataSource, $this->response->rawDataEdit, $rawDataFile, $this->response->rawDataEndOfLine);
+            if (!$page->isError()) {
+                if ($this->yellow->toolbox->renameFile($fileName, $page->fileName, true) &&
+                        $this->yellow->toolbox->createFile($page->fileName, $page->rawData)) {
+                    $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location);
+                    $statusCode = $this->yellow->sendStatus(303, $location);
+                } else {
+                    $this->yellow->page->error(500, "Can't write file '$page->fileName'!");
+                    $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+                }
+            } else {
+                $this->yellow->page->error(500, $page->get("pageError"));
+                $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+            }
+        }
+        return $statusCode;
+    }
 
-	// Process request to delete page
-	function processRequestDelete($scheme, $address, $base, $location, $fileName)
-	{
-		$statusCode = 0;
-		if(!$this->response->isUserRestrictions() && is_file($fileName))
-		{
-			$this->response->rawDataSource = $_REQUEST["rawdatasource"];
-			$this->response->rawDataEdit = $_REQUEST["rawdatasource"];
-			$this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"];
-			$rawDataFile = $this->yellow->toolbox->readFile($fileName);
-			$page = $this->response->getPageDelete($scheme, $address, $base, $location, $fileName,
-				$rawDataFile, $this->response->rawDataEndOfLine);
-			if(!$page->isError())
-			{
-				if($this->yellow->lookup->isFileLocation($location))
-				{
-					if($this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir")))
-					{
-						$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
-						$statusCode = $this->yellow->sendStatus(303, $location);
-					} else {
-						$this->yellow->page->error(500, "Can't delete file '$fileName'!");
-						$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-					}
-				} else {
-					if($this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->config->get("trashDir")))
-					{
-						$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
-						$statusCode = $this->yellow->sendStatus(303, $location);
-					} else {
-						$this->yellow->page->error(500, "Can't delete file '$fileName'!");
-						$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-					}
-				}
-			} else {
-				$this->yellow->page->error(500, $page->get("pageError"));
-				$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
-			}
-		}
-		return $statusCode;
-	}
+    // Process request to delete page
+    public function processRequestDelete($scheme, $address, $base, $location, $fileName) {
+        $statusCode = 0;
+        if (!$this->response->isUserRestrictions() && is_file($fileName)) {
+            $this->response->rawDataSource = $_REQUEST["rawdatasource"];
+            $this->response->rawDataEdit = $_REQUEST["rawdatasource"];
+            $this->response->rawDataEndOfLine = $_REQUEST["rawdataendofline"];
+            $rawDataFile = $this->yellow->toolbox->readFile($fileName);
+            $page = $this->response->getPageDelete($scheme, $address, $base, $location, $fileName,
+                $rawDataFile, $this->response->rawDataEndOfLine);
+            if (!$page->isError()) {
+                if ($this->yellow->lookup->isFileLocation($location)) {
+                    if ($this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir"))) {
+                        $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+                        $statusCode = $this->yellow->sendStatus(303, $location);
+                    } else {
+                        $this->yellow->page->error(500, "Can't delete file '$fileName'!");
+                        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+                    }
+                } else {
+                    if ($this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->config->get("trashDir"))) {
+                        $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+                        $statusCode = $this->yellow->sendStatus(303, $location);
+                    } else {
+                        $this->yellow->page->error(500, "Can't delete file '$fileName'!");
+                        $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+                    }
+                }
+            } else {
+                $this->yellow->page->error(500, $page->get("pageError"));
+                $statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
+            }
+        }
+        return $statusCode;
+    }
 
-	// Process request to show preview
-	function processRequestPreview($scheme, $address, $base, $location, $fileName)
-	{
-		$page = $this->response->getPagePreview($scheme, $address, $base, $location, $fileName,
-			$_REQUEST["rawdataedit"], $_REQUEST["rawdataendofline"]);
-		$statusCode = $this->yellow->sendData(200, $page->outputData, "", false);
-		if(defined("DEBUG") && DEBUG>=1)
-		{
-			$parser = $page->get("parser");
-			echo "YellowEdit::processRequestPreview parser:$parser<br/>\n";
-		}
-		return $statusCode;
-	}
-	
-	// Process request to upload file
-	function processRequestUpload($scheme, $address, $base, $location, $fileName)
-	{
-		$data = array();
-		$fileNameTemp = $_FILES["file"]["tmp_name"];
-		$fileNameShort = preg_replace("/[^\pL\d\-\.]/u", "-", basename($_FILES["file"]["name"]));
-		$fileSizeMax = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize"));
-		$extension = strtoloweru(($pos = strrposu($fileNameShort, '.')) ? substru($fileNameShort, $pos) : "");
-		$extensions = preg_split("/\s*,\s*/", $this->yellow->config->get("editUploadExtensions"));
-		if(!$this->response->isUserRestrictions() && is_uploaded_file($fileNameTemp) &&
-		   filesize($fileNameTemp)<=$fileSizeMax && in_array($extension, $extensions))
-		{
-			$file = $this->response->getFileUpload($scheme, $address, $base, $location, $fileNameTemp, $fileNameShort);
-			if(!$file->isError() && $this->yellow->toolbox->copyFile($fileNameTemp, $file->fileName, true))
-			{
-				$data["location"] = $file->getLocation();
-			} else {
-				$data["error"] = "Can't write file '$file->fileName'!";
-			}
-		} else {
-			$data["error"] = "Can't write file '$fileNameShort'!";
-		}
-		$statusCode = $this->yellow->sendData(is_null($data["error"]) ? 200 : 500, json_encode($data), "a.json", false);
-		return $statusCode;
-	}
-	
-	// Check request
-	function checkRequest($location)
-	{
-		$locationLength = strlenu($this->yellow->config->get("editLocation"));
-		$this->response->active = substru($location, 0, $locationLength)==$this->yellow->config->get("editLocation");
-		return $this->response->isActive();
-	}
-	
-	// Check user authentication
-	function checkUserAuth($scheme, $address, $base, $location, $fileName)
-	{
-		if($this->isRequestSameSite("POST", $scheme, $address) || $_REQUEST["action"]=="")
-		{
-			if($_REQUEST["action"]=="login")
-			{
-				$email = $_REQUEST["email"];
-				$password = $_REQUEST["password"];
-				if($this->users->checkAuthLogin($email, $password))
-				{
-					$this->response->createCookies($scheme, $address, $base, $email);
-					$this->response->userEmail = $email;
-					$this->response->userRestrictions = $this->getUserRestrictions($email, $location, $fileName);
-					$this->response->language = $this->getUserLanguage($email);
-				} else {
-					$this->response->userFailedError = "login";
-					$this->response->userFailedEmail = $email;
-					$this->response->userFailedExpire = PHP_INT_MAX;
-				}
-			} else if(isset($_COOKIE["authtoken"]) && isset($_COOKIE["csrftoken"])) {
-				if($this->users->checkAuthToken($_COOKIE["authtoken"], $_COOKIE["csrftoken"], $_POST["csrftoken"], $_REQUEST["action"]==""))
-				{
-					$this->response->userEmail = $email = $this->users->getAuthEmail($_COOKIE["authtoken"]);
-					$this->response->userRestrictions = $this->getUserRestrictions($email, $location, $fileName);
-					$this->response->language = $this->getUserLanguage($email);
-				} else {
-					$this->response->userFailedError = "auth";
-					$this->response->userFailedEmail = $this->users->getAuthEmail($_COOKIE["authtoken"]);
-					$this->response->userFailedExpire = $this->users->getAuthExpire($_COOKIE["authtoken"]);
-				}
-			}
-		}
-		return $this->response->isUser();
-	}
+    // Process request to show preview
+    public function processRequestPreview($scheme, $address, $base, $location, $fileName) {
+        $page = $this->response->getPagePreview($scheme, $address, $base, $location, $fileName,
+            $_REQUEST["rawdataedit"], $_REQUEST["rawdataendofline"]);
+        $statusCode = $this->yellow->sendData(200, $page->outputData, "", false);
+        if (defined("DEBUG") && DEBUG>=1) {
+            $parser = $page->get("parser");
+            echo "YellowEdit::processRequestPreview parser:$parser<br/>\n";
+        }
+        return $statusCode;
+    }
+    
+    // Process request to upload file
+    public function processRequestUpload($scheme, $address, $base, $location, $fileName) {
+        $data = array();
+        $fileNameTemp = $_FILES["file"]["tmp_name"];
+        $fileNameShort = preg_replace("/[^\pL\d\-\.]/u", "-", basename($_FILES["file"]["name"]));
+        $fileSizeMax = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize"));
+        $extension = strtoloweru(($pos = strrposu($fileNameShort, ".")) ? substru($fileNameShort, $pos) : "");
+        $extensions = preg_split("/\s*,\s*/", $this->yellow->config->get("editUploadExtensions"));
+        if (!$this->response->isUserRestrictions() && is_uploaded_file($fileNameTemp) &&
+           filesize($fileNameTemp)<=$fileSizeMax && in_array($extension, $extensions)) {
+            $file = $this->response->getFileUpload($scheme, $address, $base, $location, $fileNameTemp, $fileNameShort);
+            if (!$file->isError() && $this->yellow->toolbox->copyFile($fileNameTemp, $file->fileName, true)) {
+                $data["location"] = $file->getLocation();
+            } else {
+                $data["error"] = "Can't write file '$file->fileName'!";
+            }
+        } else {
+            $data["error"] = "Can't write file '$fileNameShort'!";
+        }
+        $statusCode = $this->yellow->sendData(is_null($data["error"]) ? 200 : 500, json_encode($data), "a.json", false);
+        return $statusCode;
+    }
+    
+    // Check request
+    public function checkRequest($location) {
+        $locationLength = strlenu($this->yellow->config->get("editLocation"));
+        $this->response->active = substru($location, 0, $locationLength)==$this->yellow->config->get("editLocation");
+        return $this->response->isActive();
+    }
+    
+    // Check user authentication
+    public function checkUserAuth($scheme, $address, $base, $location, $fileName) {
+        if ($this->isRequestSameSite("POST", $scheme, $address) || $_REQUEST["action"]=="") {
+            if ($_REQUEST["action"]=="login") {
+                $email = $_REQUEST["email"];
+                $password = $_REQUEST["password"];
+                if ($this->users->checkAuthLogin($email, $password)) {
+                    $this->response->createCookies($scheme, $address, $base, $email);
+                    $this->response->userEmail = $email;
+                    $this->response->userRestrictions = $this->getUserRestrictions($email, $location, $fileName);
+                    $this->response->language = $this->getUserLanguage($email);
+                } else {
+                    $this->response->userFailedError = "login";
+                    $this->response->userFailedEmail = $email;
+                    $this->response->userFailedExpire = PHP_INT_MAX;
+                }
+            } elseif (isset($_COOKIE["authtoken"]) && isset($_COOKIE["csrftoken"])) {
+                if ($this->users->checkAuthToken($_COOKIE["authtoken"], $_COOKIE["csrftoken"], $_POST["csrftoken"], $_REQUEST["action"]=="")) {
+                    $this->response->userEmail = $email = $this->users->getAuthEmail($_COOKIE["authtoken"]);
+                    $this->response->userRestrictions = $this->getUserRestrictions($email, $location, $fileName);
+                    $this->response->language = $this->getUserLanguage($email);
+                } else {
+                    $this->response->userFailedError = "auth";
+                    $this->response->userFailedEmail = $this->users->getAuthEmail($_COOKIE["authtoken"]);
+                    $this->response->userFailedExpire = $this->users->getAuthExpire($_COOKIE["authtoken"]);
+                }
+            }
+        }
+        return $this->response->isUser();
+    }
 
-	// Check user without authentication
-	function checkUserUnauth($scheme, $address, $base, $location, $fileName)
-	{
-		$ok = false;
-		if($_REQUEST["action"]=="" || $_REQUEST["action"]=="signup" || $_REQUEST["action"]=="forgot")
-		{
-			$ok = true;
-		} else if(isset($_REQUEST["actiontoken"])) {
-			if($this->users->checkActionToken($_REQUEST["actiontoken"], $_REQUEST["email"], $_REQUEST["action"], $_REQUEST["expire"]))
-			{
-				$ok = true;
-				$this->response->language = $this->getUserLanguage($_REQUEST["email"]);
-			} else {
-				$this->response->userFailedError = "action";
-				$this->response->userFailedEmail = $_REQUEST["email"];
-				$this->response->userFailedExpire = $_REQUEST["expire"];
-			}
-		}
-		return $ok;
-	}
+    // Check user without authentication
+    public function checkUserUnauth($scheme, $address, $base, $location, $fileName) {
+        $ok = false;
+        if ($_REQUEST["action"]=="" || $_REQUEST["action"]=="signup" || $_REQUEST["action"]=="forgot") {
+            $ok = true;
+        } elseif (isset($_REQUEST["actiontoken"])) {
+            if ($this->users->checkActionToken($_REQUEST["actiontoken"], $_REQUEST["email"], $_REQUEST["action"], $_REQUEST["expire"])) {
+                $ok = true;
+                $this->response->language = $this->getUserLanguage($_REQUEST["email"]);
+            } else {
+                $this->response->userFailedError = "action";
+                $this->response->userFailedEmail = $_REQUEST["email"];
+                $this->response->userFailedExpire = $_REQUEST["expire"];
+            }
+        }
+        return $ok;
+    }
 
-	// Check user failed
-	function checkUserFailed($scheme, $address, $base, $location, $fileName)
-	{
-		if(!empty($this->response->userFailedError))
-		{
-			if($this->response->userFailedExpire>time() && $this->users->isExisting($this->response->userFailedEmail))
-			{
-				$email = $this->response->userFailedEmail;
-				$modified = $this->users->getModified($email);
-				$errors = $this->users->getErrors($email)+1;
-				$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-				$status = $this->users->save($fileNameUser, $email, "", "", "", "", "", $modified, $errors) ? "ok" : "error";
-				if($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-				if($errors==$this->yellow->config->get("editBruteForceProtection"))
-				{
-					$statusBeforeProtection = $this->users->getStatus($email);
-					$statusAfterProtection = ($statusBeforeProtection=="active" || $statusBeforeProtection=="inactive") ? "inactive" : "failed";
-					if($status=="ok")
-					{
-						$status = $this->users->save($fileNameUser, $email, "", "", "", $statusAfterProtection, "", $modified, $errors) ? "ok" : "error";
-						if($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-					}
-					if($status=="ok" && $statusBeforeProtection=="active")
-					{
-						$status = $this->response->sendMail($scheme, $address, $base, $email, "reactivate") ? "done" : "error";
-						if($status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
-					}
-				}
-			}
-			if($this->response->userFailedError=="login" || $this->response->userFailedError=="auth")
-			{
-				$this->response->destroyCookies($scheme, $address, $base);
-				$this->response->status = "error";
-				$this->yellow->page->error(430);
-			} else {
-				$this->response->status = "error";
-				$this->yellow->page->error(500, "Link has expired!");
-			}
-		}
-	}
-	
-	// Return user status changes
-	function getUserStatus($email, $action)
-	{
-		switch($action)
-		{
-			case "confirm":		$statusExpected = "unconfirmed"; break;
-			case "approve":		$statusExpected = "unapproved"; break;
-			case "recover":		$statusExpected = "active"; break;
-			case "reactivate":	$statusExpected = "inactive"; break;
-			case "verify":		$statusExpected = "unverified"; break;
-			case "change":		$statusExpected = "active"; break;
-			case "remove":		$statusExpected = "active"; break;
-		}
-		return $this->users->getStatus($email)==$statusExpected ? "ok" : "done";
-	}
+    // Check user failed
+    public function checkUserFailed($scheme, $address, $base, $location, $fileName) {
+        if (!empty($this->response->userFailedError)) {
+            if ($this->response->userFailedExpire>time() && $this->users->isExisting($this->response->userFailedEmail)) {
+                $email = $this->response->userFailedEmail;
+                $modified = $this->users->getModified($email);
+                $errors = $this->users->getErrors($email)+1;
+                $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+                $status = $this->users->save($fileNameUser, $email, "", "", "", "", "", $modified, $errors) ? "ok" : "error";
+                if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+                if ($errors==$this->yellow->config->get("editBruteForceProtection")) {
+                    $statusBeforeProtection = $this->users->getStatus($email);
+                    $statusAfterProtection = ($statusBeforeProtection=="active" || $statusBeforeProtection=="inactive") ? "inactive" : "failed";
+                    if ($status=="ok") {
+                        $status = $this->users->save($fileNameUser, $email, "", "", "", $statusAfterProtection, "", $modified, $errors) ? "ok" : "error";
+                        if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+                    }
+                    if ($status=="ok" && $statusBeforeProtection=="active") {
+                        $status = $this->response->sendMail($scheme, $address, $base, $email, "reactivate") ? "done" : "error";
+                        if ($status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
+                    }
+                }
+            }
+            if ($this->response->userFailedError=="login" || $this->response->userFailedError=="auth") {
+                $this->response->destroyCookies($scheme, $address, $base);
+                $this->response->status = "error";
+                $this->yellow->page->error(430);
+            } else {
+                $this->response->status = "error";
+                $this->yellow->page->error(500, "Link has expired!");
+            }
+        }
+    }
+    
+    // Return user status changes
+    public function getUserStatus($email, $action) {
+        switch ($action) {
+            case "confirm":     $statusExpected = "unconfirmed"; break;
+            case "approve":     $statusExpected = "unapproved"; break;
+            case "recover":     $statusExpected = "active"; break;
+            case "reactivate":  $statusExpected = "inactive"; break;
+            case "verify":      $statusExpected = "unverified"; break;
+            case "change":      $statusExpected = "active"; break;
+            case "remove":      $statusExpected = "active"; break;
+        }
+        return $this->users->getStatus($email)==$statusExpected ? "ok" : "done";
+    }
 
-	// Return user account changes
-	function getUserAccount($email, $password, $action)
-	{
-		$status = null;
-		foreach($this->yellow->plugins->plugins as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onEditUserAccount"))
-			{
-				$status = $value["obj"]->onEditUserAccount($email, $password, $action, $this->users);
-				if(!is_null($status)) break;
-			}
-		}
-		if(is_null($status))
-		{
-			$status = "ok";
-			if(!empty($password) && strlenu($password)<$this->yellow->config->get("editUserPasswordMinLength")) $status = "weak";
-			if(!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) $status = "invalid";
-		}
-		return $status;
-	}
-	
-	// Return user restrictions
-	function getUserRestrictions($email, $location, $fileName)
-	{
-		$userRestrictions = null;
-		foreach($this->yellow->plugins->plugins as $key=>$value)
-		{
-			if(method_exists($value["obj"], "onEditUserRestrictions"))
-			{
-				$userRestrictions = $value["obj"]->onEditUserRestrictions($email, $location, $fileName, $this->users);
-				if(!is_null($userRestrictions)) break;
-			}
-		}
-		if(is_null($userRestrictions))
-		{
-			$userRestrictions = substru($location, 0, strlenu($this->users->getHome($email)))!=$this->users->getHome($email);
-			$userRestrictions |= empty($fileName) || strlenu(dirname($fileName))>128 || strlenu(basename($fileName))>128;
-		}
-		return $userRestrictions;
-	}
-	
-	// Return user language
-	function getUserLanguage($email)
-	{
-		$language = $this->users->getLanguage($email);
-		if(!$this->yellow->text->isLanguage($language)) $language = $this->yellow->config->get("language");
-		return $language;
-	}
-	
-	// Check if request came from same site
-	function isRequestSameSite($method, $scheme, $address)
-	{
-		if(preg_match("#^(\w+)://([^/]+)(.*)$#", $_SERVER["HTTP_REFERER"], $matches)) $origin = "$matches[1]://$matches[2]";
-		if(isset($_SERVER["HTTP_ORIGIN"])) $origin = $_SERVER["HTTP_ORIGIN"];
-		return $_SERVER["REQUEST_METHOD"]==$method && $origin=="$scheme://$address";
-	}
+    // Return user account changes
+    public function getUserAccount($email, $password, $action) {
+        $status = null;
+        foreach ($this->yellow->plugins->plugins as $key=>$value) {
+            if (method_exists($value["obj"], "onEditUserAccount")) {
+                $status = $value["obj"]->onEditUserAccount($email, $password, $action, $this->users);
+                if (!is_null($status)) break;
+            }
+        }
+        if (is_null($status)) {
+            $status = "ok";
+            if (!empty($password) && strlenu($password)<$this->yellow->config->get("editUserPasswordMinLength")) $status = "weak";
+            if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) $status = "invalid";
+        }
+        return $status;
+    }
+    
+    // Return user restrictions
+    public function getUserRestrictions($email, $location, $fileName) {
+        $userRestrictions = null;
+        foreach ($this->yellow->plugins->plugins as $key=>$value) {
+            if (method_exists($value["obj"], "onEditUserRestrictions")) {
+                $userRestrictions = $value["obj"]->onEditUserRestrictions($email, $location, $fileName, $this->users);
+                if (!is_null($userRestrictions)) break;
+            }
+        }
+        if (is_null($userRestrictions)) {
+            $userRestrictions = substru($location, 0, strlenu($this->users->getHome($email)))!=$this->users->getHome($email);
+            $userRestrictions |= empty($fileName) || strlenu(dirname($fileName))>128 || strlenu(basename($fileName))>128;
+        }
+        return $userRestrictions;
+    }
+    
+    // Return user language
+    public function getUserLanguage($email) {
+        $language = $this->users->getLanguage($email);
+        if (!$this->yellow->text->isLanguage($language)) $language = $this->yellow->config->get("language");
+        return $language;
+    }
+    
+    // Check if request came from same site
+    public function isRequestSameSite($method, $scheme, $address) {
+        if (preg_match("#^(\w+)://([^/]+)(.*)$#", $_SERVER["HTTP_REFERER"], $matches)) $origin = "$matches[1]://$matches[2]";
+        if (isset($_SERVER["HTTP_ORIGIN"])) $origin = $_SERVER["HTTP_ORIGIN"];
+        return $_SERVER["REQUEST_METHOD"]==$method && $origin=="$scheme://$address";
+    }
 }
-	
-class YellowResponse
-{
-	var $yellow;			//access to API
-	var $plugin;			//access to plugin
-	var $active;			//location is active? (boolean)
-	var $userEmail;			//user email
-	var $userRestrictions;	//user can change page? (boolean)
-	var $userFailedError;	//error of failed authentication
-	var $userFailedEmail;	//email of failed authentication
-	var $userFailedExpire;	//expiration time of failed authentication
-	var $rawDataSource;		//raw data of page for comparison
-	var $rawDataEdit;		//raw data of page for editing
-	var $rawDataOutput;		//raw data of dynamic output
-	var $rawDataEndOfLine;	//end of line format for raw data
-	var $language;			//response language
-	var $action;			//response action
-	var $status;			//response status
-	
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->plugin = $yellow->plugins->get("edit");
-	}
-	
-	// Return new page
-	function getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine)
-	{
-		$page = new YellowPage($this->yellow);
-		$page->setRequestInformation($scheme, $address, $base, $location, $fileName);
-		$page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0);
-		$this->editContentFile($page, "create");
-		if($this->yellow->lookup->isFileLocation($location) || $this->yellow->pages->find($page->location))
-		{
-			$page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation"));
-			$page->fileName = $this->yellow->lookup->findFileNew($page->location, $page->get("published"));
-			while($this->yellow->pages->find($page->location) || empty($page->fileName))
-			{
-				$rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData));
-				$page->rawData = $this->normaliseLines($rawData, $endOfLine);
-				$page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation"));
-				$page->fileName = $this->yellow->lookup->findFileNew($page->location, $page->get("published"));
-				if(++$pageCounter>999) break;
-			}
-			if($this->yellow->pages->find($page->location) || empty($page->fileName))
-			{
-				$page->error(500, "Page '".$page->get("title")."' is not possible!");
-			}
-		} else {
-			$page->fileName = $this->yellow->lookup->findFileNew($page->location);
-		}
-		if($this->plugin->getUserRestrictions($this->userEmail, $page->location, $page->fileName))
-		{
-			$page->error(500, "Page '".$page->get("title")."' is restricted!");
-		}
-		return $page;
-	}
-	
-	// Return modified page
-	function getPageEdit($scheme, $address, $base, $location, $fileName, $rawDataSource, $rawDataEdit, $rawDataFile, $endOfLine)
-	{
-		$page = new YellowPage($this->yellow);
-		$page->setRequestInformation($scheme, $address, $base, $location, $fileName);
-		$rawData = $this->plugin->merge->merge(
-			$this->normaliseLines($rawDataSource, $endOfLine),
-			$this->normaliseLines($rawDataEdit, $endOfLine),
-			$this->normaliseLines($rawDataFile, $endOfLine));
-		$page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0);
-		$this->editContentFile($page, "edit");
-		if(empty($page->rawData)) $page->error(500, "Page has been modified by someone else!");
-		if($this->yellow->lookup->isFileLocation($location) && !$page->isError())
-		{
-			$pageSource = new YellowPage($this->yellow);
-			$pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName);
-			$pageSource->parseData($this->normaliseLines($rawDataSource, $endOfLine), false, 0);
-			if(substrb($pageSource->rawData, 0, $pageSource->metaDataOffsetBytes) !=
-			   substrb($page->rawData, 0, $page->metaDataOffsetBytes))
-			{
-				$page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation"));
-				$page->fileName = $this->yellow->lookup->findFileNew($page->location, $page->get("published"));
-				if($page->location!=$pageSource->location)
-				{
-					if(!$this->yellow->lookup->isFileLocation($page->location) || empty($page->fileName))
-					{
-						$page->error(500, "Page '".$page->get("title")."' is not possible!");
-					} else if($this->yellow->pages->find($page->location)) {
-						$page->error(500, "Page '".$page->get("title")."' already exists!");
-					}
-				}
-			}
-		}
-		if($this->plugin->getUserRestrictions($this->userEmail, $page->location, $page->fileName))
-		{
-			$page->error(500, "Page '".$page->get("title")."' is restricted!");
-		}
-		return $page;
-	}
-	
-	// Return deleted page
-	function getPageDelete($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine)
-	{
-		$page = new YellowPage($this->yellow);
-		$page->setRequestInformation($scheme, $address, $base, $location, $fileName);
-		$page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0);
-		$this->editContentFile($page, "delete");
-		if($this->plugin->getUserRestrictions($this->userEmail, $page->location, $page->fileName))
-		{
-			$page->error(500, "Page '".$page->get("title")."' is restricted!");
-		}
-		return $page;
-	}
+    
+class YellowResponse {
+    public $yellow;             //access to API
+    public $plugin;             //access to plugin
+    public $active;             //location is active? (boolean)
+    public $userEmail;          //user email
+    public $userRestrictions;   //user can change page? (boolean)
+    public $userFailedError;    //error of failed authentication
+    public $userFailedEmail;    //email of failed authentication
+    public $userFailedExpire;   //expiration time of failed authentication
+    public $rawDataSource;      //raw data of page for comparison
+    public $rawDataEdit;        //raw data of page for editing
+    public $rawDataOutput;      //raw data of dynamic output
+    public $rawDataEndOfLine;   //end of line format for raw data
+    public $language;           //response language
+    public $action;             //response action
+    public $status;             //response status
+    
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+        $this->plugin = $yellow->plugins->get("edit");
+    }
+    
+    // Return new page
+    public function getPageNew($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) {
+        $page = new YellowPage($this->yellow);
+        $page->setRequestInformation($scheme, $address, $base, $location, $fileName);
+        $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0);
+        $this->editContentFile($page, "create");
+        if ($this->yellow->lookup->isFileLocation($location) || $this->yellow->pages->find($page->location)) {
+            $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation"));
+            $page->fileName = $this->yellow->lookup->findFileNew($page->location, $page->get("published"));
+            while ($this->yellow->pages->find($page->location) || empty($page->fileName)) {
+                $rawData = $this->yellow->toolbox->setMetaData($page->rawData, "title", $this->getTitleNext($page->rawData));
+                $page->rawData = $this->normaliseLines($rawData, $endOfLine);
+                $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation"));
+                $page->fileName = $this->yellow->lookup->findFileNew($page->location, $page->get("published"));
+                if (++$pageCounter>999) break;
+            }
+            if ($this->yellow->pages->find($page->location) || empty($page->fileName)) {
+                $page->error(500, "Page '".$page->get("title")."' is not possible!");
+            }
+        } else {
+            $page->fileName = $this->yellow->lookup->findFileNew($page->location);
+        }
+        if ($this->plugin->getUserRestrictions($this->userEmail, $page->location, $page->fileName)) {
+            $page->error(500, "Page '".$page->get("title")."' is restricted!");
+        }
+        return $page;
+    }
+    
+    // Return modified page
+    public function getPageEdit($scheme, $address, $base, $location, $fileName, $rawDataSource, $rawDataEdit, $rawDataFile, $endOfLine) {
+        $page = new YellowPage($this->yellow);
+        $page->setRequestInformation($scheme, $address, $base, $location, $fileName);
+        $rawData = $this->plugin->merge->merge(
+            $this->normaliseLines($rawDataSource, $endOfLine),
+            $this->normaliseLines($rawDataEdit, $endOfLine),
+            $this->normaliseLines($rawDataFile, $endOfLine));
+        $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0);
+        $this->editContentFile($page, "edit");
+        if (empty($page->rawData)) $page->error(500, "Page has been modified by someone else!");
+        if ($this->yellow->lookup->isFileLocation($location) && !$page->isError()) {
+            $pageSource = new YellowPage($this->yellow);
+            $pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName);
+            $pageSource->parseData($this->normaliseLines($rawDataSource, $endOfLine), false, 0);
+            if (substrb($pageSource->rawData, 0, $pageSource->metaDataOffsetBytes) !=
+               substrb($page->rawData, 0, $page->metaDataOffsetBytes)) {
+                $page->location = $this->getPageNewLocation($page->rawData, $page->location, $page->get("pageNewLocation"));
+                $page->fileName = $this->yellow->lookup->findFileNew($page->location, $page->get("published"));
+                if ($page->location!=$pageSource->location) {
+                    if (!$this->yellow->lookup->isFileLocation($page->location) || empty($page->fileName)) {
+                        $page->error(500, "Page '".$page->get("title")."' is not possible!");
+                    } elseif ($this->yellow->pages->find($page->location)) {
+                        $page->error(500, "Page '".$page->get("title")."' already exists!");
+                    }
+                }
+            }
+        }
+        if ($this->plugin->getUserRestrictions($this->userEmail, $page->location, $page->fileName)) {
+            $page->error(500, "Page '".$page->get("title")."' is restricted!");
+        }
+        return $page;
+    }
+    
+    // Return deleted page
+    public function getPageDelete($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) {
+        $page = new YellowPage($this->yellow);
+        $page->setRequestInformation($scheme, $address, $base, $location, $fileName);
+        $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 0);
+        $this->editContentFile($page, "delete");
+        if ($this->plugin->getUserRestrictions($this->userEmail, $page->location, $page->fileName)) {
+            $page->error(500, "Page '".$page->get("title")."' is restricted!");
+        }
+        return $page;
+    }
 
-	// Return preview page
-	function getPagePreview($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine)
-	{
-		$page = new YellowPage($this->yellow);
-		$page->setRequestInformation($scheme, $address, $base, $location, $fileName);
-		$page->parseData($this->normaliseLines($rawData, $endOfLine), false, 200);
-		$this->yellow->text->setLanguage($page->get("language"));
-		$page->set("pageClass", "page-preview");
-		$page->set("pageClass", $page->get("pageClass")." template-".$page->get("template"));
-		$output = "<div class=\"".$page->getHtml("pageClass")."\"><div class=\"content\">";
-		if($this->yellow->config->get("editToolbarButtons")!="none")
-		{
-			$output .= "<h1>".$page->getHtml("titleContent")."</h1>\n";
-		}
-		$output .= $page->getContent();
-		$output .= "</div></div>";
-		$page->setOutput($output);
-		return $page;
-	}
-	
-	// Return uploaded file
-	function getFileUpload($scheme, $address, $base, $pageLocation, $fileNameTemp, $fileNameShort)
-	{
-		$file = new YellowPage($this->yellow);
-		$file->setRequestInformation($scheme, $address, $base, "/".$fileNameTemp, $fileNameTemp);
-		$file->parseData(null, false, 0);
-		$file->set("fileNameShort", $fileNameShort);
-		$this->editMediaFile($file, "upload");
-		$file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation"));
-		$file->fileName = substru($file->location, 1);
-		while(is_file($file->fileName))
-		{
-			$fileNameShort = $this->getFileNext(basename($file->fileName));
-			$file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation"));
-			$file->fileName = substru($file->location, 1);
-			if(++$fileCounter>999) break;
-		}
-		if(is_file($file->fileName)) $file->error(500, "File '".$file->get("fileNameShort")."' is not possible!");
-		return $file;
-	}
+    // Return preview page
+    public function getPagePreview($scheme, $address, $base, $location, $fileName, $rawData, $endOfLine) {
+        $page = new YellowPage($this->yellow);
+        $page->setRequestInformation($scheme, $address, $base, $location, $fileName);
+        $page->parseData($this->normaliseLines($rawData, $endOfLine), false, 200);
+        $this->yellow->text->setLanguage($page->get("language"));
+        $page->set("pageClass", "page-preview");
+        $page->set("pageClass", $page->get("pageClass")." template-".$page->get("template"));
+        $output = "<div class=\"".$page->getHtml("pageClass")."\"><div class=\"content\">";
+        if ($this->yellow->config->get("editToolbarButtons")!="none") $output .= "<h1>".$page->getHtml("titleContent")."</h1>\n";
+        $output .= $page->getContent();
+        $output .= "</div></div>";
+        $page->setOutput($output);
+        return $page;
+    }
+    
+    // Return uploaded file
+    public function getFileUpload($scheme, $address, $base, $pageLocation, $fileNameTemp, $fileNameShort) {
+        $file = new YellowPage($this->yellow);
+        $file->setRequestInformation($scheme, $address, $base, "/".$fileNameTemp, $fileNameTemp);
+        $file->parseData(null, false, 0);
+        $file->set("fileNameShort", $fileNameShort);
+        $this->editMediaFile($file, "upload");
+        $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation"));
+        $file->fileName = substru($file->location, 1);
+        while (is_file($file->fileName)) {
+            $fileNameShort = $this->getFileNext(basename($file->fileName));
+            $file->location = $this->getFileNewLocation($fileNameShort, $pageLocation, $file->get("fileNewLocation"));
+            $file->fileName = substru($file->location, 1);
+            if (++$fileCounter>999) break;
+        }
+        if (is_file($file->fileName)) $file->error(500, "File '".$file->get("fileNameShort")."' is not possible!");
+        return $file;
+    }
 
-	// Return page data including status information
-	function getPageData()
-	{
-		$data = array();
-		if($this->isUser())
-		{
-			$data["title"] = $this->yellow->toolbox->getMetaData($this->rawDataEdit, "title");
-			$data["rawDataSource"] = $this->rawDataSource;
-			$data["rawDataEdit"] = $this->rawDataEdit;
-			$data["rawDataNew"] = $this->getRawDataNew();
-			$data["rawDataOutput"] = strval($this->rawDataOutput);
-			$data["rawDataEndOfLine"] = $this->rawDataEndOfLine;
-			$data["scheme"] = $this->yellow->page->scheme;
-			$data["address"] = $this->yellow->page->address;
-			$data["base"] = $this->yellow->page->base;
-			$data["location"] = $this->yellow->page->location;
-			$data["parserSafeMode"] = $this->yellow->page->parserSafeMode;
-		}
-		if($this->action!="none") $data = array_merge($data, $this->getRequestData());
-		$data["action"] = $this->action;
-		$data["status"] = $this->status;
-		$data["statusCode"] = $this->yellow->page->statusCode;
-		return $data;
-	}
-	
-	// Return configuration data including user information
-	function getConfigData()
-	{
-		$data = $this->yellow->config->getData("", "Location");
-		if($this->isUser())
-		{
-			$data["userEmail"] = $this->userEmail;
-			$data["userName"] = $this->plugin->users->getName($this->userEmail);
-			$data["userLanguage"] = $this->plugin->users->getLanguage($this->userEmail);
-			$data["userStatus"] = $this->plugin->users->getStatus($this->userEmail);
-			$data["userHome"] = $this->plugin->users->getHome($this->userEmail);
-			$data["userRestrictions"] = intval($this->isUserRestrictions());
-			$data["userWebmaster"] = intval($this->isUserWebmaster());
-			$data["serverScheme"] = $this->yellow->config->get("serverScheme");
-			$data["serverAddress"] = $this->yellow->config->get("serverAddress");
-			$data["serverBase"] = $this->yellow->config->get("serverBase");
-			$data["serverFileSizeMax"] = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize"));
-			$data["serverVersion"] = "Datenstrom Yellow ".YellowCore::VERSION;
-			$data["serverPlugins"] = array();
-			foreach($this->yellow->plugins->plugins as $key=>$value)
-			{
-				$data["serverPlugins"][$key] = $value["plugin"];
-			}
-			$data["serverLanguages"] = array();
-			foreach($this->yellow->text->getLanguages() as $language)
-			{
-				$data["serverLanguages"][$language] = $this->yellow->text->getTextHtml("languageDescription", $language);
-			}
-			$data["editUploadExtensions"] = $this->yellow->config->get("editUploadExtensions");
-			$data["editKeyboardShortcuts"] = $this->yellow->config->get("editKeyboardShortcuts");
-			$data["editToolbarButtons"] = $this->getToolbarButtons("edit");
-			$data["emojiawesomeToolbarButtons"] =  $this->getToolbarButtons("emojiawesome");
-			$data["fontawesomeToolbarButtons"] =  $this->getToolbarButtons("fontawesome");
-		} else {
-			$data["editLoginEmail"] = $this->yellow->page->get("editLoginEmail");
-			$data["editLoginPassword"] = $this->yellow->page->get("editLoginPassword");
-			$data["editLoginRestrictions"] = intval($this->isLoginRestrictions());
-		}
-		if(defined("DEBUG") && DEBUG>=1) $data["debug"] = DEBUG;
-		return $data;
-	}
-	
-	// Return request strings
-	function getRequestData()
-	{
-		$data = array();
-		foreach($_REQUEST as $key=>$value)
-		{
-			if($key=="password" || $key=="authtoken" || $key=="csrftoken" || $key=="actiontoken" || substru($key, 0, 7)=="rawdata") continue;
-			$data["request".ucfirst($key)] = trim($value);
-		}
-		return $data;
-	}
-	
-	// Return text strings
-	function getTextData()
-	{
-		$textLanguage = $this->yellow->text->getData("language", $this->language);
-		$textEdit = $this->yellow->text->getData("edit", $this->language);
-		$textYellow = $this->yellow->text->getData("yellow", $this->language);
-		return array_merge($textLanguage, $textEdit, $textYellow);
-	}
-	
-	// Return toolbar buttons
-	function getToolbarButtons($name)
-	{
-		if($name=="edit")
-		{
-			$toolbarButtons = $this->yellow->config->get("editToolbarButtons");
-			if($toolbarButtons=="auto")
-			{
-				$toolbarButtons = "";
-				if($this->yellow->plugins->isExisting("markdown")) $toolbarButtons = "preview, format, bold, italic, code, list, link, file";
-				if($this->yellow->plugins->isExisting("emojiawesome")) $toolbarButtons .= ", emojiawesome";
-				if($this->yellow->plugins->isExisting("fontawesome")) $toolbarButtons .= ", fontawesome";
-				if($this->yellow->plugins->isExisting("draft")) $toolbarButtons .= ", draft";
-				if($this->yellow->plugins->isExisting("markdown")) $toolbarButtons .= ", markdown";
-			}
-		} else {
-			$toolbarButtons = $this->yellow->config->get("{$name}ToolbarButtons");
-		}
-		return $toolbarButtons;
-	}
-	
-	// Return end of line format
-	function getEndOfLine($rawData = "")
-	{
-		$endOfLine = $this->yellow->config->get("editEndOfLine");
-		if($endOfLine=="auto")
-		{
-			$rawData = empty($rawData) ? PHP_EOL : substru($rawData, 0, 4096);
-			$endOfLine = strposu($rawData, "\r")===false ? "lf" : "crlf";
-		}
-		return $endOfLine;
-	}
-	
-	// Return raw data for new page
-	function getRawDataNew($location = "")
-	{
-		foreach($this->yellow->pages->path($this->yellow->page->location)->reverse() as $page)
-		{
-			if($page->isExisting("templateNew"))
-			{
-				$name = $this->yellow->lookup->normaliseName($page->get("templateNew"));
-				$fileName = strreplaceu("(.*)", $name, $this->yellow->config->get("configDir").$this->yellow->config->get("newFile"));
-				if(is_file($fileName)) break;
-			}
-		}
-		if(!is_file($fileName))
-		{
-			$name = $this->yellow->lookup->normaliseName($this->yellow->config->get("template"));
-			$fileName = strreplaceu("(.*)", $name, $this->yellow->config->get("configDir").$this->yellow->config->get("newFile"));
-		}
-		$rawData = $this->yellow->toolbox->readFile($fileName);
-		$rawData = preg_replace("/@timestamp/i", time(), $rawData);
-		$rawData = preg_replace("/@datetime/i", date("Y-m-d H:i:s"), $rawData);
-		$rawData = preg_replace("/@date/i", date("Y-m-d"), $rawData);
-		$rawData = preg_replace("/@usershort/i", strtok($this->plugin->users->getName($this->userEmail), " "), $rawData);
-		$rawData = preg_replace("/@username/i", $this->plugin->users->getName($this->userEmail), $rawData);
-		$rawData = preg_replace("/@userlanguage/i", $this->plugin->users->getLanguage($this->userEmail), $rawData);
-		if(!empty($location))
-		{
-			$rawData = $this->yellow->toolbox->setMetaData($rawData, "title", $this->yellow->toolbox->createTextTitle($location));
-		}
-		return $rawData;
-	}
-	
-	// Return location for new/modified page
-	function getPageNewLocation($rawData, $pageLocation, $pageNewLocation)
-	{
-		$location = empty($pageNewLocation) ? "@title" : $pageNewLocation;
-		$location = preg_replace("/@title/i", $this->getPageNewTitle($rawData), $location);
-		$location = preg_replace("/@timestamp/i", $this->getPageNewData($rawData, "published", true, "U"), $location);
-		$location = preg_replace("/@date/i", $this->getPageNewData($rawData, "published", true, "Y-m-d"), $location);
-		$location = preg_replace("/@year/i", $this->getPageNewData($rawData, "published", true, "Y"), $location);
-		$location = preg_replace("/@month/i", $this->getPageNewData($rawData, "published", true, "m"), $location);
-		$location = preg_replace("/@day/i", $this->getPageNewData($rawData, "published", true, "d"), $location);
-		$location = preg_replace("/@tag/i", $this->getPageNewData($rawData, "tag", true), $location);
-		$location = preg_replace("/@author/i", $this->getPageNewData($rawData, "author", true), $location);
-		if(!preg_match("/^\//", $location))
-		{
-			$location = $this->yellow->lookup->getDirectoryLocation($pageLocation).$location;
-		}
-		return $location;
-	}
-	
-	// Return title for new/modified page
-	function getPageNewTitle($rawData)
-	{
-		$title = $this->yellow->toolbox->getMetaData($rawData, "title");
-		$titleSlug = $this->yellow->toolbox->getMetaData($rawData, "titleSlug");
-		$value = empty($titleSlug) ? $title : $titleSlug;
-		$value = $this->yellow->lookup->normaliseName($value, true, false, true);
-		return trim(preg_replace("/-+/", "-", $value), "-");
-	}
-	
-	// Return data for new/modified page
-	function getPageNewData($rawData, $key, $filterFirst = false, $dateFormat = "")
-	{
-		$value = $this->yellow->toolbox->getMetaData($rawData, $key);
-		if($filterFirst && preg_match("/^(.*?)\,(.*)$/", $value, $matches)) $value = $matches[1];
-		if(!empty($dateFormat)) $value = date($dateFormat, strtotime($value));
-		if(strempty($value)) $value = "none";
-		$value = $this->yellow->lookup->normaliseName($value, true, false, true);
-		return trim(preg_replace("/-+/", "-", $value), "-");
-	}
+    // Return page data including status information
+    public function getPageData() {
+        $data = array();
+        if ($this->isUser()) {
+            $data["title"] = $this->yellow->toolbox->getMetaData($this->rawDataEdit, "title");
+            $data["rawDataSource"] = $this->rawDataSource;
+            $data["rawDataEdit"] = $this->rawDataEdit;
+            $data["rawDataNew"] = $this->getRawDataNew();
+            $data["rawDataOutput"] = strval($this->rawDataOutput);
+            $data["rawDataEndOfLine"] = $this->rawDataEndOfLine;
+            $data["scheme"] = $this->yellow->page->scheme;
+            $data["address"] = $this->yellow->page->address;
+            $data["base"] = $this->yellow->page->base;
+            $data["location"] = $this->yellow->page->location;
+            $data["parserSafeMode"] = $this->yellow->page->parserSafeMode;
+        }
+        if ($this->action!="none") $data = array_merge($data, $this->getRequestData());
+        $data["action"] = $this->action;
+        $data["status"] = $this->status;
+        $data["statusCode"] = $this->yellow->page->statusCode;
+        return $data;
+    }
+    
+    // Return configuration data including user information
+    public function getConfigData() {
+        $data = $this->yellow->config->getData("", "Location");
+        if ($this->isUser()) {
+            $data["userEmail"] = $this->userEmail;
+            $data["userName"] = $this->plugin->users->getName($this->userEmail);
+            $data["userLanguage"] = $this->plugin->users->getLanguage($this->userEmail);
+            $data["userStatus"] = $this->plugin->users->getStatus($this->userEmail);
+            $data["userHome"] = $this->plugin->users->getHome($this->userEmail);
+            $data["userRestrictions"] = intval($this->isUserRestrictions());
+            $data["userWebmaster"] = intval($this->isUserWebmaster());
+            $data["serverScheme"] = $this->yellow->config->get("serverScheme");
+            $data["serverAddress"] = $this->yellow->config->get("serverAddress");
+            $data["serverBase"] = $this->yellow->config->get("serverBase");
+            $data["serverFileSizeMax"] = $this->yellow->toolbox->getNumberBytes(ini_get("upload_max_filesize"));
+            $data["serverVersion"] = "Datenstrom Yellow ".YellowCore::VERSION;
+            $data["serverPlugins"] = array();
+            foreach ($this->yellow->plugins->plugins as $key=>$value) {
+                $data["serverPlugins"][$key] = $value["plugin"];
+            }
+            $data["serverLanguages"] = array();
+            foreach ($this->yellow->text->getLanguages() as $language) {
+                $data["serverLanguages"][$language] = $this->yellow->text->getTextHtml("languageDescription", $language);
+            }
+            $data["editUploadExtensions"] = $this->yellow->config->get("editUploadExtensions");
+            $data["editKeyboardShortcuts"] = $this->yellow->config->get("editKeyboardShortcuts");
+            $data["editToolbarButtons"] = $this->getToolbarButtons("edit");
+            $data["emojiawesomeToolbarButtons"] =  $this->getToolbarButtons("emojiawesome");
+            $data["fontawesomeToolbarButtons"] =  $this->getToolbarButtons("fontawesome");
+        } else {
+            $data["editLoginEmail"] = $this->yellow->page->get("editLoginEmail");
+            $data["editLoginPassword"] = $this->yellow->page->get("editLoginPassword");
+            $data["editLoginRestrictions"] = intval($this->isLoginRestrictions());
+        }
+        if (defined("DEBUG") && DEBUG>=1) $data["debug"] = DEBUG;
+        return $data;
+    }
+    
+    // Return request strings
+    public function getRequestData() {
+        $data = array();
+        foreach ($_REQUEST as $key=>$value) {
+            if ($key=="password" || $key=="authtoken" || $key=="csrftoken" || $key=="actiontoken" || substru($key, 0, 7)=="rawdata") continue;
+            $data["request".ucfirst($key)] = trim($value);
+        }
+        return $data;
+    }
+    
+    // Return text strings
+    public function getTextData() {
+        $textLanguage = $this->yellow->text->getData("language", $this->language);
+        $textEdit = $this->yellow->text->getData("edit", $this->language);
+        $textYellow = $this->yellow->text->getData("yellow", $this->language);
+        return array_merge($textLanguage, $textEdit, $textYellow);
+    }
+    
+    // Return toolbar buttons
+    public function getToolbarButtons($name) {
+        if ($name=="edit") {
+            $toolbarButtons = $this->yellow->config->get("editToolbarButtons");
+            if ($toolbarButtons=="auto") {
+                $toolbarButtons = "";
+                if ($this->yellow->plugins->isExisting("markdown")) $toolbarButtons = "preview, format, bold, italic, code, list, link, file";
+                if ($this->yellow->plugins->isExisting("emojiawesome")) $toolbarButtons .= ", emojiawesome";
+                if ($this->yellow->plugins->isExisting("fontawesome")) $toolbarButtons .= ", fontawesome";
+                if ($this->yellow->plugins->isExisting("draft")) $toolbarButtons .= ", draft";
+                if ($this->yellow->plugins->isExisting("markdown")) $toolbarButtons .= ", markdown";
+            }
+        } else {
+            $toolbarButtons = $this->yellow->config->get("{$name}ToolbarButtons");
+        }
+        return $toolbarButtons;
+    }
+    
+    // Return end of line format
+    public function getEndOfLine($rawData = "") {
+        $endOfLine = $this->yellow->config->get("editEndOfLine");
+        if ($endOfLine=="auto") {
+            $rawData = empty($rawData) ? PHP_EOL : substru($rawData, 0, 4096);
+            $endOfLine = strposu($rawData, "\r")===false ? "lf" : "crlf";
+        }
+        return $endOfLine;
+    }
+    
+    // Return raw data for new page
+    public function getRawDataNew($location = "") {
+        foreach ($this->yellow->pages->path($this->yellow->page->location)->reverse() as $page) {
+            if ($page->isExisting("templateNew")) {
+                $name = $this->yellow->lookup->normaliseName($page->get("templateNew"));
+                $fileName = strreplaceu("(.*)", $name, $this->yellow->config->get("configDir").$this->yellow->config->get("newFile"));
+                if (is_file($fileName)) break;
+            }
+        }
+        if (!is_file($fileName)) {
+            $name = $this->yellow->lookup->normaliseName($this->yellow->config->get("template"));
+            $fileName = strreplaceu("(.*)", $name, $this->yellow->config->get("configDir").$this->yellow->config->get("newFile"));
+        }
+        $rawData = $this->yellow->toolbox->readFile($fileName);
+        $rawData = preg_replace("/@timestamp/i", time(), $rawData);
+        $rawData = preg_replace("/@datetime/i", date("Y-m-d H:i:s"), $rawData);
+        $rawData = preg_replace("/@date/i", date("Y-m-d"), $rawData);
+        $rawData = preg_replace("/@usershort/i", strtok($this->plugin->users->getName($this->userEmail), " "), $rawData);
+        $rawData = preg_replace("/@username/i", $this->plugin->users->getName($this->userEmail), $rawData);
+        $rawData = preg_replace("/@userlanguage/i", $this->plugin->users->getLanguage($this->userEmail), $rawData);
+        if (!empty($location)) {
+            $rawData = $this->yellow->toolbox->setMetaData($rawData, "title", $this->yellow->toolbox->createTextTitle($location));
+        }
+        return $rawData;
+    }
+    
+    // Return location for new/modified page
+    public function getPageNewLocation($rawData, $pageLocation, $pageNewLocation) {
+        $location = empty($pageNewLocation) ? "@title" : $pageNewLocation;
+        $location = preg_replace("/@title/i", $this->getPageNewTitle($rawData), $location);
+        $location = preg_replace("/@timestamp/i", $this->getPageNewData($rawData, "published", true, "U"), $location);
+        $location = preg_replace("/@date/i", $this->getPageNewData($rawData, "published", true, "Y-m-d"), $location);
+        $location = preg_replace("/@year/i", $this->getPageNewData($rawData, "published", true, "Y"), $location);
+        $location = preg_replace("/@month/i", $this->getPageNewData($rawData, "published", true, "m"), $location);
+        $location = preg_replace("/@day/i", $this->getPageNewData($rawData, "published", true, "d"), $location);
+        $location = preg_replace("/@tag/i", $this->getPageNewData($rawData, "tag", true), $location);
+        $location = preg_replace("/@author/i", $this->getPageNewData($rawData, "author", true), $location);
+        if (!preg_match("/^\//", $location)) {
+            $location = $this->yellow->lookup->getDirectoryLocation($pageLocation).$location;
+        }
+        return $location;
+    }
+    
+    // Return title for new/modified page
+    public function getPageNewTitle($rawData) {
+        $title = $this->yellow->toolbox->getMetaData($rawData, "title");
+        $titleSlug = $this->yellow->toolbox->getMetaData($rawData, "titleSlug");
+        $value = empty($titleSlug) ? $title : $titleSlug;
+        $value = $this->yellow->lookup->normaliseName($value, true, false, true);
+        return trim(preg_replace("/-+/", "-", $value), "-");
+    }
+    
+    // Return data for new/modified page
+    public function getPageNewData($rawData, $key, $filterFirst = false, $dateFormat = "") {
+        $value = $this->yellow->toolbox->getMetaData($rawData, $key);
+        if ($filterFirst && preg_match("/^(.*?)\,(.*)$/", $value, $matches)) $value = $matches[1];
+        if (!empty($dateFormat)) $value = date($dateFormat, strtotime($value));
+        if (strempty($value)) $value = "none";
+        $value = $this->yellow->lookup->normaliseName($value, true, false, true);
+        return trim(preg_replace("/-+/", "-", $value), "-");
+    }
 
-	// Return location for new file
-	function getFileNewLocation($fileNameShort, $pageLocation, $fileNewLocation)
-	{
-		$location = empty($fileNewLocation) ? $this->yellow->config->get("editUploadNewLocation") : $fileNewLocation;
-		$location = preg_replace("/@timestamp/i", time(), $location);
-		$location = preg_replace("/@type/i", $this->yellow->toolbox->getFileType($fileNameShort), $location);
-		$location = preg_replace("/@group/i", $this->getFileNewGroup($fileNameShort), $location);
-		$location = preg_replace("/@folder/i", $this->getFileNewFolder($pageLocation), $location);
-		$location = preg_replace("/@filename/i", strtoloweru($fileNameShort), $location);
-		if(!preg_match("/^\//", $location))
-		{
-			$location = $this->yellow->config->get("mediaLocation").$location;
-		}
-		return $location;
-	}
-	
-	// Return group for new file
-	function getFileNewGroup($fileNameShort)
-	{
-		$path = $this->yellow->config->get("mediaDir");
-		$fileType = $this->yellow->toolbox->getFileType($fileNameShort);
-		$fileName = $this->yellow->config->get(preg_match("/(gif|jpg|png|svg)$/", $fileType) ? "imageDir" : "downloadDir").$fileNameShort;
-		preg_match("#^$path(.+?)\/#", $fileName, $matches);
-		return strtoloweru($matches[1]);
-	}
+    // Return location for new file
+    public function getFileNewLocation($fileNameShort, $pageLocation, $fileNewLocation) {
+        $location = empty($fileNewLocation) ? $this->yellow->config->get("editUploadNewLocation") : $fileNewLocation;
+        $location = preg_replace("/@timestamp/i", time(), $location);
+        $location = preg_replace("/@type/i", $this->yellow->toolbox->getFileType($fileNameShort), $location);
+        $location = preg_replace("/@group/i", $this->getFileNewGroup($fileNameShort), $location);
+        $location = preg_replace("/@folder/i", $this->getFileNewFolder($pageLocation), $location);
+        $location = preg_replace("/@filename/i", strtoloweru($fileNameShort), $location);
+        if (!preg_match("/^\//", $location)) {
+            $location = $this->yellow->config->get("mediaLocation").$location;
+        }
+        return $location;
+    }
+    
+    // Return group for new file
+    public function getFileNewGroup($fileNameShort) {
+        $path = $this->yellow->config->get("mediaDir");
+        $fileType = $this->yellow->toolbox->getFileType($fileNameShort);
+        $fileName = $this->yellow->config->get(preg_match("/(gif|jpg|png|svg)$/", $fileType) ? "imageDir" : "downloadDir").$fileNameShort;
+        preg_match("#^$path(.+?)\/#", $fileName, $matches);
+        return strtoloweru($matches[1]);
+    }
 
-	// Return folder for new file
-	function getFileNewFolder($pageLocation)
-	{
-		$parentTopLocation = $this->yellow->pages->getParentTopLocation($pageLocation);
-		if($parentTopLocation==$this->yellow->pages->getHomeLocation($pageLocation)) $parentTopLocation .= "home";
-		return strtoloweru(trim($parentTopLocation, '/'));
-	}
-	
-	// Return next title
-	function getTitleNext($rawData)
-	{
-		preg_match("/^(.*?)(\d*)$/", $this->yellow->toolbox->getMetaData($rawData, "title"), $matches);
-		$titleText = $matches[1];
-		$titleNumber = strempty($matches[2]) ? " 2" : $matches[2]+1;
-		return $titleText.$titleNumber;
-	}
-	
-	// Return next file name
-	function getFileNext($fileNameShort)
-	{
-		preg_match("/^(.*?)(\d*)(\..*?)?$/", $fileNameShort, $matches);
-		$fileText = $matches[1];
-		$fileNumber = strempty($matches[2]) ? "-2" : $matches[2]+1;
-		$fileExtension = $matches[3];
-		return $fileText.$fileNumber.$fileExtension;
-	}
+    // Return folder for new file
+    public function getFileNewFolder($pageLocation) {
+        $parentTopLocation = $this->yellow->pages->getParentTopLocation($pageLocation);
+        if ($parentTopLocation==$this->yellow->pages->getHomeLocation($pageLocation)) $parentTopLocation .= "home";
+        return strtoloweru(trim($parentTopLocation, "/"));
+    }
+    
+    // Return next title
+    public function getTitleNext($rawData) {
+        preg_match("/^(.*?)(\d*)$/", $this->yellow->toolbox->getMetaData($rawData, "title"), $matches);
+        $titleText = $matches[1];
+        $titleNumber = strempty($matches[2]) ? " 2" : $matches[2]+1;
+        return $titleText.$titleNumber;
+    }
+    
+    // Return next file name
+    public function getFileNext($fileNameShort) {
+        preg_match("/^(.*?)(\d*)(\..*?)?$/", $fileNameShort, $matches);
+        $fileText = $matches[1];
+        $fileNumber = strempty($matches[2]) ? "-2" : $matches[2]+1;
+        $fileExtension = $matches[3];
+        return $fileText.$fileNumber.$fileExtension;
+    }
 
-	// Normalise text lines, convert line endings
-	function normaliseLines($text, $endOfLine = "lf")
-	{
-		if($endOfLine=="lf")
-		{
-			$text = preg_replace("/\R/u", "\n", $text);
-		} else {
-			$text = preg_replace("/\R/u", "\r\n", $text);
-		}
-		return $text;
-	}
-	
-	// Create browser cookies
-	function createCookies($scheme, $address, $base, $email)
-	{
-		$expire = time() + $this->yellow->config->get("editLoginSessionTimeout");
-		$authToken = $this->plugin->users->createAuthToken($email, $expire);
-		$csrfToken = $this->plugin->users->createCsrfToken();
-		setcookie("authtoken", $authToken, $expire, "$base/", "", $scheme=="https", true);
-		setcookie("csrftoken", $csrfToken, $expire, "$base/", "", $scheme=="https", false);
-	}
-	
-	// Destroy browser cookies
-	function destroyCookies($scheme, $address, $base)
-	{
-		setcookie("authtoken", "", 1, "$base/", "", $scheme=="https", true);
-		setcookie("csrftoken", "", 1, "$base/", "", $scheme=="https", false);
-	}
-	
-	// Send mail to user
-	function sendMail($scheme, $address, $base, $email, $action)
-	{
-		if($action=="welcome" || $action=="goodbye")
-		{
-			$url = "$scheme://$address$base/";
-		} else {
-			$expire = time() + 60*60*24;
-			$actionToken = $this->plugin->users->createActionToken($email, $action, $expire);
-			$url = "$scheme://$address$base"."/action:$action/email:$email/expire:$expire/actiontoken:$actionToken/";
-		}
-		if($action=="approve")
-		{
-			$account = $email;
-			$name = $this->yellow->config->get("author");
-			$email = $this->yellow->config->get("email");
-		} else {
-			$account = $email;
-			$name = $this->plugin->users->getName($email);
-		}
-		$language = $this->plugin->users->getLanguage($email);
-		if(!$this->yellow->text->isLanguage($language)) $language = $this->yellow->config->get("language");
-		$sitename = $this->yellow->config->get("sitename");
-		$prefix = "edit".ucfirst($action);
-		$message = $this->yellow->text->getText("{$prefix}Message", $language);
-		$message = strreplaceu("\\n", "\n", $message);
-		$message = preg_replace("/@useraccount/i", $account, $message);
-		$message = preg_replace("/@usershort/i", strtok($name, " "), $message);
-		$message = preg_replace("/@username/i", $name, $message);
-		$message = preg_replace("/@userlanguage/i", $language, $message);
-		$mailTo = mb_encode_mimeheader("$name")." <$email>";
-		$mailSubject = mb_encode_mimeheader($this->yellow->text->getText("{$prefix}Subject", $language));
-		$mailHeaders = mb_encode_mimeheader("From: $sitename")." <noreply>\r\n";
-		$mailHeaders .= mb_encode_mimeheader("X-Request-Url: $scheme://$address$base")."\r\n";
-		$mailHeaders .= "Mime-Version: 1.0\r\n";
-		$mailHeaders .= "Content-Type: text/plain; charset=utf-8\r\n";
-		$mailMessage = "$message\r\n\r\n$url\r\n-- \r\n$sitename";
-		return mail($mailTo, $mailSubject, $mailMessage, $mailHeaders);
-	}
-	
-	// Change content file
-	function editContentFile($page, $action)
-	{
-		if(!$page->isError())
-		{
-			foreach($this->yellow->plugins->plugins as $key=>$value)
-			{
-				if(method_exists($value["obj"], "onEditContentFile")) $value["obj"]->onEditContentFile($page, $action);
-			}
-		}
-	}
+    // Normalise text lines, convert line endings
+    public function normaliseLines($text, $endOfLine = "lf") {
+        if ($endOfLine=="lf") {
+            $text = preg_replace("/\R/u", "\n", $text);
+        } else {
+            $text = preg_replace("/\R/u", "\r\n", $text);
+        }
+        return $text;
+    }
+    
+    // Create browser cookies
+    public function createCookies($scheme, $address, $base, $email) {
+        $expire = time() + $this->yellow->config->get("editLoginSessionTimeout");
+        $authToken = $this->plugin->users->createAuthToken($email, $expire);
+        $csrfToken = $this->plugin->users->createCsrfToken();
+        setcookie("authtoken", $authToken, $expire, "$base/", "", $scheme=="https", true);
+        setcookie("csrftoken", $csrfToken, $expire, "$base/", "", $scheme=="https", false);
+    }
+    
+    // Destroy browser cookies
+    public function destroyCookies($scheme, $address, $base) {
+        setcookie("authtoken", "", 1, "$base/", "", $scheme=="https", true);
+        setcookie("csrftoken", "", 1, "$base/", "", $scheme=="https", false);
+    }
+    
+    // Send mail to user
+    public function sendMail($scheme, $address, $base, $email, $action) {
+        if ($action=="welcome" || $action=="goodbye") {
+            $url = "$scheme://$address$base/";
+        } else {
+            $expire = time() + 60*60*24;
+            $actionToken = $this->plugin->users->createActionToken($email, $action, $expire);
+            $url = "$scheme://$address$base"."/action:$action/email:$email/expire:$expire/actiontoken:$actionToken/";
+        }
+        if ($action=="approve") {
+            $account = $email;
+            $name = $this->yellow->config->get("author");
+            $email = $this->yellow->config->get("email");
+        } else {
+            $account = $email;
+            $name = $this->plugin->users->getName($email);
+        }
+        $language = $this->plugin->users->getLanguage($email);
+        if (!$this->yellow->text->isLanguage($language)) $language = $this->yellow->config->get("language");
+        $sitename = $this->yellow->config->get("sitename");
+        $prefix = "edit".ucfirst($action);
+        $message = $this->yellow->text->getText("{$prefix}Message", $language);
+        $message = strreplaceu("\\n", "\n", $message);
+        $message = preg_replace("/@useraccount/i", $account, $message);
+        $message = preg_replace("/@usershort/i", strtok($name, " "), $message);
+        $message = preg_replace("/@username/i", $name, $message);
+        $message = preg_replace("/@userlanguage/i", $language, $message);
+        $mailTo = mb_encode_mimeheader("$name")." <$email>";
+        $mailSubject = mb_encode_mimeheader($this->yellow->text->getText("{$prefix}Subject", $language));
+        $mailHeaders = mb_encode_mimeheader("From: $sitename")." <noreply>\r\n";
+        $mailHeaders .= mb_encode_mimeheader("X-Request-Url: $scheme://$address$base")."\r\n";
+        $mailHeaders .= "Mime-Version: 1.0\r\n";
+        $mailHeaders .= "Content-Type: text/plain; charset=utf-8\r\n";
+        $mailMessage = "$message\r\n\r\n$url\r\n-- \r\n$sitename";
+        return mail($mailTo, $mailSubject, $mailMessage, $mailHeaders);
+    }
+    
+    // Change content file
+    public function editContentFile($page, $action) {
+        if (!$page->isError()) {
+            foreach ($this->yellow->plugins->plugins as $key=>$value) {
+                if (method_exists($value["obj"], "onEditContentFile")) $value["obj"]->onEditContentFile($page, $action);
+            }
+        }
+    }
 
-	// Change media file
-	function editMediaFile($file, $action)
-	{
-		if(!$file->isError())
-		{
-			foreach($this->yellow->plugins->plugins as $key=>$value)
-			{
-				if(method_exists($value["obj"], "onEditMediaFile")) $value["obj"]->onEditMediaFile($file, $action);
-			}
-		}
-	}
-	
-	// Check if active
-	function isActive()
-	{
-		return $this->active;
-	}
-	
-	// Check if user is logged in
-	function isUser()
-	{
-		return !empty($this->userEmail);
-	}
-	
-	// Check if user has restrictions
-	function isUserRestrictions()
-	{
-		return empty($this->userEmail) || $this->userRestrictions;
-	}
-	
-	// Check if user is webmaster
-	function isUserWebmaster()
-	{
-		return !empty($this->userEmail) && $this->userEmail==$this->yellow->config->get("email");
-	}
-	
-	// Check if login has restrictions
-	function isLoginRestrictions()
-	{
-		return $this->yellow->config->get("editLoginRestrictions");
-	}
+    // Change media file
+    public function editMediaFile($file, $action) {
+        if (!$file->isError()) {
+            foreach ($this->yellow->plugins->plugins as $key=>$value) {
+                if (method_exists($value["obj"], "onEditMediaFile")) $value["obj"]->onEditMediaFile($file, $action);
+            }
+        }
+    }
+    
+    // Check if active
+    public function isActive() {
+        return $this->active;
+    }
+    
+    // Check if user is logged in
+    public function isUser() {
+        return !empty($this->userEmail);
+    }
+    
+    // Check if user has restrictions
+    public function isUserRestrictions() {
+        return empty($this->userEmail) || $this->userRestrictions;
+    }
+    
+    // Check if user is webmaster
+    public function isUserWebmaster() {
+        return !empty($this->userEmail) && $this->userEmail==$this->yellow->config->get("email");
+    }
+    
+    // Check if login has restrictions
+    public function isLoginRestrictions() {
+        return $this->yellow->config->get("editLoginRestrictions");
+    }
 }
 
-class YellowUsers
-{
-	var $yellow;	//access to API
-	var $users;		//registered users
-	
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->users = array();
-	}
+class YellowUsers {
+    public $yellow;     //access to API
+    public $users;      //registered users
+    
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+        $this->users = array();
+    }
 
-	// Load users from file
-	function load($fileName)
-	{
-		if(defined("DEBUG") && DEBUG>=2) echo "YellowUsers::load file:$fileName<br/>\n";
-		$fileData = $this->yellow->toolbox->readFile($fileName);
-		foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-		{
-			if(preg_match("/^\#/", $line)) continue;
-			preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-			if(!empty($matches[1]) && !empty($matches[2]))
-			{
-				list($hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) = explode(',', $matches[2]);
-				$this->set($matches[1], $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home);
-				if(defined("DEBUG") && DEBUG>=3) echo "YellowUsers::load email:$matches[1]<br/>\n";
-			}
-		}
-	}
+    // Load users from file
+    public function load($fileName) {
+        if (defined("DEBUG") && DEBUG>=2) echo "YellowUsers::load file:$fileName<br/>\n";
+        $fileData = $this->yellow->toolbox->readFile($fileName);
+        foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+            if (preg_match("/^\#/", $line)) continue;
+            preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+            if (!empty($matches[1]) && !empty($matches[2])) {
+                list($hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) = explode(",", $matches[2]);
+                $this->set($matches[1], $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home);
+                if (defined("DEBUG") && DEBUG>=3) echo "YellowUsers::load email:$matches[1]<br/>\n";
+            }
+        }
+    }
 
-	// Save user to file
-	function save($fileName, $email, $password = "", $name = "", $language = "", $status = "", $stamp = "", $modified = "", $errors = "", $pending = "", $home = "")
-	{
-		if(!empty($password)) $hash = $this->createHash($password);
-		if($this->isExisting($email))
-		{
-			$email = strreplaceu(',', '-', $email);
-			$hash = strreplaceu(',', '-', empty($hash) ? $this->users[$email]["hash"] : $hash);
-			$name = strreplaceu(',', '-', empty($name) ? $this->users[$email]["name"] : $name);
-			$language = strreplaceu(',', '-', empty($language) ? $this->users[$email]["language"] : $language);
-			$status = strreplaceu(',', '-', empty($status) ? $this->users[$email]["status"] : $status);
-			$stamp = strreplaceu(',', '-', empty($stamp) ? $this->users[$email]["stamp"] : $stamp);
-			$modified = strreplaceu(',', '-', empty($modified) ? time() : $modified);
-			$errors = strreplaceu(',', '-', empty($errors) ? "0" : $errors);
-			$pending = strreplaceu(',', '-', empty($pending) ? $this->users[$email]["pending"] : $pending);
-			$home = strreplaceu(',', '-', empty($home) ? $this->users[$email]["home"] : $home);
-		} else {
-			$email = strreplaceu(',', '-', empty($email) ? "none" : $email);
-			$hash = strreplaceu(',', '-', empty($hash) ? "none" : $hash);
-			$name = strreplaceu(',', '-', empty($name) ? $this->yellow->config->get("sitename") : $name);
-			$language = strreplaceu(',', '-', empty($language) ? $this->yellow->config->get("language") : $language);
-			$status = strreplaceu(',', '-', empty($status) ? "active" : $status);
-			$stamp = strreplaceu(',', '-', empty($stamp) ? $this->createStamp() : $stamp);
-			$modified = strreplaceu(',', '-', empty($modified) ? time() : $modified);
-			$errors = strreplaceu(',', '-', empty($errors) ? "0" : $errors);
-			$pending = strreplaceu(',', '-', empty($pending) ? "none" : $pending);
-			$home = strreplaceu(',', '-', empty($home) ? $this->yellow->config->get("editUserHome") : $home);
-		}
-		$this->set($email, $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home);
-		$fileData = $this->yellow->toolbox->readFile($fileName);
-		foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-		{
-			preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-			if(!empty($matches[1]) && $matches[1]==$email)
-			{
-				$fileDataNew .= "$email: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n";
-				$found = true;
-			} else {
-				$fileDataNew .= $line;
-			}
-		}
-		if(!$found) $fileDataNew .= "$email: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n";
-		return $this->yellow->toolbox->createFile($fileName, $fileDataNew);
-	}
-	
-	// Remove user from file
-	function remove($fileName, $email)
-	{
-		unset($this->users[$email]);
-		$fileData = $this->yellow->toolbox->readFile($fileName);
-		foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-		{
-			preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-			if(!empty($matches[1]) && !empty($matches[2]) && $matches[1]!=$email)
-			{
-				$fileDataNew .= $line;
-			}
-		}
-		return $this->yellow->toolbox->createFile($fileName, $fileDataNew);
-	}
-	
-	// Set user data
-	function set($email, $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home)
-	{
-		$this->users[$email] = array();
-		$this->users[$email]["email"] = $email;
-		$this->users[$email]["hash"] = $hash;
-		$this->users[$email]["name"] = $name;
-		$this->users[$email]["language"] = $language;
-		$this->users[$email]["status"] = $status;
-		$this->users[$email]["stamp"] = $stamp;
-		$this->users[$email]["modified"] = $modified;
-		$this->users[$email]["errors"] = $errors;
-		$this->users[$email]["pending"] = $pending;
-		$this->users[$email]["home"] = $home;
-	}
-	
-	// Check user authentication from email and password
-	function checkAuthLogin($email, $password)
-	{
-		$algorithm = $this->yellow->config->get("editUserHashAlgorithm");
-		return $this->isExisting($email) && $this->users[$email]["status"]=="active" &&
-			$this->yellow->toolbox->verifyHash($password, $algorithm, $this->users[$email]["hash"]);
-	}
+    // Save user to file
+    public function save($fileName, $email, $password = "", $name = "", $language = "", $status = "", $stamp = "", $modified = "", $errors = "", $pending = "", $home = "") {
+        if (!empty($password)) $hash = $this->createHash($password);
+        if ($this->isExisting($email)) {
+            $email = strreplaceu(",", "-", $email);
+            $hash = strreplaceu(",", "-", empty($hash) ? $this->users[$email]["hash"] : $hash);
+            $name = strreplaceu(",", "-", empty($name) ? $this->users[$email]["name"] : $name);
+            $language = strreplaceu(",", "-", empty($language) ? $this->users[$email]["language"] : $language);
+            $status = strreplaceu(",", "-", empty($status) ? $this->users[$email]["status"] : $status);
+            $stamp = strreplaceu(",", "-", empty($stamp) ? $this->users[$email]["stamp"] : $stamp);
+            $modified = strreplaceu(",", "-", empty($modified) ? time() : $modified);
+            $errors = strreplaceu(",", "-", empty($errors) ? "0" : $errors);
+            $pending = strreplaceu(",", "-", empty($pending) ? $this->users[$email]["pending"] : $pending);
+            $home = strreplaceu(",", "-", empty($home) ? $this->users[$email]["home"] : $home);
+        } else {
+            $email = strreplaceu(",", "-", empty($email) ? "none" : $email);
+            $hash = strreplaceu(",", "-", empty($hash) ? "none" : $hash);
+            $name = strreplaceu(",", "-", empty($name) ? $this->yellow->config->get("sitename") : $name);
+            $language = strreplaceu(",", "-", empty($language) ? $this->yellow->config->get("language") : $language);
+            $status = strreplaceu(",", "-", empty($status) ? "active" : $status);
+            $stamp = strreplaceu(",", "-", empty($stamp) ? $this->createStamp() : $stamp);
+            $modified = strreplaceu(",", "-", empty($modified) ? time() : $modified);
+            $errors = strreplaceu(",", "-", empty($errors) ? "0" : $errors);
+            $pending = strreplaceu(",", "-", empty($pending) ? "none" : $pending);
+            $home = strreplaceu(",", "-", empty($home) ? $this->yellow->config->get("editUserHome") : $home);
+        }
+        $this->set($email, $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home);
+        $fileData = $this->yellow->toolbox->readFile($fileName);
+        foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+            preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+            if (!empty($matches[1]) && $matches[1]==$email) {
+                $fileDataNew .= "$email: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n";
+                $found = true;
+            } else {
+                $fileDataNew .= $line;
+            }
+        }
+        if (!$found) $fileDataNew .= "$email: $hash,$name,$language,$status,$stamp,$modified,$errors,$pending,$home\n";
+        return $this->yellow->toolbox->createFile($fileName, $fileDataNew);
+    }
+    
+    // Remove user from file
+    public function remove($fileName, $email) {
+        unset($this->users[$email]);
+        $fileData = $this->yellow->toolbox->readFile($fileName);
+        foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+            preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+            if (!empty($matches[1]) && !empty($matches[2]) && $matches[1]!=$email) $fileDataNew .= $line;
+        }
+        return $this->yellow->toolbox->createFile($fileName, $fileDataNew);
+    }
+    
+    // Set user data
+    public function set($email, $hash, $name, $language, $status, $stamp, $modified, $errors, $pending, $home) {
+        $this->users[$email] = array();
+        $this->users[$email]["email"] = $email;
+        $this->users[$email]["hash"] = $hash;
+        $this->users[$email]["name"] = $name;
+        $this->users[$email]["language"] = $language;
+        $this->users[$email]["status"] = $status;
+        $this->users[$email]["stamp"] = $stamp;
+        $this->users[$email]["modified"] = $modified;
+        $this->users[$email]["errors"] = $errors;
+        $this->users[$email]["pending"] = $pending;
+        $this->users[$email]["home"] = $home;
+    }
+    
+    // Check user authentication from email and password
+    public function checkAuthLogin($email, $password) {
+        $algorithm = $this->yellow->config->get("editUserHashAlgorithm");
+        return $this->isExisting($email) && $this->users[$email]["status"]=="active" &&
+            $this->yellow->toolbox->verifyHash($password, $algorithm, $this->users[$email]["hash"]);
+    }
 
-	// Check user authentication from tokens
-	function checkAuthToken($authToken, $csrfTokenExpected, $csrfTokenReceived, $ignoreCsrfToken)
-	{
-		$signature = "$5y$".substrb($authToken, 0, 96);
-		$email = $this->getAuthEmail($authToken);
-		$expire = $this->getAuthExpire($authToken);
-		return $expire>time() && $this->isExisting($email) && $this->users[$email]["status"]=="active" &&
-			$this->yellow->toolbox->verifyHash($this->users[$email]["hash"]."auth".$expire, "sha256", $signature) &&
-			($this->yellow->toolbox->verifyToken($csrfTokenExpected, $csrfTokenReceived) || $ignoreCsrfToken);
-	}
-	
-	// Check action token
-	function checkActionToken($actionToken, $email, $action, $expire)
-	{
-		$signature = "$5y$".$actionToken;
-		return $expire>time() && $this->isExisting($email) &&
-			$this->yellow->toolbox->verifyHash($this->users[$email]["hash"].$action.$expire, "sha256", $signature);
-	}
-		   
-	// Create authentication token
-	function createAuthToken($email, $expire)
-	{
-		$signature = $this->yellow->toolbox->createHash($this->users[$email]["hash"]."auth".$expire, "sha256");
-		if(empty($signature)) $signature = "padd"."error-hash-algorithm-sha256";
-		return substrb($signature, 4).$this->getStamp($email).dechex($expire);
-	}
-	
-	// Create action token
-	function createActionToken($email, $action, $expire)
-	{
-		$signature = $this->yellow->toolbox->createHash($this->users[$email]["hash"].$action.$expire, "sha256");
-		if(empty($signature)) $signature = "padd"."error-hash-algorithm-sha256";
-		return substrb($signature, 4);
-	}
-	
-	// Create CSRF token
-	function createCsrfToken()
-	{
-		return $this->yellow->toolbox->createSalt(64);
-	}
-	
-	// Create password hash
-	function createHash($password)
-	{
-		$algorithm = $this->yellow->config->get("editUserHashAlgorithm");
-		$cost = $this->yellow->config->get("editUserHashCost");
-		$hash = $this->yellow->toolbox->createHash($password, $algorithm, $cost);
-		if(empty($hash)) $hash = "error-hash-algorithm-$algorithm";
-		return $hash;
-	}
-	
-	// Create user stamp
-	function createStamp()
-	{
-		$stamp = $this->yellow->toolbox->createSalt(20);
-		while($this->getAuthEmail("none", $stamp)) $stamp = $this->yellow->toolbox->createSalt(20);
-		return $stamp;
-	}
-	
-	// Return user email from authentication, timing attack safe email lookup
-	function getAuthEmail($authToken, $stamp = "")
-	{
-		if(empty($stamp)) $stamp = substrb($authToken, 96, 20);
-		foreach($this->users as $key=>$value)
-		{
-			if($this->yellow->toolbox->verifyToken($value["stamp"], $stamp)) $email = $key;
-		}
-		return $email;
-	}
-	
-	// Return expiration time from authentication
-	function getAuthExpire($authToken)
-	{
-		return hexdec(substrb($authToken, 96+20));
-	}
-	
-	// Return user hash
-	function getHash($email)
-	{
-		return $this->isExisting($email) ? $this->users[$email]["hash"] : "";
-	}
-	
-	// Return user name
-	function getName($email)
-	{
-		return $this->isExisting($email) ? $this->users[$email]["name"] : "";
-	}
+    // Check user authentication from tokens
+    public function checkAuthToken($authToken, $csrfTokenExpected, $csrfTokenReceived, $ignoreCsrfToken) {
+        $signature = "$5y$".substrb($authToken, 0, 96);
+        $email = $this->getAuthEmail($authToken);
+        $expire = $this->getAuthExpire($authToken);
+        return $expire>time() && $this->isExisting($email) && $this->users[$email]["status"]=="active" &&
+            $this->yellow->toolbox->verifyHash($this->users[$email]["hash"]."auth".$expire, "sha256", $signature) &&
+            ($this->yellow->toolbox->verifyToken($csrfTokenExpected, $csrfTokenReceived) || $ignoreCsrfToken);
+    }
+    
+    // Check action token
+    public function checkActionToken($actionToken, $email, $action, $expire) {
+        $signature = "$5y$".$actionToken;
+        return $expire>time() && $this->isExisting($email) &&
+            $this->yellow->toolbox->verifyHash($this->users[$email]["hash"].$action.$expire, "sha256", $signature);
+    }
+           
+    // Create authentication token
+    public function createAuthToken($email, $expire) {
+        $signature = $this->yellow->toolbox->createHash($this->users[$email]["hash"]."auth".$expire, "sha256");
+        if (empty($signature)) $signature = "padd"."error-hash-algorithm-sha256";
+        return substrb($signature, 4).$this->getStamp($email).dechex($expire);
+    }
+    
+    // Create action token
+    public function createActionToken($email, $action, $expire) {
+        $signature = $this->yellow->toolbox->createHash($this->users[$email]["hash"].$action.$expire, "sha256");
+        if (empty($signature)) $signature = "padd"."error-hash-algorithm-sha256";
+        return substrb($signature, 4);
+    }
+    
+    // Create CSRF token
+    public function createCsrfToken() {
+        return $this->yellow->toolbox->createSalt(64);
+    }
+    
+    // Create password hash
+    public function createHash($password) {
+        $algorithm = $this->yellow->config->get("editUserHashAlgorithm");
+        $cost = $this->yellow->config->get("editUserHashCost");
+        $hash = $this->yellow->toolbox->createHash($password, $algorithm, $cost);
+        if (empty($hash)) $hash = "error-hash-algorithm-$algorithm";
+        return $hash;
+    }
+    
+    // Create user stamp
+    public function createStamp() {
+        $stamp = $this->yellow->toolbox->createSalt(20);
+        while ($this->getAuthEmail("none", $stamp)) {
+            $stamp = $this->yellow->toolbox->createSalt(20);
+        }
+        return $stamp;
+    }
+    
+    // Return user email from authentication, timing attack safe email lookup
+    public function getAuthEmail($authToken, $stamp = "") {
+        if (empty($stamp)) $stamp = substrb($authToken, 96, 20);
+        foreach ($this->users as $key=>$value) {
+            if ($this->yellow->toolbox->verifyToken($value["stamp"], $stamp)) $email = $key;
+        }
+        return $email;
+    }
+    
+    // Return expiration time from authentication
+    public function getAuthExpire($authToken) {
+        return hexdec(substrb($authToken, 96+20));
+    }
+    
+    // Return user hash
+    public function getHash($email) {
+        return $this->isExisting($email) ? $this->users[$email]["hash"] : "";
+    }
+    
+    // Return user name
+    public function getName($email) {
+        return $this->isExisting($email) ? $this->users[$email]["name"] : "";
+    }
 
-	// Return user language
-	function getLanguage($email)
-	{
-		return $this->isExisting($email) ? $this->users[$email]["language"] : "";
-	}	
-	
-	// Return user status
-	function getStatus($email)
-	{
-		return $this->isExisting($email) ? $this->users[$email]["status"] : "";
-	}
-	
-	// Return user stamp
-	function getStamp($email)
-	{
-		return $this->isExisting($email) ? $this->users[$email]["stamp"] : "";
-	}
-	
-	// Return user modified
-	function getModified($email)
-	{
-		return $this->isExisting($email) ? $this->users[$email]["modified"] : "";
-	}
+    // Return user language
+    public function getLanguage($email) {
+        return $this->isExisting($email) ? $this->users[$email]["language"] : "";
+    }
+    
+    // Return user status
+    public function getStatus($email) {
+        return $this->isExisting($email) ? $this->users[$email]["status"] : "";
+    }
+    
+    // Return user stamp
+    public function getStamp($email) {
+        return $this->isExisting($email) ? $this->users[$email]["stamp"] : "";
+    }
+    
+    // Return user modified
+    public function getModified($email) {
+        return $this->isExisting($email) ? $this->users[$email]["modified"] : "";
+    }
 
-	// Return user errors
-	function getErrors($email)
-	{
-		return $this->isExisting($email) ? $this->users[$email]["errors"] : "";
-	}
+    // Return user errors
+    public function getErrors($email) {
+        return $this->isExisting($email) ? $this->users[$email]["errors"] : "";
+    }
 
-	// Return user pending
-	function getPending($email)
-	{
-		return $this->isExisting($email) ? $this->users[$email]["pending"] : "";
-	}
-	
-	// Return user home
-	function getHome($email)
-	{
-		return $this->isExisting($email) ? $this->users[$email]["home"] : "";
-	}
-	
-	// Return number of users
-	function getNumber()
-	{
-		return count($this->users);
-	}
+    // Return user pending
+    public function getPending($email) {
+        return $this->isExisting($email) ? $this->users[$email]["pending"] : "";
+    }
+    
+    // Return user home
+    public function getHome($email) {
+        return $this->isExisting($email) ? $this->users[$email]["home"] : "";
+    }
+    
+    // Return number of users
+    public function getNumber() {
+        return count($this->users);
+    }
 
-	// Return user data
-	function getData()
-	{
-		$data = array();
-		foreach($this->users as $key=>$value)
-		{
-			$name = $value["name"]; if(preg_match("/\s/", $name)) $name = "\"$name\"";
-			$status = $value["status"]; if(preg_match("/\s/", $status)) $status = "\"$status\"";
-			$data[$key] = "$value[email] $name $status";
-		}
-		uksort($data, "strnatcasecmp");
-		return $data;
-	}
-	
-	// Check if user is taken
-	function isTaken($email)
-	{
-		$taken = false;
-		if($this->isExisting($email))
-		{
-			$status = $this->users[$email]["status"];
-			$reserved = $this->users[$email]["modified"] + 60*60*24;
-			if($status=="active" || $status=="inactive" || $reserved>time()) $taken = true;
-		}
-		return $taken;
-	}
-	
-	// Check if user exists
-	function isExisting($email)
-	{
-		return !is_null($this->users[$email]);
-	}
+    // Return user data
+    public function getData() {
+        $data = array();
+        foreach ($this->users as $key=>$value) {
+            $name = $value["name"];
+            $status = $value["status"];
+            if (preg_match("/\s/", $name)) $name = "\"$name\"";
+            if (preg_match("/\s/", $status)) $status = "\"$status\"";
+            $data[$key] = "$value[email] $name $status";
+        }
+        uksort($data, "strnatcasecmp");
+        return $data;
+    }
+    
+    // Check if user is taken
+    public function isTaken($email) {
+        $taken = false;
+        if ($this->isExisting($email)) {
+            $status = $this->users[$email]["status"];
+            $reserved = $this->users[$email]["modified"] + 60*60*24;
+            if ($status=="active" || $status=="inactive" || $reserved>time()) $taken = true;
+        }
+        return $taken;
+    }
+    
+    // Check if user exists
+    public function isExisting($email) {
+        return !is_null($this->users[$email]);
+    }
 }
-	
-class YellowMerge
-{
-	var $yellow;		//access to API
-	const ADD = '+';	//merge types
-	const MODIFY = '*';
-	const REMOVE = '-';
-	const SAME = ' ';
-	
-	function __construct($yellow)
-	{
-		$this->yellow = $yellow;
-	}
-	
-	// Merge text, null if not possible
-	function merge($textSource, $textMine, $textYours, $showDiff = false)
-	{
-		if($textMine!=$textYours)
-		{
-			$diffMine = $this->buildDiff($textSource, $textMine);
-			$diffYours = $this->buildDiff($textSource, $textYours);
-			$diff = $this->mergeDiff($diffMine, $diffYours);
-			$output = $this->getOutput($diff, $showDiff);
-		} else {
-			$output = $textMine;
-		}
-		return $output;
-	}
-	
-	// Build differences to common source
-	function buildDiff($textSource, $textOther)
-	{
-		$diff = array();
-		$lastRemove = -1;
-		$textStart = 0;
-		$textSource = $this->yellow->toolbox->getTextLines($textSource);
-		$textOther = $this->yellow->toolbox->getTextLines($textOther);
-		$sourceEnd = $sourceSize = count($textSource);
-		$otherEnd = $otherSize = count($textOther);
-		while($textStart<$sourceEnd && $textStart<$otherEnd && $textSource[$textStart]==$textOther[$textStart]) ++$textStart;
-		while($textStart<$sourceEnd && $textStart<$otherEnd && $textSource[$sourceEnd-1]==$textOther[$otherEnd-1])
-		{
-			--$sourceEnd; --$otherEnd;
-		}
-		for($pos=0; $pos<$textStart; ++$pos) array_push($diff, array(YellowMerge::SAME, $textSource[$pos], false));
-		$lcs = $this->buildDiffLCS($textSource, $textOther, $textStart, $sourceEnd-$textStart, $otherEnd-$textStart);
-		for($x=0,$y=0,$xEnd=$otherEnd-$textStart,$yEnd=$sourceEnd-$textStart; $x<$xEnd || $y<$yEnd;)
-		{
-			$max = $lcs[$y][$x];
-			if($y<$yEnd && $lcs[$y+1][$x]==$max)
-			{
-				array_push($diff, array(YellowMerge::REMOVE, $textSource[$textStart+$y], false));
-				if($lastRemove==-1) $lastRemove = count($diff)-1;
-				++$y;
-				continue;
-			}
-			if($x<$xEnd && $lcs[$y][$x+1]==$max)
-			{
-				if($lastRemove==-1 || $diff[$lastRemove][0]!=YellowMerge::REMOVE)
-				{
-					array_push($diff, array(YellowMerge::ADD, $textOther[$textStart+$x], false));
-					$lastRemove = -1;
-				} else {
-					$diff[$lastRemove] = array(YellowMerge::MODIFY, $textOther[$textStart+$x], false);
-					++$lastRemove; if(count($diff)==$lastRemove) $lastRemove = -1;
-				}
-				++$x;
-				continue;
-			}
-			array_push($diff, array(YellowMerge::SAME, $textSource[$textStart+$y], false));
-			$lastRemove = -1;
-			++$x;
-			++$y;
-		}
-		for($pos=$sourceEnd;$pos<$sourceSize; ++$pos) array_push($diff, array(YellowMerge::SAME, $textSource[$pos], false));
-		return $diff;
-	}
-	
-	// Build longest common subsequence
-	function buildDiffLCS($textSource, $textOther, $textStart, $yEnd, $xEnd)
-	{
-		$lcs = array_fill(0, $yEnd+1, array_fill(0, $xEnd+1, 0));
-		for($y=$yEnd-1; $y>=0; --$y)
-		{
-			for($x=$xEnd-1; $x>=0; --$x)
-			{
-				if($textSource[$textStart+$y]==$textOther[$textStart+$x])
-				{
-					$lcs[$y][$x] = $lcs[$y+1][$x+1]+1;
-				} else {
-					$lcs[$y][$x] = max($lcs[$y][$x+1], $lcs[$y+1][$x]);
-				}
-			}
-		}
-		return $lcs;
-	}
-	
-	// Merge differences
-	function mergeDiff($diffMine, $diffYours)
-	{
-		$diff = array();
-		$posMine = $posYours = 0;
-		while($posMine<count($diffMine) && $posYours<count($diffYours))
-		{
-			$typeMine = $diffMine[$posMine][0];
-			$typeYours = $diffYours[$posYours][0];
-			if($typeMine==YellowMerge::SAME)
-			{
-				array_push($diff, $diffYours[$posYours]);
-			} else if($typeYours==YellowMerge::SAME) {
-				array_push($diff, $diffMine[$posMine]);
-			} else if($typeMine==YellowMerge::ADD && $typeYours==YellowMerge::ADD) {
-				$this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false);
-			} else if($typeMine==YellowMerge::MODIFY && $typeYours==YellowMerge::MODIFY) {
-				$this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false);
-			} else if($typeMine==YellowMerge::REMOVE && $typeYours==YellowMerge::REMOVE) {
-				array_push($diff, $diffMine[$posMine]);
-			} else if($typeMine==YellowMerge::ADD) {
-				array_push($diff, $diffMine[$posMine]);
-			} else if($typeYours==YellowMerge::ADD) {
-				array_push($diff, $diffYours[$posYours]);
-			} else {
-				$this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], true);
-			}
-			if(defined("DEBUG") && DEBUG>=2) echo "YellowMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n";
-			if($typeMine==YellowMerge::ADD || $typeYours==YellowMerge::ADD)
-			{
-				if($typeMine==YellowMerge::ADD) ++$posMine;
-				if($typeYours==YellowMerge::ADD) ++$posYours;
-			} else {
-				++$posMine;
-				++$posYours;
-			}
-		}
-		for(;$posMine<count($diffMine); ++$posMine)
-		{
-			array_push($diff, $diffMine[$posMine]);
-			$typeMine = $diffMine[$posMine][0]; $typeYours = ' ';
-			if(defined("DEBUG") && DEBUG>=2) echo "YellowMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n";
-		}
-		for(;$posYours<count($diffYours); ++$posYours)
-		{
-			array_push($diff, $diffYours[$posYours]);
-			$typeYours = $diffYours[$posYours][0]; $typeMine = ' ';
-			if(defined("DEBUG") && DEBUG>=2) echo "YellowMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n";
-		}
-		return $diff;
-	}
-	
-	// Merge potential conflict
-	function mergeConflict(&$diff, $diffMine, $diffYours, $conflict)
-	{
-		if(!$conflict && $diffMine[1]==$diffYours[1])
-		{
-			array_push($diff, $diffMine);
-		} else {
-			array_push($diff, array($diffMine[0], $diffMine[1], true));
-			array_push($diff, array($diffYours[0], $diffYours[1], true));
-		}
-	}
-	
-	// Return merged text, null if not possible
-	function getOutput($diff, $showDiff = false)
-	{
-		$output = "";
-		if(!$showDiff)
-		{
-			for($i=0; $i<count($diff); ++$i)
-			{
-				if($diff[$i][0]!=YellowMerge::REMOVE) $output .= $diff[$i][1];
-				$conflict |= $diff[$i][2];
-			}
-		} else {
-			for($i=0; $i<count($diff); ++$i)
-			{
-				$output .= $diff[$i][2] ? "! " : $diff[$i][0].' ';
-				$output .= $diff[$i][1];
-			}
-		}
-		return !$conflict ? $output : null;
-	}
+    
+class YellowMerge {
+    public $yellow;     //access to API
+    const ADD = "+";    //merge types
+    const MODIFY = "*";
+    const REMOVE = "-";
+    const SAME = " ";
+    
+    public function __construct($yellow) {
+        $this->yellow = $yellow;
+    }
+    
+    // Merge text, null if not possible
+    public function merge($textSource, $textMine, $textYours, $showDiff = false) {
+        if ($textMine!=$textYours) {
+            $diffMine = $this->buildDiff($textSource, $textMine);
+            $diffYours = $this->buildDiff($textSource, $textYours);
+            $diff = $this->mergeDiff($diffMine, $diffYours);
+            $output = $this->getOutput($diff, $showDiff);
+        } else {
+            $output = $textMine;
+        }
+        return $output;
+    }
+    
+    // Build differences to common source
+    public function buildDiff($textSource, $textOther) {
+        $diff = array();
+        $lastRemove = -1;
+        $textStart = 0;
+        $textSource = $this->yellow->toolbox->getTextLines($textSource);
+        $textOther = $this->yellow->toolbox->getTextLines($textOther);
+        $sourceEnd = $sourceSize = count($textSource);
+        $otherEnd = $otherSize = count($textOther);
+        while ($textStart<$sourceEnd && $textStart<$otherEnd && $textSource[$textStart]==$textOther[$textStart]) {
+            ++$textStart;
+        }
+        while ($textStart<$sourceEnd && $textStart<$otherEnd && $textSource[$sourceEnd-1]==$textOther[$otherEnd-1]) {
+            --$sourceEnd;
+            --$otherEnd;
+        }
+        for ($pos=0; $pos<$textStart; ++$pos) {
+            array_push($diff, array(YellowMerge::SAME, $textSource[$pos], false));
+        }
+        $lcs = $this->buildDiffLCS($textSource, $textOther, $textStart, $sourceEnd-$textStart, $otherEnd-$textStart);
+        for ($x=0,$y=0,$xEnd=$otherEnd-$textStart,$yEnd=$sourceEnd-$textStart; $x<$xEnd || $y<$yEnd;) {
+            $max = $lcs[$y][$x];
+            if ($y<$yEnd && $lcs[$y+1][$x]==$max) {
+                array_push($diff, array(YellowMerge::REMOVE, $textSource[$textStart+$y], false));
+                if ($lastRemove==-1) $lastRemove = count($diff)-1;
+                ++$y;
+                continue;
+            }
+            if ($x<$xEnd && $lcs[$y][$x+1]==$max) {
+                if ($lastRemove==-1 || $diff[$lastRemove][0]!=YellowMerge::REMOVE) {
+                    array_push($diff, array(YellowMerge::ADD, $textOther[$textStart+$x], false));
+                    $lastRemove = -1;
+                } else {
+                    $diff[$lastRemove] = array(YellowMerge::MODIFY, $textOther[$textStart+$x], false);
+                    ++$lastRemove;
+                    if (count($diff)==$lastRemove) $lastRemove = -1;
+                }
+                ++$x;
+                continue;
+            }
+            array_push($diff, array(YellowMerge::SAME, $textSource[$textStart+$y], false));
+            $lastRemove = -1;
+            ++$x;
+            ++$y;
+        }
+        for ($pos=$sourceEnd;$pos<$sourceSize; ++$pos) {
+            array_push($diff, array(YellowMerge::SAME, $textSource[$pos], false));
+        }
+        return $diff;
+    }
+    
+    // Build longest common subsequence
+    public function buildDiffLCS($textSource, $textOther, $textStart, $yEnd, $xEnd) {
+        $lcs = array_fill(0, $yEnd+1, array_fill(0, $xEnd+1, 0));
+        for ($y=$yEnd-1; $y>=0; --$y) {
+            for ($x=$xEnd-1; $x>=0; --$x) {
+                if ($textSource[$textStart+$y]==$textOther[$textStart+$x]) {
+                    $lcs[$y][$x] = $lcs[$y+1][$x+1]+1;
+                } else {
+                    $lcs[$y][$x] = max($lcs[$y][$x+1], $lcs[$y+1][$x]);
+                }
+            }
+        }
+        return $lcs;
+    }
+    
+    // Merge differences
+    public function mergeDiff($diffMine, $diffYours) {
+        $diff = array();
+        $posMine = $posYours = 0;
+        while ($posMine<count($diffMine) && $posYours<count($diffYours)) {
+            $typeMine = $diffMine[$posMine][0];
+            $typeYours = $diffYours[$posYours][0];
+            if ($typeMine==YellowMerge::SAME) {
+                array_push($diff, $diffYours[$posYours]);
+            } elseif ($typeYours==YellowMerge::SAME) {
+                array_push($diff, $diffMine[$posMine]);
+            } elseif ($typeMine==YellowMerge::ADD && $typeYours==YellowMerge::ADD) {
+                $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false);
+            } elseif ($typeMine==YellowMerge::MODIFY && $typeYours==YellowMerge::MODIFY) {
+                $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], false);
+            } elseif ($typeMine==YellowMerge::REMOVE && $typeYours==YellowMerge::REMOVE) {
+                array_push($diff, $diffMine[$posMine]);
+            } elseif ($typeMine==YellowMerge::ADD) {
+                array_push($diff, $diffMine[$posMine]);
+            } elseif ($typeYours==YellowMerge::ADD) {
+                array_push($diff, $diffYours[$posYours]);
+            } else {
+                $this->mergeConflict($diff, $diffMine[$posMine], $diffYours[$posYours], true);
+            }
+            if (defined("DEBUG") && DEBUG>=2) echo "YellowMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n";
+            if ($typeMine==YellowMerge::ADD || $typeYours==YellowMerge::ADD) {
+                if ($typeMine==YellowMerge::ADD) ++$posMine;
+                if ($typeYours==YellowMerge::ADD) ++$posYours;
+            } else {
+                ++$posMine;
+                ++$posYours;
+            }
+        }
+        for (;$posMine<count($diffMine); ++$posMine) {
+            array_push($diff, $diffMine[$posMine]);
+            $typeMine = $diffMine[$posMine][0];
+            $typeYours = " ";
+            if (defined("DEBUG") && DEBUG>=2) echo "YellowMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n";
+        }
+        for (;$posYours<count($diffYours); ++$posYours) {
+            array_push($diff, $diffYours[$posYours]);
+            $typeYours = $diffYours[$posYours][0];
+            $typeMine = " ";
+            if (defined("DEBUG") && DEBUG>=2) echo "YellowMerge::mergeDiff $typeMine $typeYours pos:$posMine\t$posYours<br/>\n";
+        }
+        return $diff;
+    }
+    
+    // Merge potential conflict
+    public function mergeConflict(&$diff, $diffMine, $diffYours, $conflict) {
+        if (!$conflict && $diffMine[1]==$diffYours[1]) {
+            array_push($diff, $diffMine);
+        } else {
+            array_push($diff, array($diffMine[0], $diffMine[1], true));
+            array_push($diff, array($diffYours[0], $diffYours[1], true));
+        }
+    }
+    
+    // Return merged text, null if not possible
+    public function getOutput($diff, $showDiff = false) {
+        $output = "";
+        if (!$showDiff) {
+            for ($i=0; $i<count($diff); ++$i) {
+                if ($diff[$i][0]!=YellowMerge::REMOVE) $output .= $diff[$i][1];
+                $conflict |= $diff[$i][2];
+            }
+        } else {
+            for ($i=0; $i<count($diff); ++$i) {
+                $output .= $diff[$i][2] ? "! " : $diff[$i][0]." ";
+                $output .= $diff[$i][1];
+            }
+        }
+        return !$conflict ? $output : null;
+    }
 }
 
 $yellow->plugins->register("edit", "YellowEdit", YellowEdit::VERSION);
-?>

+ 153 - 179
system/plugins/image.php

@@ -3,194 +3,168 @@
 // Copyright (c) 2013-2017 Datenstrom, https://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
-class YellowImage
-{
-	const VERSION = "0.7.3";
-	var $yellow;			//access to API
-	var $graphicsLibrary;	//graphics library support? (boolean)
+class YellowImage {
+    const VERSION = "0.7.3";
+    public $yellow;             //access to API
+    public $graphicsLibrary;    //graphics library support? (boolean)
 
-	// Handle initialisation
-	function onLoad($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->yellow->config->setDefault("imageThumbnailLocation", "/media/thumbnails/");
-		$this->yellow->config->setDefault("imageThumbnailDir", "media/thumbnails/");
-		$this->yellow->config->setDefault("imageThumbnailJpgQuality", 80);
-		$this->yellow->config->setDefault("imageAlt", "Image");
-		$this->graphicsLibrary = $this->isGraphicsLibrary();
-	}
+    // Handle initialisation
+    public function onLoad($yellow) {
+        $this->yellow = $yellow;
+        $this->yellow->config->setDefault("imageThumbnailLocation", "/media/thumbnails/");
+        $this->yellow->config->setDefault("imageThumbnailDir", "media/thumbnails/");
+        $this->yellow->config->setDefault("imageThumbnailJpgQuality", 80);
+        $this->yellow->config->setDefault("imageAlt", "Image");
+        $this->graphicsLibrary = $this->isGraphicsLibrary();
+    }
 
-	// Handle page content parsing of custom block
-	function onParseContentBlock($page, $name, $text, $shortcut)
-	{
-		$output = null;
-		if($name=="image" && $shortcut)
-		{
-			if(!$this->graphicsLibrary)
-			{
-				$this->yellow->page->error(500, "Plugin 'image' requires GD library with gif/jpg/png support!");
-				return $output;
-			}
-			list($name, $alt, $style, $width, $height) = $this->yellow->toolbox->getTextArgs($text);
-			if(!preg_match("/^\w+:/", $name))
-			{
-				if(empty($alt)) $alt = $this->yellow->config->get("imageAlt");
-				if(empty($width)) $width = "100%";
-				if(empty($height)) $height = $width;
-				list($src, $width, $height) = $this->getImageInfo($this->yellow->config->get("imageDir").$name, $width, $height);
-			} else {
-				if(empty($alt)) $alt = $this->yellow->config->get("imageAlt");
-				$src = $this->yellow->lookup->normaliseUrl("", "", "", $name);
-				$width = $height = 0;
-			}
-			$output = "<img src=\"".htmlspecialchars($src)."\"";
-			if($width && $height) $output .= " width=\"".htmlspecialchars($width)."\" height=\"".htmlspecialchars($height)."\"";
-			if(!empty($alt)) $output .= " alt=\"".htmlspecialchars($alt)."\" title=\"".htmlspecialchars($alt)."\"";
-			if(!empty($style)) $output .= " class=\"".htmlspecialchars($style)."\"";
-			$output .= " />";
-		}
-		return $output;
-	}
-	
-	// Handle command
-	function onCommand($args)
-	{
-		list($command) = $args;
-		switch($command)
-		{
-			case "clean":	$statusCode = $this->cleanCommand($args); break;
-			default:		$statusCode = 0;
-		}
-		return $statusCode;
-	}
+    // Handle page content parsing of custom block
+    public function onParseContentBlock($page, $name, $text, $shortcut) {
+        $output = null;
+        if ($name=="image" && $shortcut) {
+            if (!$this->graphicsLibrary) {
+                $this->yellow->page->error(500, "Plugin 'image' requires GD library with gif/jpg/png support!");
+                return $output;
+            }
+            list($name, $alt, $style, $width, $height) = $this->yellow->toolbox->getTextArgs($text);
+            if (!preg_match("/^\w+:/", $name)) {
+                if (empty($alt)) $alt = $this->yellow->config->get("imageAlt");
+                if (empty($width)) $width = "100%";
+                if (empty($height)) $height = $width;
+                list($src, $width, $height) = $this->getImageInfo($this->yellow->config->get("imageDir").$name, $width, $height);
+            } else {
+                if (empty($alt)) $alt = $this->yellow->config->get("imageAlt");
+                $src = $this->yellow->lookup->normaliseUrl("", "", "", $name);
+                $width = $height = 0;
+            }
+            $output = "<img src=\"".htmlspecialchars($src)."\"";
+            if ($width && $height) $output .= " width=\"".htmlspecialchars($width)."\" height=\"".htmlspecialchars($height)."\"";
+            if (!empty($alt)) $output .= " alt=\"".htmlspecialchars($alt)."\" title=\"".htmlspecialchars($alt)."\"";
+            if (!empty($style)) $output .= " class=\"".htmlspecialchars($style)."\"";
+            $output .= " />";
+        }
+        return $output;
+    }
+    
+    // Handle command
+    public function onCommand($args) {
+        list($command) = $args;
+        switch ($command) {
+            case "clean":   $statusCode = $this->cleanCommand($args); break;
+            default:        $statusCode = 0;
+        }
+        return $statusCode;
+    }
 
-	// Clean thumbnails
-	function cleanCommand($args)
-	{
-		$statusCode = 0;
-		list($command, $path) = $args;
-		if($path=="all")
-		{
-			$path = $this->yellow->config->get("imageThumbnailDir");
-			foreach($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry)
-			{
-				if(!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
-			}
-			if($statusCode==500) echo "ERROR cleaning thumbnails: Can't delete files in directory '$path'!\n";
-		}
-		return $statusCode;
-	}
+    // Clean thumbnails
+    public function cleanCommand($args) {
+        $statusCode = 0;
+        list($command, $path) = $args;
+        if ($path=="all") {
+            $path = $this->yellow->config->get("imageThumbnailDir");
+            foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", false, false) as $entry) {
+                if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
+            }
+            if ($statusCode==500) echo "ERROR cleaning thumbnails: Can't delete files in directory '$path'!\n";
+        }
+        return $statusCode;
+    }
 
-	// Return image info, create thumbnail on demand
-	function getImageInfo($fileName, $widthOutput, $heightOutput)
-	{
-		$fileNameShort = substru($fileName, strlenu($this->yellow->config->get("imageDir")));
-		list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInfo($fileName);
-		$widthOutput = $this->convertValueAndUnit($widthOutput, $widthInput);
-		$heightOutput = $this->convertValueAndUnit($heightOutput, $heightInput);
-		if(($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg")
-		{
-			$src = $this->yellow->config->get("serverBase").$this->yellow->config->get("imageLocation").$fileNameShort;
-			$width = $widthOutput; $height = $heightOutput;
-		} else {
-			$fileNameThumb = ltrim(str_replace(array("/", "\\", "."), "-", dirname($fileNameShort)."/".pathinfo($fileName, PATHINFO_FILENAME)), "-");
-			$fileNameThumb .= "-".$widthOutput."x".$heightOutput;
-			$fileNameThumb .= ".".pathinfo($fileName, PATHINFO_EXTENSION);
-			$fileNameOutput = $this->yellow->config->get("imageThumbnailDir").$fileNameThumb;
-			if($this->isFileNotUpdated($fileName, $fileNameOutput))
-			{
-				$image = $this->loadImage($fileName, $type);
-				$image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput);
-				if(!$this->saveImage($image, $fileNameOutput, $type) ||
-				   !$this->yellow->toolbox->modifyFile($fileNameOutput, $this->yellow->toolbox->getFileModified($fileName)))
-				{
-					$this->yellow->page->error(500, "Can't write file '$fileNameOutput'!");
-				}
-			}
-			$src = $this->yellow->config->get("serverBase").$this->yellow->config->get("imageThumbnailLocation").$fileNameThumb;
-			list($width, $height) = $this->yellow->toolbox->detectImageInfo($fileNameOutput);
-		}
-		return array($src, $width, $height);
-	}
+    // Return image info, create thumbnail on demand
+    public function getImageInfo($fileName, $widthOutput, $heightOutput) {
+        $fileNameShort = substru($fileName, strlenu($this->yellow->config->get("imageDir")));
+        list($widthInput, $heightInput, $type) = $this->yellow->toolbox->detectImageInfo($fileName);
+        $widthOutput = $this->convertValueAndUnit($widthOutput, $widthInput);
+        $heightOutput = $this->convertValueAndUnit($heightOutput, $heightInput);
+        if (($widthInput==$widthOutput && $heightInput==$heightOutput) || $type=="svg") {
+            $src = $this->yellow->config->get("serverBase").$this->yellow->config->get("imageLocation").$fileNameShort;
+            $width = $widthOutput;
+            $height = $heightOutput;
+        } else {
+            $fileNameThumb = ltrim(str_replace(array("/", "\\", "."), "-", dirname($fileNameShort)."/".pathinfo($fileName, PATHINFO_FILENAME)), "-");
+            $fileNameThumb .= "-".$widthOutput."x".$heightOutput;
+            $fileNameThumb .= ".".pathinfo($fileName, PATHINFO_EXTENSION);
+            $fileNameOutput = $this->yellow->config->get("imageThumbnailDir").$fileNameThumb;
+            if ($this->isFileNotUpdated($fileName, $fileNameOutput)) {
+                $image = $this->loadImage($fileName, $type);
+                $image = $this->resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput);
+                if (!$this->saveImage($image, $fileNameOutput, $type) ||
+                    !$this->yellow->toolbox->modifyFile($fileNameOutput, $this->yellow->toolbox->getFileModified($fileName))) {
+                    $this->yellow->page->error(500, "Can't write file '$fileNameOutput'!");
+                }
+            }
+            $src = $this->yellow->config->get("serverBase").$this->yellow->config->get("imageThumbnailLocation").$fileNameThumb;
+            list($width, $height) = $this->yellow->toolbox->detectImageInfo($fileNameOutput);
+        }
+        return array($src, $width, $height);
+    }
 
-	// Load image from file
-	function loadImage($fileName, $type)
-	{
-		$image = false;
-		switch($type)
-		{
-			case "gif":	$image = @imagecreatefromgif($fileName); break;
-			case "jpg":	$image = @imagecreatefromjpeg($fileName); break;
-			case "png":	$image = @imagecreatefrompng($fileName); break;
-		}
-		return $image;
-	}
-	
-	// Save image to file
-	function saveImage($image, $fileName, $type)
-	{
-		$ok = false;
-		switch($type)
-		{
-			case "gif":	$ok = @imagegif($image, $fileName); break;
-			case "jpg":	$ok = @imagejpeg($image, $fileName, $this->yellow->config->get("imageThumbnailJpgQuality")); break;
-			case "png":	$ok = @imagepng($image, $fileName); break;
-		}
-		return $ok;
-	}
+    // Load image from file
+    public function loadImage($fileName, $type) {
+        $image = false;
+        switch ($type) {
+            case "gif": $image = @imagecreatefromgif($fileName); break;
+            case "jpg": $image = @imagecreatefromjpeg($fileName); break;
+            case "png": $image = @imagecreatefrompng($fileName); break;
+        }
+        return $image;
+    }
+    
+    // Save image to file
+    public function saveImage($image, $fileName, $type) {
+        $ok = false;
+        switch ($type) {
+            case "gif": $ok = @imagegif($image, $fileName); break;
+            case "jpg": $ok = @imagejpeg($image, $fileName, $this->yellow->config->get("imageThumbnailJpgQuality")); break;
+            case "png": $ok = @imagepng($image, $fileName); break;
+        }
+        return $ok;
+    }
 
-	// Create image from scratch
-	function createImage($width, $height)
-	{
-		$image = imagecreatetruecolor($width, $height);
-		imagealphablending($image, false);
-		imagesavealpha($image, true);
-		return $image;
-	}
+    // Create image from scratch
+    public function createImage($width, $height) {
+        $image = imagecreatetruecolor($width, $height);
+        imagealphablending($image, false);
+        imagesavealpha($image, true);
+        return $image;
+    }
 
-	// Resize image
-	function resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput)
-	{
-		$widthFit = $widthInput * ($heightOutput / $heightInput);
-		$heightFit = $heightInput * ($widthOutput / $widthInput);
-		$widthDiff = abs($widthOutput - $widthFit);
-		$heightDiff = abs($heightOutput - $heightFit);
-		$imageOutput = $this->createImage($widthOutput, $heightOutput);
-		if($heightFit>$heightOutput)
-		{
-			imagecopyresampled($imageOutput, $image, 0, $heightDiff/-2, 0, 0, $widthOutput, $heightFit, $widthInput, $heightInput);
-		} else {
-			imagecopyresampled($imageOutput, $image, $widthDiff/-2, 0, 0, 0, $widthFit, $heightOutput, $widthInput, $heightInput);
-		}
-		return $imageOutput;
-	}
-	
-	// Return value according to unit
-	function convertValueAndUnit($text, $valueBase)
-	{
-		$value = $unit = "";
-		if(preg_match("/([\d\.]+)(\S*)/", $text, $matches))
-		{
-			$value = $matches[1];
-			$unit = $matches[2];
-			if($unit=="%") $value = $valueBase * $value / 100;
-		}
-		return intval($value);
-	}
+    // Resize image
+    public function resizeImage($image, $widthInput, $heightInput, $widthOutput, $heightOutput) {
+        $widthFit = $widthInput * ($heightOutput / $heightInput);
+        $heightFit = $heightInput * ($widthOutput / $widthInput);
+        $widthDiff = abs($widthOutput - $widthFit);
+        $heightDiff = abs($heightOutput - $heightFit);
+        $imageOutput = $this->createImage($widthOutput, $heightOutput);
+        if ($heightFit>$heightOutput) {
+            imagecopyresampled($imageOutput, $image, 0, $heightDiff/-2, 0, 0, $widthOutput, $heightFit, $widthInput, $heightInput);
+        } else {
+            imagecopyresampled($imageOutput, $image, $widthDiff/-2, 0, 0, 0, $widthFit, $heightOutput, $widthInput, $heightInput);
+        }
+        return $imageOutput;
+    }
+    
+    // Return value according to unit
+    public function convertValueAndUnit($text, $valueBase) {
+        $value = $unit = "";
+        if (preg_match("/([\d\.]+)(\S*)/", $text, $matches)) {
+            $value = $matches[1];
+            $unit = $matches[2];
+            if ($unit=="%") $value = $valueBase * $value / 100;
+        }
+        return intval($value);
+    }
 
-	// Check if file needs to be updated
-	function isFileNotUpdated($fileNameInput, $fileNameOutput)
-	{
-		return $this->yellow->toolbox->getFileModified($fileNameInput)!=$this->yellow->toolbox->getFileModified($fileNameOutput);
-	}
+    // Check if file needs to be updated
+    public function isFileNotUpdated($fileNameInput, $fileNameOutput) {
+        return $this->yellow->toolbox->getFileModified($fileNameInput)!=$this->yellow->toolbox->getFileModified($fileNameOutput);
+    }
 
-	// Check graphics library support
-	function isGraphicsLibrary()
-	{
-		return extension_loaded("gd") && function_exists("gd_info") &&
-			((imagetypes()&(IMG_GIF|IMG_JPG|IMG_PNG))==(IMG_GIF|IMG_JPG|IMG_PNG));
-	}
+    // Check graphics library support
+    public function isGraphicsLibrary() {
+        return extension_loaded("gd") && function_exists("gd_info") &&
+            ((imagetypes()&(IMG_GIF|IMG_JPG|IMG_PNG))==(IMG_GIF|IMG_JPG|IMG_PNG));
+    }
 }
 
 $yellow->plugins->register("image", "YellowImage", YellowImage::VERSION);
-?>

+ 2 - 4
system/plugins/language.php

@@ -3,10 +3,8 @@
 // Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
-class YellowLanguage
-{
-	const VERSION = "0.7.13";
+class YellowLanguage {
+    const VERSION = "0.7.13";
 }
 
 $yellow->plugins->register("language", "YellowLanguage", YellowLanguage::VERSION);
-?>

+ 144 - 163
system/plugins/markdown.php

@@ -3,23 +3,20 @@
 // Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
-class YellowMarkdown
-{
-	const VERSION = "0.6.8";
-	var $yellow;			//access to API
-	
-	// Handle initialisation
-	function onLoad($yellow)
-	{
-		$this->yellow = $yellow;
-	}
-	
-	// Handle page content parsing of raw format
-	function onParseContentRaw($page, $text)
-	{
-		$markdown = new YellowMarkdownExtraParser($this->yellow, $page);
-		return $markdown->transform($text);
-	}
+class YellowMarkdown {
+    const VERSION = "0.6.8";
+    public $yellow;         //access to API
+    
+    // Handle initialisation
+    public function onLoad($yellow) {
+        $this->yellow = $yellow;
+    }
+    
+    // Handle page content parsing of raw format
+    public function onParseContentRaw($page, $text) {
+        $markdown = new YellowMarkdownExtraParser($this->yellow, $page);
+        return $markdown->transform($text);
+    }
 }
 
 // PHP Markdown Lib
@@ -3734,154 +3731,138 @@ class MarkdownExtraParser extends MarkdownParser {
 		}
 	}
 }
-									  
+
 // Markdown extra parser extensions
 // Copyright (c) 2013-2018 Datenstrom
-									  
-class YellowMarkdownExtraParser extends MarkdownExtraParser
-{
-	var $yellow;			//access to API
-	var $page;				//access to page
-	var $idAttributes;		//id attributes
-
-	function __construct($yellow, $page)
-	{
-		$this->yellow = $yellow;
-		$this->page = $page;
-		$this->idAttributes = array();
-		$this->no_markup = $page->parserSafeMode;
-		$this->url_filter_func = function($url) use ($yellow, $page)
-		{
-			return $yellow->lookup->normaliseLocation($url, $page->location,
-				$page->parserSafeMode && $page->statusCode==200);
-		};
-		parent::__construct();
-	}
 
-	// Return unique id attribute
-	function getIdAttribute($text)
-	{
-		$text = $this->yellow->lookup->normaliseName($text, true, false, true);
-		$text = trim(preg_replace("/-+/", "-", $text), "-");
-		if(is_null($this->idAttributes[$text]))
-		{
-			$this->idAttributes[$text] = $text;
-			$attr = " id=\"$text\"";
-		}
-		return $attr;
-	}
-	
-	// Handle links
-	function doAutoLinks($text)
-	{
-		$text = preg_replace_callback("/<(\w+:[^\'\">\s]+)>/", array(&$this, "_doAutoLinks_url_callback"), $text);
-		$text = preg_replace_callback("/<([\w\-\.]+@[\w\-\.]+)>/", array(&$this, "_doAutoLinks_email_callback"), $text);
-		$text = preg_replace_callback("/\[\-\-(.*?)\-\-\]/", array(&$this, "_doAutoLinks_comment_callback"), $text);
-		$text = preg_replace_callback("/\[(\w+)(.*?)\]/", array(&$this, "_doAutoLinks_shortcut_callback"), $text);
-		$text = preg_replace_callback("/\:([\w\+\-\_]+)\:/", array(&$this, "_doAutoLinks_shortcode_callback"), $text);
-		$text = preg_replace_callback("/((http|https|ftp):\/\/\S+[^\'\"\,\.\;\:\s]+)/", array(&$this, "_doAutoLinks_url_callback"), $text);
-		$text = preg_replace_callback("/([\w\-\.]+@[\w\-\.]+\.[\w]{2,4})/", array(&$this, "_doAutoLinks_email_callback"), $text);
-		return $text;
-	}
-	
-	// Handle comments
-	function _doAutoLinks_comment_callback($matches)
-	{
-		$text = $matches[1];
-		$output = "<!--".htmlspecialchars($text, ENT_NOQUOTES)."-->";
-		if($text[0]=='-') $output = "";
-		return $this->hashBlock($output);
-	}
-	
-	// Handle shortcuts
-	function _doAutoLinks_shortcut_callback($matches)
-	{
-		$output = $this->page->parseContentBlock($matches[1], trim($matches[2]), true);
-		if(is_null($output)) $output = htmlspecialchars($matches[0], ENT_NOQUOTES);
-		return substr($output, 0, 4)=="<div" ? $this->hashBlock(trim($output)) : $this->hashPart(trim($output));
-	}
-
-	// Handle shortcodes
-	function _doAutoLinks_shortcode_callback($matches)
-	{
-		$output = $this->page->parseContentBlock("", $matches[1], true);
-		if(is_null($output)) $output = htmlspecialchars($matches[0], ENT_NOQUOTES);
-		return $this->hashPart($output);
-	}
-	
-	// Handle fenced code blocks
-	function _doFencedCodeBlocks_callback($matches)
-	{
-		$text = $matches[4];
-		$name = empty($matches[2]) ? "" : "$matches[2] $matches[3]";
-		$output = $this->page->parseContentBlock($name, $text, false);
-		if(is_null($output))
-		{
-			$attr = $this->doExtraAttributes("pre", ".$matches[2] $matches[3]");
-			$output = "<pre$attr><code>".htmlspecialchars($text, ENT_NOQUOTES)."</code></pre>";
-		}
-		return "\n\n".$this->hashBlock($output)."\n\n";
-	}
-	
-	// Handle headers, text style
-	function _doHeaders_callback_setext($matches)
-	{
-		if($matches[3]=='-' && preg_match('{^- }', $matches[1])) return $matches[0];
-		$text = $matches[1];
-		$level = $matches[3]{0}=='=' ? 1 : 2;
-		$attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]);
-		if(empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text);
-		$output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>";
-		return "\n".$this->hashBlock($output)."\n\n";
-	}
-	
-	// Handle headers, atx style
-	function _doHeaders_callback_atx($matches)
-	{
-		$text = $matches[2];
-		$level = strlen($matches[1]);
-		$attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]);
-		if(empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text);
-		$output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>";
-		return "\n".$this->hashBlock($output)."\n\n";
-	}
-	
-	// Handle inline links
-	function _doAnchors_inline_callback($matches)
-	{
-		$url = $matches[3]=="" ? $matches[4] : $matches[3];
-		$text = $matches[2];
-		$title = $matches[7];
-		$attr = $this->doExtraAttributes("a", $dummy =& $matches[8]);
-		$output = "<a href=\"".$this->encodeURLAttribute($url)."\"";
-		if(!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\"";
-		$output .= $attr;
-		$output .= ">".$this->runSpanGamut($text)."</a>";
-		return $this->hashPart($output);
-	}
-	
-	// Handle inline images
-	function _doImages_inline_callback($matches)
-	{
-		$width = $height = 0;
-		$src = $matches[3]=="" ? $matches[4] : $matches[3];
-		if(!preg_match("/^\w+:/", $src))
-		{
-			list($width, $height) = $this->yellow->toolbox->detectImageInfo($this->yellow->config->get("imageDir").$src);
-			$src = $this->yellow->config->get("serverBase").$this->yellow->config->get("imageLocation").$src;
-		}
-		$alt = $matches[2];
-		$title = $matches[7]=="" ? $matches[2] : $matches[7];
-		$attr = $this->doExtraAttributes("img", $dummy =& $matches[8]);
-		$output = "<img src=\"".$this->encodeURLAttribute($src)."\"";
-		if($width && $height) $output .= " width=\"$width\" height=\"$height\"";
-		if(!empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\"";
-		if(!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\"";
-		$output .= $attr;
-		$output .= $this->empty_element_suffix;
-		return $this->hashPart($output);
-	}
+class YellowMarkdownExtraParser extends MarkdownExtraParser {
+    public $yellow;             //access to API
+    public $page;               //access to page
+    public $idAttributes;       //id attributes
+
+    public function __construct($yellow, $page) {
+        $this->yellow = $yellow;
+        $this->page = $page;
+        $this->idAttributes = array();
+        $this->no_markup = $page->parserSafeMode;
+        $this->url_filter_func = function($url) use ($yellow, $page) {
+            return $yellow->lookup->normaliseLocation($url, $page->location,
+                $page->parserSafeMode && $page->statusCode==200);
+        };
+        parent::__construct();
+    }
+
+    // Return unique id attribute
+    public function getIdAttribute($text) {
+        $text = $this->yellow->lookup->normaliseName($text, true, false, true);
+        $text = trim(preg_replace("/-+/", "-", $text), "-");
+        if (is_null($this->idAttributes[$text])) {
+            $this->idAttributes[$text] = $text;
+            $attr = " id=\"$text\"";
+        }
+        return $attr;
+    }
+    
+    // Handle links
+    public function doAutoLinks($text) {
+        $text = preg_replace_callback("/<(\w+:[^\'\">\s]+)>/", array(&$this, "_doAutoLinks_url_callback"), $text);
+        $text = preg_replace_callback("/<([\w\-\.]+@[\w\-\.]+)>/", array(&$this, "_doAutoLinks_email_callback"), $text);
+        $text = preg_replace_callback("/\[\-\-(.*?)\-\-\]/", array(&$this, "_doAutoLinks_comment_callback"), $text);
+        $text = preg_replace_callback("/\[(\w+)(.*?)\]/", array(&$this, "_doAutoLinks_shortcut_callback"), $text);
+        $text = preg_replace_callback("/\:([\w\+\-\_]+)\:/", array(&$this, "_doAutoLinks_shortcode_callback"), $text);
+        $text = preg_replace_callback("/((http|https|ftp):\/\/\S+[^\'\"\,\.\;\:\s]+)/", array(&$this, "_doAutoLinks_url_callback"), $text);
+        $text = preg_replace_callback("/([\w\-\.]+@[\w\-\.]+\.[\w]{2,4})/", array(&$this, "_doAutoLinks_email_callback"), $text);
+        return $text;
+    }
+    
+    // Handle comments
+    public function _doAutoLinks_comment_callback($matches) {
+        $text = $matches[1];
+        $output = "<!--".htmlspecialchars($text, ENT_NOQUOTES)."-->";
+        if ($text[0]=="-") $output = "";
+        return $this->hashBlock($output);
+    }
+    
+    // Handle shortcuts
+    public function _doAutoLinks_shortcut_callback($matches) {
+        $output = $this->page->parseContentBlock($matches[1], trim($matches[2]), true);
+        if (is_null($output)) $output = htmlspecialchars($matches[0], ENT_NOQUOTES);
+        return substr($output, 0, 4)=="<div" ? $this->hashBlock(trim($output)) : $this->hashPart(trim($output));
+    }
+
+    // Handle shortcodes
+    public function _doAutoLinks_shortcode_callback($matches) {
+        $output = $this->page->parseContentBlock("", $matches[1], true);
+        if (is_null($output)) $output = htmlspecialchars($matches[0], ENT_NOQUOTES);
+        return $this->hashPart($output);
+    }
+    
+    // Handle fenced code blocks
+    public function _doFencedCodeBlocks_callback($matches) {
+        $text = $matches[4];
+        $name = empty($matches[2]) ? "" : "$matches[2] $matches[3]";
+        $output = $this->page->parseContentBlock($name, $text, false);
+        if (is_null($output)) {
+            $attr = $this->doExtraAttributes("pre", ".$matches[2] $matches[3]");
+            $output = "<pre$attr><code>".htmlspecialchars($text, ENT_NOQUOTES)."</code></pre>";
+        }
+        return "\n\n".$this->hashBlock($output)."\n\n";
+    }
+    
+    // Handle headers, text style
+    public function _doHeaders_callback_setext($matches) {
+        if ($matches[3]=="-" && preg_match('{^- }', $matches[1])) return $matches[0];
+        $text = $matches[1];
+        $level = $matches[3]{0}=="=" ? 1 : 2;
+        $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]);
+        if (empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text);
+        $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>";
+        return "\n".$this->hashBlock($output)."\n\n";
+    }
+    
+    // Handle headers, atx style
+    public function _doHeaders_callback_atx($matches) {
+        $text = $matches[2];
+        $level = strlen($matches[1]);
+        $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]);
+        if (empty($attr) && $level>=2 && $level<=3) $attr = $this->getIdAttribute($text);
+        $output = "<h$level$attr>".$this->runSpanGamut($text)."</h$level>";
+        return "\n".$this->hashBlock($output)."\n\n";
+    }
+    
+    // Handle inline links
+    public function _doAnchors_inline_callback($matches) {
+        $url = $matches[3]=="" ? $matches[4] : $matches[3];
+        $text = $matches[2];
+        $title = $matches[7];
+        $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]);
+        $output = "<a href=\"".$this->encodeURLAttribute($url)."\"";
+        if (!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\"";
+        $output .= $attr;
+        $output .= ">".$this->runSpanGamut($text)."</a>";
+        return $this->hashPart($output);
+    }
+    
+    // Handle inline images
+    public function _doImages_inline_callback($matches) {
+        $width = $height = 0;
+        $src = $matches[3]=="" ? $matches[4] : $matches[3];
+        if (!preg_match("/^\w+:/", $src)) {
+            list($width, $height) = $this->yellow->toolbox->detectImageInfo($this->yellow->config->get("imageDir").$src);
+            $src = $this->yellow->config->get("serverBase").$this->yellow->config->get("imageLocation").$src;
+        }
+        $alt = $matches[2];
+        $title = $matches[7]=="" ? $matches[2] : $matches[7];
+        $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]);
+        $output = "<img src=\"".$this->encodeURLAttribute($src)."\"";
+        if ($width && $height) $output .= " width=\"$width\" height=\"$height\"";
+        if (!empty($alt)) $output .= " alt=\"".$this->encodeAttribute($alt)."\"";
+        if (!empty($title)) $output .= " title=\"".$this->encodeAttribute($title)."\"";
+        $output .= $attr;
+        $output .= $this->empty_element_suffix;
+        return $this->hashPart($output);
+    }
 }
-									  
+
 $yellow->plugins->register("markdown", "YellowMarkdown", YellowMarkdown::VERSION);
-?>
+

BIN
system/plugins/update-blog.installation


BIN
system/plugins/update-wiki.installation


+ 687 - 824
system/plugins/update.php

@@ -3,835 +3,698 @@
 // Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
-class YellowUpdate
-{
-	const VERSION = "0.7.16";
-	var $yellow;					//access to API
-	var $updates;					//number of updates
-	
-	// Handle initialisation
-	function onLoad($yellow)
-	{
-		$this->yellow = $yellow;
-		$this->yellow->config->setDefault("updatePluginsUrl", "https://github.com/datenstrom/yellow-plugins");
-		$this->yellow->config->setDefault("updateThemesUrl", "https://github.com/datenstrom/yellow-themes");
-		$this->yellow->config->setDefault("updateInformationFile", "update.ini");
-		$this->yellow->config->setDefault("updateVersionFile", "version.ini");
-		$this->yellow->config->setDefault("updateResourceFile", "resource.ini");
-	}
-	
-	// Handle startup
-	function onStartup($update)
-	{
-		if($update)
-		{
-			$fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
-			$fileData = $this->yellow->toolbox->readFile($fileNameConfig);
-			$configDefaults = new YellowDataCollection();
-			$configDefaults->exchangeArray($this->yellow->config->configDefaults->getArrayCopy());
-			foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-			{
-				preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-				if(!empty($matches[1]) && !is_null($configDefaults[$matches[1]])) unset($configDefaults[$matches[1]]);
-				if(!empty($matches[1]) && $matches[1][0]!='#' && is_null($this->yellow->config->configDefaults[$matches[1]]))
-				{
-					$fileDataNew .= "# $line";
-				} else {
-					$fileDataNew .= $line;
-				}
-			}
-			unset($configDefaults["configFile"]);
-			foreach($configDefaults as $key=>$value)
-			{
-				$fileDataNew .= ucfirst($key).": $value\n";
-			}
-			if($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileNameConfig, $fileDataNew);
-		}
-		if($update)	//TODO: remove later, converts old theme
-		{
-			$path = $this->yellow->config->get("themeDir");
-			foreach($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false) as $entry)
-			{
-				$fileNameAsset = $this->yellow->config->get("assetDir").basename($entry);
-				if(!is_file($fileNameAsset))
-				{
-					$fileData = $this->yellow->toolbox->readFile($entry);
-					$fileData = preg_replace("#url\(assets/(.*?)\)#", "url($1)", $fileData);
-					$this->yellow->toolbox->createFile($fileNameAsset, $fileData);
-				}
-				$this->yellow->toolbox->deleteFile($entry, $this->yellow->config->get("trashDir"));
-				$_GET["clean-url"] = "theme-has-been-updated";
-			}
-		}
-		if($update)	//TODO: remove later, converts old error page
-		{
-			$fileName = $this->yellow->config->get("configDir")."page-error-500.txt";
-			if(is_file($fileName))
-			{
-				$fileData = $this->yellow->toolbox->readFile($fileName);
-				$fileDataNew = preg_replace("/@pageerror/", "[yellow error]", $fileData);
-				if($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileName, $fileDataNew);
-			}
-		}
-		if($update)	//TODO: remove later, converts new blog page
-		{
-			$fileName = $this->yellow->config->get("configDir")."page-new-blog.txt";
-			if(is_file($fileName))
-			{
-				$fileData = $this->yellow->toolbox->readFile($fileName);
-				$fileDataNew = $this->yellow->toolbox->setMetaData($fileData, "template", "blog");
-				if($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileName, $fileDataNew);
-			}
-		}
-		if($update)	//TODO: remove later, converts new wiki page
-		{
-			$fileName = $this->yellow->config->get("configDir")."page-new-wiki.txt";
-			if(is_file($fileName))
-			{
-				$fileData = $this->yellow->toolbox->readFile($fileName);
-				$fileDataNew = $this->yellow->toolbox->setMetaData($fileData, "template", "wiki");
-				if($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileName, $fileDataNew);
-			}
-		}
-		if($update)	//TODO: remove later, converts template settings
-		{
-			$valueDefault = $this->yellow->config->get("template");
-			foreach($this->yellow->pages->index(true, true) as $page)
-			{
-				preg_match("/^.*\/(.+?)$/", dirname($page->fileName), $matches);
-				$value = $this->yellow->lookup->normaliseName($matches[1], true, false, true);
-				if(!is_file($this->yellow->config->get("templateDir").$value.".html")) $value = $valueDefault;
-				$pageTemplate = $this->yellow->toolbox->getMetaData($page->rawData, "template");
-				$pagePublished = $this->yellow->toolbox->getMetaData($page->rawData, "published");
-				if(empty($pagePublished) && $value=="blog") $value = $valueDefault;
-				if(empty($pageTemplate) && $value!=$valueDefault)
-				{
-					$rawDataNew = $this->yellow->toolbox->setMetaData($page->rawData, "template", $value);
-					if($page->rawData!=$rawDataNew) $this->yellow->toolbox->createFile($page->fileName, $rawDataNew);
-				}
-			}
-			foreach($this->yellow->pages->index(true, true)->filter("template", "blogpages") as $page)
-			{
-				$rawDataNew = $this->yellow->toolbox->setMetaData($page->rawData, "templateNew", "blog");
-				if($page->rawData!=$rawDataNew) $this->yellow->toolbox->createFile($page->fileName, $rawDataNew);
-			}
-			foreach($this->yellow->pages->index(true, true)->filter("template", "wikipages") as $page)
-			{
-				$rawDataNew = $this->yellow->toolbox->setMetaData($page->rawData, "templateNew", "wiki");
-				if($page->rawData!=$rawDataNew) $this->yellow->toolbox->createFile($page->fileName, $rawDataNew);
-			}
-			$this->yellow->pages = new YellowPages($this->yellow);
-		}
-	}
-	
-	// Handle request
-	function onRequest($scheme, $address, $base, $location, $fileName)
-	{
-		$statusCode = 0;
-		if($this->yellow->config->get("installationMode"))
-		{
-			$statusCode = $this->processRequestInstallationMode($scheme, $address, $base, $location, $fileName);
-		} else {
-			$statusCode = $this->processRequestInstallationPending($scheme, $address, $base, $location, $fileName);
-		}
-		return $statusCode;
-	}
-	
-	// Handle command
-	function onCommand($args)
-	{
-		list($command) = $args;
-		switch($command)
-		{
-			case "clean":	$statusCode = $this->cleanCommand($args); break;				
-			case "update":	$statusCode = $this->updateCommand($args); break;
-			default:		$statusCode = $this->processCommandInstallationPending($args); break;
-		}
-		return $statusCode;
-	}
-	
-	// Handle command help
-	function onCommandHelp()
-	{
-		return "update [option feature]";
-	}
-	
-	// Clean downloads
-	function cleanCommand($args)
-	{
-		$statusCode = 0;
-		list($command, $path) = $args;
-		if($path=="all")
-		{
-			$path = $this->yellow->config->get("pluginDir");
-			$regex = "/^.*\\".$this->yellow->config->get("downloadExtension")."$/";
-			foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, false) as $entry)
-			{
-				if(!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
-			}
-			if($statusCode==500) echo "ERROR cleaning downloads: Can't delete files in directory '$path'!\n";
-		}
-		return $statusCode;
-	}
-	
-	// Update website
-	function updateCommand($args)
-	{
-		list($command, $option, $feature) = $args;
-		if(empty($option) || $option=="normal" || $option=="force")
-		{
-			$force = $option=="force";
-			list($statusCode, $data) = $this->detectSoftware($force, $feature);
-			if($statusCode!=200 || !empty($data))
-			{
-				$this->updates = 0;
-				if($statusCode==200) $statusCode = $this->downloadSoftware($data);
-				if($statusCode==200) $statusCode = $this->updateSoftware($force);
-				if($statusCode>=400) echo "ERROR updating files: ".$this->yellow->page->get("pageError")."\n";
-				echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
-				echo ", $this->updates update".($this->updates!=1 ? 's' : '')." installed\n";
-			} else {
-				echo "Your website is up to date\n";
-			}
-		} else {
-			$statusCode = 400;
-			echo "Yellow $command: Invalid arguments\n";
-		}
-		return $statusCode;
-	}
-	
-	// Detect software
-	function detectSoftware($force, $feature)
-	{
-		$data = array();
-		list($statusCodeCurrent, $dataCurrent) = $this->getSoftwareVersion();
-		list($statusCodeLatest, $dataLatest) = $this->getSoftwareVersion(true, true);
-		list($statusCodeModified, $dataModified) = $this->getSoftwareModified();
-		$statusCode = max($statusCodeCurrent, $statusCodeLatest, $statusCodeModified);
-		if(empty($feature))
-		{
-			foreach($dataCurrent as $key=>$value)
-			{
-				list($version) = explode(',', $dataLatest[$key]);
-				if(strnatcasecmp($dataCurrent[$key], $version)<0) $data[$key] = $dataLatest[$key];
-				if(!is_null($dataModified[$key]) && !empty($version) && $force) $data[$key] = $dataLatest[$key];
-			}
-		} else {
-			foreach($dataCurrent as $key=>$value)
-			{
-				list($version) = explode(',', $dataLatest[$key]);
-				if(strtoloweru($key)==strtoloweru($feature) && !empty($version))
-				{
-					$data[$key] = $dataLatest[$key];
-					$dataModified = array_intersect_key($dataModified, $data);
-					break;
-				}
-			}
-			if(empty($data))
-			{
-				$statusCode = 500;
-				$this->yellow->page->error($statusCode, "Can't find feature '$feature'!");
-			}
-		}
-		if($statusCode==200)
-		{
-			foreach(array_merge($dataModified, $data) as $key=>$value)
-			{
-				list($version) = explode(',', $value);
-				if(is_null($dataModified[$key]) || $force)
-				{
-					echo "$key $version\n";
-				} else {
-					echo "$key $version has been modified - Force update\n";
-				}
-			}
-		}
-		return array($statusCode, $data);
-	}
-	
-	// Download software
-	function downloadSoftware($data)
-	{
-		$statusCode = 200;
-		$path = $this->yellow->config->get("pluginDir");
-		$fileExtension = $this->yellow->config->get("downloadExtension");
-		foreach($data as $key=>$value)
-		{
-			$fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip";
-			list($version, $url) = explode(',', $value);
-			list($statusCode, $fileData) = $this->getSoftwareFile($url);
-			if(empty($fileData) || !$this->yellow->toolbox->createFile($fileName.$fileExtension, $fileData))
-			{
-				$statusCode = 500;
-				$this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
-				break;
-			}
-		}
-		if($statusCode==200)
-		{
-			foreach($data as $key=>$value)
-			{
-				$fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip";
-				if(!$this->yellow->toolbox->renameFile($fileName.$fileExtension, $fileName))
-				{
-					$statusCode = 500;
-					$this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
-				}
-			}
-		}
-		return $statusCode;
-	}
+class YellowUpdate {
+    const VERSION = "0.7.16";
+    public $yellow;                 //access to API
+    public $updates;                //number of updates
+    
+    // Handle initialisation
+    public function onLoad($yellow) {
+        $this->yellow = $yellow;
+        $this->yellow->config->setDefault("updatePluginsUrl", "https://github.com/datenstrom/yellow-plugins");
+        $this->yellow->config->setDefault("updateThemesUrl", "https://github.com/datenstrom/yellow-themes");
+        $this->yellow->config->setDefault("updateInformationFile", "update.ini");
+        $this->yellow->config->setDefault("updateVersionFile", "version.ini");
+        $this->yellow->config->setDefault("updateResourceFile", "resource.ini");
+    }
+    
+    // Handle startup
+    public function onStartup($update) {
+        if ($update) {
+            $fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
+            $fileData = $this->yellow->toolbox->readFile($fileNameConfig);
+            $configDefaults = new YellowDataCollection();
+            $configDefaults->exchangeArray($this->yellow->config->configDefaults->getArrayCopy());
+            foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+                preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+                if (!empty($matches[1]) && !is_null($configDefaults[$matches[1]])) unset($configDefaults[$matches[1]]);
+                if (!empty($matches[1]) && $matches[1][0]!="#" && is_null($this->yellow->config->configDefaults[$matches[1]])) {
+                    $fileDataNew .= "# $line";
+                } else {
+                    $fileDataNew .= $line;
+                }
+            }
+            unset($configDefaults["configFile"]);
+            foreach ($configDefaults as $key=>$value) {
+                $fileDataNew .= ucfirst($key).": $value\n";
+            }
+            if ($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileNameConfig, $fileDataNew);
+        }
+        if ($update) {  //TODO: remove later, converts old theme
+            $path = $this->yellow->config->get("themeDir");
+            foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.css$/", true, false) as $entry) {
+                $fileNameAsset = $this->yellow->config->get("assetDir").basename($entry);
+                if (!is_file($fileNameAsset)) {
+                    $fileData = $this->yellow->toolbox->readFile($entry);
+                    $fileData = preg_replace("#url\(assets/(.*?)\)#", "url($1)", $fileData);
+                    $this->yellow->toolbox->createFile($fileNameAsset, $fileData);
+                }
+                $this->yellow->toolbox->deleteFile($entry, $this->yellow->config->get("trashDir"));
+                $_GET["clean-url"] = "theme-has-been-updated";
+            }
+        }
+        if ($update) {  //TODO: remove later, converts old error page
+            $fileName = $this->yellow->config->get("configDir")."page-error-500.txt";
+            if (is_file($fileName)) {
+                $fileData = $this->yellow->toolbox->readFile($fileName);
+                $fileDataNew = preg_replace("/@pageerror/", "[yellow error]", $fileData);
+                if ($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileName, $fileDataNew);
+            }
+        }
+        if ($update) {  //TODO: remove later, converts new blog page
+            $fileName = $this->yellow->config->get("configDir")."page-new-blog.txt";
+            if (is_file($fileName)) {
+                $fileData = $this->yellow->toolbox->readFile($fileName);
+                $fileDataNew = $this->yellow->toolbox->setMetaData($fileData, "template", "blog");
+                if ($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileName, $fileDataNew);
+            }
+        }
+        if ($update) {  //TODO: remove later, converts new wiki page
+            $fileName = $this->yellow->config->get("configDir")."page-new-wiki.txt";
+            if (is_file($fileName)) {
+                $fileData = $this->yellow->toolbox->readFile($fileName);
+                $fileDataNew = $this->yellow->toolbox->setMetaData($fileData, "template", "wiki");
+                if ($fileData!=$fileDataNew) $this->yellow->toolbox->createFile($fileName, $fileDataNew);
+            }
+        }
+        if ($update) {  //TODO: remove later, converts template settings
+            $valueDefault = $this->yellow->config->get("template");
+            foreach ($this->yellow->pages->index(true, true) as $page) {
+                preg_match("/^.*\/(.+?)$/", dirname($page->fileName), $matches);
+                $value = $this->yellow->lookup->normaliseName($matches[1], true, false, true);
+                if (!is_file($this->yellow->config->get("templateDir").$value.".html")) $value = $valueDefault;
+                $pageTemplate = $this->yellow->toolbox->getMetaData($page->rawData, "template");
+                $pagePublished = $this->yellow->toolbox->getMetaData($page->rawData, "published");
+                if (empty($pagePublished) && $value=="blog") $value = $valueDefault;
+                if (empty($pageTemplate) && $value!=$valueDefault) {
+                    $rawDataNew = $this->yellow->toolbox->setMetaData($page->rawData, "template", $value);
+                    if ($page->rawData!=$rawDataNew) $this->yellow->toolbox->createFile($page->fileName, $rawDataNew);
+                }
+            }
+            foreach ($this->yellow->pages->index(true, true)->filter("template", "blogpages") as $page) {
+                $rawDataNew = $this->yellow->toolbox->setMetaData($page->rawData, "templateNew", "blog");
+                if ($page->rawData!=$rawDataNew) $this->yellow->toolbox->createFile($page->fileName, $rawDataNew);
+            }
+            foreach ($this->yellow->pages->index(true, true)->filter("template", "wikipages") as $page) {
+                $rawDataNew = $this->yellow->toolbox->setMetaData($page->rawData, "templateNew", "wiki");
+                if ($page->rawData!=$rawDataNew) $this->yellow->toolbox->createFile($page->fileName, $rawDataNew);
+            }
+            $this->yellow->pages = new YellowPages($this->yellow);
+        }
+    }
+    
+    // Handle request
+    public function onRequest($scheme, $address, $base, $location, $fileName) {
+        $statusCode = 0;
+        if ($this->yellow->config->get("installationMode")) {
+            $statusCode = $this->processRequestInstallationMode($scheme, $address, $base, $location, $fileName);
+        } else {
+            $statusCode = $this->processRequestInstallationPending($scheme, $address, $base, $location, $fileName);
+        }
+        return $statusCode;
+    }
+    
+    // Handle command
+    public function onCommand($args) {
+        list($command) = $args;
+        switch ($command) {
+            case "clean":   $statusCode = $this->cleanCommand($args); break;
+            case "update":  $statusCode = $this->updateCommand($args); break;
+            default:        $statusCode = $this->processCommandInstallationPending($args); break;
+        }
+        return $statusCode;
+    }
+    
+    // Handle command help
+    public function onCommandHelp() {
+        return "update [option feature]";
+    }
+    
+    // Clean downloads
+    public function cleanCommand($args) {
+        $statusCode = 0;
+        list($command, $path) = $args;
+        if ($path=="all") {
+            $path = $this->yellow->config->get("pluginDir");
+            $regex = "/^.*\\".$this->yellow->config->get("downloadExtension")."$/";
+            foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, false, false) as $entry) {
+                if (!$this->yellow->toolbox->deleteFile($entry)) $statusCode = 500;
+            }
+            if ($statusCode==500) echo "ERROR cleaning downloads: Can't delete files in directory '$path'!\n";
+        }
+        return $statusCode;
+    }
+    
+    // Update website
+    public function updateCommand($args) {
+        list($command, $option, $feature) = $args;
+        if (empty($option) || $option=="normal" || $option=="force") {
+            $force = $option=="force";
+            list($statusCode, $data) = $this->detectSoftware($force, $feature);
+            if ($statusCode!=200 || !empty($data)) {
+                $this->updates = 0;
+                if ($statusCode==200) $statusCode = $this->downloadSoftware($data);
+                if ($statusCode==200) $statusCode = $this->updateSoftware($force);
+                if ($statusCode>=400) echo "ERROR updating files: ".$this->yellow->page->get("pageError")."\n";
+                echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated";
+                echo ", $this->updates update".($this->updates!=1 ? "s" : "")." installed\n";
+            } else {
+                echo "Your website is up to date\n";
+            }
+        } else {
+            $statusCode = 400;
+            echo "Yellow $command: Invalid arguments\n";
+        }
+        return $statusCode;
+    }
+    
+    // Detect software
+    public function detectSoftware($force, $feature) {
+        $data = array();
+        list($statusCodeCurrent, $dataCurrent) = $this->getSoftwareVersion();
+        list($statusCodeLatest, $dataLatest) = $this->getSoftwareVersion(true, true);
+        list($statusCodeModified, $dataModified) = $this->getSoftwareModified();
+        $statusCode = max($statusCodeCurrent, $statusCodeLatest, $statusCodeModified);
+        if (empty($feature)) {
+            foreach ($dataCurrent as $key=>$value) {
+                list($version) = explode(",", $dataLatest[$key]);
+                if (strnatcasecmp($dataCurrent[$key], $version)<0) $data[$key] = $dataLatest[$key];
+                if (!is_null($dataModified[$key]) && !empty($version) && $force) $data[$key] = $dataLatest[$key];
+            }
+        } else {
+            foreach ($dataCurrent as $key=>$value) {
+                list($version) = explode(",", $dataLatest[$key]);
+                if (strtoloweru($key)==strtoloweru($feature) && !empty($version)) {
+                    $data[$key] = $dataLatest[$key];
+                    $dataModified = array_intersect_key($dataModified, $data);
+                    break;
+                }
+            }
+            if (empty($data)) {
+                $statusCode = 500;
+                $this->yellow->page->error($statusCode, "Can't find feature '$feature'!");
+            }
+        }
+        if ($statusCode==200) {
+            foreach (array_merge($dataModified, $data) as $key=>$value) {
+                list($version) = explode(",", $value);
+                if (is_null($dataModified[$key]) || $force) {
+                    echo "$key $version\n";
+                } else {
+                    echo "$key $version has been modified - Force update\n";
+                }
+            }
+        }
+        return array($statusCode, $data);
+    }
+    
+    // Download software
+    public function downloadSoftware($data) {
+        $statusCode = 200;
+        $path = $this->yellow->config->get("pluginDir");
+        $fileExtension = $this->yellow->config->get("downloadExtension");
+        foreach ($data as $key=>$value) {
+            $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip";
+            list($version, $url) = explode(",", $value);
+            list($statusCode, $fileData) = $this->getSoftwareFile($url);
+            if (empty($fileData) || !$this->yellow->toolbox->createFile($fileName.$fileExtension, $fileData)) {
+                $statusCode = 500;
+                $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+                break;
+            }
+        }
+        if ($statusCode==200) {
+            foreach ($data as $key=>$value) {
+                $fileName = $path.$this->yellow->lookup->normaliseName($key, true, false, true).".zip";
+                if (!$this->yellow->toolbox->renameFile($fileName.$fileExtension, $fileName)) {
+                    $statusCode = 500;
+                    $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+                }
+            }
+        }
+        return $statusCode;
+    }
 
-	// Update software
-	function updateSoftware($force = false)
-	{
-		$statusCode = 200;
-		if(function_exists("opcache_reset")) opcache_reset();
-		$path = $this->yellow->config->get("pluginDir");
-		foreach($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry)
-		{
-			$statusCode = max($statusCode, $this->updateSoftwareArchive($entry, $force));
-			if(!$this->yellow->toolbox->deleteFile($entry))
-			{
-				$statusCode = 500;
-				$this->yellow->page->error($statusCode, "Can't delete file '$entry'!");
-			}
-		}
-		$path = $this->yellow->config->get("themeDir");
-		foreach($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry)
-		{
-			$statusCode = max($statusCode, $this->updateSoftwareArchive($entry, $force));
-			if(!$this->yellow->toolbox->deleteFile($entry))
-			{
-				$statusCode = 500;
-				$this->yellow->page->error($statusCode, "Can't delete file '$entry'!");
-			}
-		}
-		return $statusCode;
-	}
+    // Update software
+    public function updateSoftware($force = false) {
+        $statusCode = 200;
+        if (function_exists("opcache_reset")) opcache_reset();
+        $path = $this->yellow->config->get("pluginDir");
+        foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) {
+            $statusCode = max($statusCode, $this->updateSoftwareArchive($entry, $force));
+            if (!$this->yellow->toolbox->deleteFile($entry)) {
+                $statusCode = 500;
+                $this->yellow->page->error($statusCode, "Can't delete file '$entry'!");
+            }
+        }
+        $path = $this->yellow->config->get("themeDir");
+        foreach ($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry) {
+            $statusCode = max($statusCode, $this->updateSoftwareArchive($entry, $force));
+            if (!$this->yellow->toolbox->deleteFile($entry)) {
+                $statusCode = 500;
+                $this->yellow->page->error($statusCode, "Can't delete file '$entry'!");
+            }
+        }
+        return $statusCode;
+    }
 
-	// Update software from archive
-	function updateSoftwareArchive($path, $force = false)
-	{
-		$statusCode = 200;
-		$zip = new ZipArchive();
-		if($zip->open($path)===true)
-		{
-			if(defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateSoftwareArchive file:$path<br/>\n";
-			if(preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1];
-			$fileData = $zip->getFromName($pathBase.$this->yellow->config->get("updateInformationFile"));
-			foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-			{
-				preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-				if(!empty($matches[1]) && !empty($matches[2]))
-				{
-					list($dummy, $entry) = explode('/', $matches[1], 2);
-					list($fileName, $flags) = explode(',', $matches[2], 2);
-					if($dummy[0]!='Y') $fileName = $matches[1]; //TODO: remove later, converts old file format
-					if(is_file($fileName)) { $lastPublished = filemtime($fileName); break; }
-				}
-			}
-			foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-			{
-				preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-				if(lcfirst($matches[1])=="plugin" || lcfirst($matches[1])=="theme") $software = $matches[2];
-				if(lcfirst($matches[1])=="published") $modified = strtotime($matches[2]);
-				if(!empty($matches[1]) && !empty($matches[2]) && strposu($matches[1], '/'))
-				{
-					list($dummy, $entry) = explode('/', $matches[1], 2);
-					list($fileName, $flags) = explode(',', $matches[2], 2);
-					if($dummy[0]!='Y') //TODO: remove later, converts old file format
-					{
-						list($entry, $flags) = explode(',', $matches[2], 2);
-						$fileName = $matches[1];
-					}
-					$fileData = $zip->getFromName($pathBase.$entry);
-					$lastModified = $this->yellow->toolbox->getFileModified($fileName);
-					$statusCode = $this->updateSoftwareFile($fileName, $fileData, $modified, $lastModified, $lastPublished, $flags, $force, $software);
-					if($statusCode!=200) break;
-				}
-			}
-			$zip->close();
-			if($statusCode==200) $statusCode = $this->updateSoftwareMultiLanguage($software);
-			if($statusCode==200) $statusCode = $this->updateSoftwareNotification($software);
-			++$this->updates;
-		} else {
-			$statusCode = 500;
-			$this->yellow->page->error(500, "Can't open file '$path'!");
-		}
-		return $statusCode;
-	}
-	
-	// Update software file
-	function updateSoftwareFile($fileName, $fileData, $modified, $lastModified, $lastPublished, $flags, $force, $software)
-	{
-		$statusCode = 200;
-		$fileName = $this->yellow->toolbox->normaliseTokens($fileName);
-		if($this->yellow->lookup->isValidFile($fileName) && !empty($software))
-		{
-			$create = $update = $delete = false;
-			if(preg_match("/create/i", $flags) && !is_file($fileName) && !empty($fileData)) $create = true;
-			if(preg_match("/update/i", $flags) && is_file($fileName) && !empty($fileData)) $update = true;
-			if(preg_match("/delete/i", $flags) && is_file($fileName)) $delete = true;
-			if(preg_match("/careful/i", $flags) && is_file($fileName) && $lastModified!=$lastPublished && !$force) $update = false;
-			if(preg_match("/optional/i", $flags) && $this->isSoftwareExisting($software)) $create = $update = $delete = false;
-			if($create)
-			{
-				if(!$this->yellow->toolbox->createFile($fileName, $fileData, true) ||
-				   !$this->yellow->toolbox->modifyFile($fileName, $modified))
-				{
-					$statusCode = 500;
-					$this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
-				}
-			}
-			if($update)
-			{
-				if(!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir")) ||
-				   !$this->yellow->toolbox->createFile($fileName, $fileData) ||
-				   !$this->yellow->toolbox->modifyFile($fileName, $modified))
-				{
-					$statusCode = 500;
-					$this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
-				}
-			}
-			if($delete)
-			{
-				if(!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir")))
-				{
-					$statusCode = 500;
-					$this->yellow->page->error($statusCode, "Can't delete file '$fileName'!");
-				}
-			}
-			if(defined("DEBUG") && DEBUG>=2)
-			{
-				$debug = "action:".($create ? "create" : "").($update ? "update" : "").($delete ? "delete" : "");
-				if(!$create && !$update && !$delete) $debug = "action:none";
-				echo "YellowUpdate::updateSoftwareFile file:$fileName $debug<br/>\n";
-			}
-		}
-		return $statusCode;
-	}
+    // Update software from archive
+    public function updateSoftwareArchive($path, $force = false) {
+        $statusCode = 200;
+        $zip = new ZipArchive();
+        if ($zip->open($path)===true) {
+            if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateSoftwareArchive file:$path<br/>\n";
+            if (preg_match("#^(.*\/).*?$#", $zip->getNameIndex(0), $matches)) $pathBase = $matches[1];
+            $fileData = $zip->getFromName($pathBase.$this->yellow->config->get("updateInformationFile"));
+            foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+                preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+                if (!empty($matches[1]) && !empty($matches[2])) {
+                    list($dummy, $entry) = explode("/", $matches[1], 2);
+                    list($fileName, $flags) = explode(",", $matches[2], 2);
+                    if ($dummy[0]!="Y") $fileName = $matches[1];    //TODO: remove later, converts old file format
+                    if (is_file($fileName)) {
+                        $lastPublished = filemtime($fileName);
+                        break;
+                    }
+                }
+            }
+            foreach ($this->yellow->toolbox->getTextLines($fileData) as $line) {
+                preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+                if (lcfirst($matches[1])=="plugin" || lcfirst($matches[1])=="theme") $software = $matches[2];
+                if (lcfirst($matches[1])=="published") $modified = strtotime($matches[2]);
+                if (!empty($matches[1]) && !empty($matches[2]) && strposu($matches[1], "/")) {
+                    list($dummy, $entry) = explode("/", $matches[1], 2);
+                    list($fileName, $flags) = explode(",", $matches[2], 2);
+                    if ($dummy[0]!="Y") { //TODO: remove later, converts old file format
+                        list($entry, $flags) = explode(",", $matches[2], 2);
+                        $fileName = $matches[1];
+                    }
+                    $fileData = $zip->getFromName($pathBase.$entry);
+                    $lastModified = $this->yellow->toolbox->getFileModified($fileName);
+                    $statusCode = $this->updateSoftwareFile($fileName, $fileData, $modified, $lastModified, $lastPublished, $flags, $force, $software);
+                    if ($statusCode!=200) break;
+                }
+            }
+            $zip->close();
+            if ($statusCode==200) $statusCode = $this->updateSoftwareMultiLanguage($software);
+            if ($statusCode==200) $statusCode = $this->updateSoftwareNotification($software);
+            ++$this->updates;
+        } else {
+            $statusCode = 500;
+            $this->yellow->page->error(500, "Can't open file '$path'!");
+        }
+        return $statusCode;
+    }
+    
+    // Update software file
+    public function updateSoftwareFile($fileName, $fileData, $modified, $lastModified, $lastPublished, $flags, $force, $software) {
+        $statusCode = 200;
+        $fileName = $this->yellow->toolbox->normaliseTokens($fileName);
+        if ($this->yellow->lookup->isValidFile($fileName) && !empty($software)) {
+            $create = $update = $delete = false;
+            if (preg_match("/create/i", $flags) && !is_file($fileName) && !empty($fileData)) $create = true;
+            if (preg_match("/update/i", $flags) && is_file($fileName) && !empty($fileData)) $update = true;
+            if (preg_match("/delete/i", $flags) && is_file($fileName)) $delete = true;
+            if (preg_match("/careful/i", $flags) && is_file($fileName) && $lastModified!=$lastPublished && !$force) $update = false;
+            if (preg_match("/optional/i", $flags) && $this->isSoftwareExisting($software)) $create = $update = $delete = false;
+            if ($create) {
+                if (!$this->yellow->toolbox->createFile($fileName, $fileData, true) ||
+                    !$this->yellow->toolbox->modifyFile($fileName, $modified)) {
+                    $statusCode = 500;
+                    $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+                }
+            }
+            if ($update) {
+                if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir")) ||
+                    !$this->yellow->toolbox->createFile($fileName, $fileData) ||
+                    !$this->yellow->toolbox->modifyFile($fileName, $modified)) {
+                    $statusCode = 500;
+                    $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+                }
+            }
+            if ($delete) {
+                if (!$this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir"))) {
+                    $statusCode = 500;
+                    $this->yellow->page->error($statusCode, "Can't delete file '$fileName'!");
+                }
+            }
+            if (defined("DEBUG") && DEBUG>=2) {
+                $debug = "action:".($create ? "create" : "").($update ? "update" : "").($delete ? "delete" : "");
+                if (!$create && !$update && !$delete) $debug = "action:none";
+                echo "YellowUpdate::updateSoftwareFile file:$fileName $debug<br/>\n";
+            }
+        }
+        return $statusCode;
+    }
 
-	// Update software for multiple languages
-	function updateSoftwareMultiLanguage($software)
-	{
-		$statusCode = 200;
-		if($this->yellow->config->get("multiLanguageMode") && !$this->isSoftwareExisting($software))
-		{
-			$pathsSource = $pathsTarget = array();
-			$pathBase = $this->yellow->config->get("contentDir");
-			$fileExtension = $this->yellow->config->get("contentExtension");
-			$fileRegex = "/^.*\\".$fileExtension."$/";
-			foreach($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true) as $entry)
-			{
-				if(count($this->yellow->toolbox->getDirectoryEntries($entry, $fileRegex, false, false)))
-				{
-					array_push($pathsSource, $entry."/");
-				} else if(count($this->yellow->toolbox->getDirectoryEntries($entry, "/.*/", false, true))) {
-					array_push($pathsTarget, $entry."/");
-				}
-			}
-			if(count($pathsSource) && count($pathsTarget))
-			{
-				foreach($pathsSource as $pathSource)
-				{
-					foreach($pathsTarget as $pathTarget)
-					{
-						$fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($pathSource, "/.*/", false, false);
-						foreach($fileNames as $fileName)
-						{
-							$modified = $this->yellow->toolbox->getFileModified($fileName);
-							$fileNameTarget = $pathTarget.substru($fileName, strlenu($pathBase));
-							if(!is_file($fileNameTarget))
-							{
-								if(!$this->yellow->toolbox->copyFile($fileName, $fileNameTarget, true) ||
-								   !$this->yellow->toolbox->modifyFile($fileNameTarget, $modified))
-								{
-									$statusCode = 500;
-									$this->yellow->page->error(500, "Can't write file '$fileNameTarget'!");
-								}
-							}
-							if(defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateSoftwareNew file:$fileNameTarget<br/>\n";
-						}
-					}
-					if(!$this->yellow->toolbox->deleteDirectory($pathSource))
-					{
-						$statusCode = 500;
-						$this->yellow->page->error(500, "Can't delete path '$pathSource'!");
-					}
-				}
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Update software notification for next startup
-	function updateSoftwareNotification($software)
-	{
-		$statusCode = 200;
-		$startupUpdate = $this->yellow->config->get("startupUpdate");
-		if($startupUpdate=="none") $startupUpdate = "YellowUpdate";
-		if($software!="YellowUpdate") $startupUpdate .= ",$software";
-		$fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
-		if(!$this->yellow->config->save($fileNameConfig, array("startupUpdate" => $startupUpdate)))
-		{
-			$statusCode = 500;
-			$this->yellow->page->error(500, "Can't write file '$fileNameConfig'!");
-		}
-		return $statusCode;
-	}
-	
-	// Update installation features
-	function updateInstallationFeatures($feature)
-	{
-		$statusCode = 200;
-		$path = $this->yellow->config->get("pluginDir");
-		$regex = "/^.*\.installation$/";
-		foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false) as $entry)
-		{
-			if(preg_match("/^(.*?)-(.*?)\./", basename($entry), $matches))
-			{
-				if(strtoloweru($matches[2])==strtoloweru($feature))
-				{
-					$statusCode = max($statusCode, $this->updateSoftwareArchive($entry));
-					break;
-				}
-			}
-		}
-		if($statusCode==200)
-		{
-			foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false) as $entry)
-			{
-				$this->yellow->toolbox->deleteFile($entry);
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Update installation page
-	function updateInstallationPage($fileName, $name, $language)
-	{
-		$statusCode = 200;
-		if($language!="en")
-		{
-			$fileData = strreplaceu("\r\n", "\n", $this->yellow->toolbox->readFile($fileName));
-			$rawDataOld = strreplaceu("\\n", "\n", $this->yellow->text->getText("editInstallation{$name}Page", "en"));
-			$rawDataNew = strreplaceu("\\n", "\n", $this->yellow->text->getText("editInstallation{$name}Page", $language));
-			if(!$this->yellow->toolbox->createFile($fileName, strreplaceu($rawDataOld, $rawDataNew, $fileData)))
-			{
-				$statusCode = 500;
-				$this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Process command to install pending software
-	function processCommandInstallationPending($args)
-	{
-		$statusCode = 0;
-		if($this->isSoftwarePending())
-		{
-			$statusCode = $this->updateSoftware();
-			if($statusCode!=200) echo "ERROR updating files: ".$this->yellow->page->get("pageError")."\n";
-			echo "Yellow has ".($statusCode!=200 ? "not " : "")."been updated: Please run command again\n";
-		}
-		return $statusCode;
-	}
-	
-	// Process request to install pending software
-	function processRequestInstallationPending($scheme, $address, $base, $location, $fileName)
-	{
-		$statusCode = 0;
-		if($this->yellow->lookup->isContentFile($fileName) && !$this->yellow->isCommandLine() && $this->isSoftwarePending())
-		{
-			$statusCode = $this->updateSoftware();
-			if($statusCode==200)
-			{
-				$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
-				$statusCode = $this->yellow->sendStatus(303, $location);
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Process request to install website
-	function processRequestInstallationMode($scheme, $address, $base, $location, $fileName)
-	{
-		$statusCode = 0;
-		if($this->yellow->lookup->isContentFile($fileName) && !$this->yellow->isCommandLine())
-		{
-			$this->yellow->pages->pages["root/"] = array();
-			$this->yellow->page = new YellowPage($this->yellow);
-			$this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName);
-			$this->yellow->page->parseData($this->getRawDataInstallation(), false, 404);
-			$this->yellow->page->parserSafeMode = false;
-			$this->yellow->page->parseContent();
-			$name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"]));
-			$email = trim($_REQUEST["email"]);
-			$password = trim($_REQUEST["password"]);
-			$language = trim($_REQUEST["language"]);
-			$feature = trim($_REQUEST["feature"]);
-			$status = trim($_REQUEST["status"]);
-			if($status=="install")
-			{
-				$serverVersion = $this->yellow->toolbox->getServerVersion(true);
-				$status = $this->checkServerRewrite($scheme, $address, $base, $location, $fileName) ? "ok" : "error";
-				if($status=="error") $this->yellow->page->error(500, "Rewrite module not working on $serverVersion web server!");
-			}
-			if($status=="ok")
-			{
-				if(!empty($email) && !empty($password) && $this->yellow->plugins->isExisting("edit"))
-				{
-					$fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
-					$status = $this->yellow->plugins->get("edit")->users->save($fileNameUser, $email, $password, $name, $language) ? "ok" : "error";
-					if($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
-				}
-			}
-			if($status=="ok")
-			{
-				if(!empty($feature))
-				{
-					$status = $this->updateInstallationFeatures($feature)==200 ? "ok" : "error";
-					if($status=="error") $this->yellow->page->error(500, "Can't install feature '$feature'!");
-				}
-			}
-			if($status=="ok")
-			{
-				$fileNameHome = $this->yellow->lookup->findFileFromLocation("/");
-				$status = $this->updateInstallationPage($fileNameHome, "Home", $language)==200 ? "ok" : "error";
-				if($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameHome'!");
-			}
-			if($status=="ok")
-			{
-				$fileNameAbout = $this->yellow->lookup->findFileFromLocation("/about/");
-				$status = $this->updateInstallationPage($fileNameAbout, "About", $language)==200 ? "ok" : "error";
-				if($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameAbout'!");
-			}
-			if($status=="ok")
-			{
-				if($this->yellow->config->get("sitename")=="Yellow") $_REQUEST["sitename"] = $name;
-				$fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
-				$status = $this->yellow->config->save($fileNameConfig, $this->getConfigData()) ? "done" : "error";
-				if($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameConfig'!");
-			}
-			if($status=="done")
-			{
-				$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
-				$statusCode = $this->yellow->sendStatus(303, $location);
-			} else {
-				$statusCode = $this->yellow->sendPage();
-			}
-		}
-		return $statusCode;
-	}
-	
-	// Check web server rewrite
-	function checkServerRewrite($scheme, $address, $base, $location, $fileName)
-	{
-		$curlHandle = curl_init();
-		$location = $this->yellow->config->get("assetLocation").$this->yellow->page->get("theme").".css";
-		$url = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
-		curl_setopt($curlHandle, CURLOPT_URL, $url);
-		curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowCore/".YellowCore::VERSION).")";
-		curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
-		curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30);
-		$rawData = curl_exec($curlHandle);
-		$statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
-		curl_close($curlHandle);
-		return !empty($rawData) && $statusCode==200;
-	}
-	
-	// Return raw data for installation page
-	function getRawDataInstallation()
-	{
-		$language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->text->getLanguages(), $this->yellow->config->get("language"));
-		$fileName = strreplaceu("(.*)", "installation", $this->yellow->config->get("configDir").$this->yellow->config->get("newFile"));
-		$rawData = $this->yellow->toolbox->readFile($fileName);
-		if(empty($rawData))
-		{
-			$this->yellow->text->setLanguage($language);
-			$rawData = "---\nTitle:".$this->yellow->text->get("editInstallationTitle")."\nLanguage:$language\nNavigation:navigation\n---\n";
-			$rawData .= "<form class=\"installation-form\" action=\"".$this->yellow->page->getLocation(true)."\" method=\"post\">\n";
-			$rawData .= "<p><label for=\"name\">".$this->yellow->text->get("editSignupName")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"name\" id=\"name\" value=\"\"></p>\n";
-			$rawData .= "<p><label for=\"email\">".$this->yellow->text->get("editSignupEmail")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"email\" id=\"email\" value=\"\"></p>\n";
-			$rawData .= "<p><label for=\"password\">".$this->yellow->text->get("editSignupPassword")."</label><br /><input class=\"form-control\" type=\"password\" maxlength=\"64\" name=\"password\" id=\"password\" value=\"\"></p>\n";
-			if(count($this->yellow->text->getLanguages())>1)
-			{
-				$rawData .= "<p>";
-				foreach($this->yellow->text->getLanguages() as $language)
-				{
-					$checked = $language==$this->yellow->text->language ? " checked=\"checked\"" : "";
-					$rawData .= "<label for=\"$language\"><input type=\"radio\" name=\"language\" id=\"$language\" value=\"$language\"$checked> ".$this->yellow->text->getTextHtml("languageDescription", $language)."</label><br />";
-				}
-				$rawData .= "</p>\n";
-			}
-			if(count($this->getInstallationFeatures())>1)
-			{
-				$rawData .= "<p>".$this->yellow->text->get("editInstallationFeature")."<p>";
-				foreach($this->getInstallationFeatures() as $feature)
-				{
-					$checked = $feature=="website" ? " checked=\"checked\"" : "";
-					$rawData .= "<label for=\"$feature\"><input type=\"radio\" name=\"feature\" id=\"$feature\" value=\"$feature\"$checked> ".ucfirst($feature)."</label><br />";
-				}
-				$rawData .= "</p>\n";
-			}
-			$rawData .= "<input class=\"btn\" type=\"submit\" value=\"".$this->yellow->text->get("editOkButton")."\" />\n";
-			$rawData .= "<input type=\"hidden\" name=\"status\" value=\"install\" />\n";
-			$rawData .= "</form>\n";
-		}
-		return $rawData;
-	}
-	
-	// Return configuration data
-	function getConfigData()
-	{
-		$data = array();
-		foreach($_REQUEST as $key=>$value)
-		{
-			if(!$this->yellow->config->isExisting($key)) continue;
-			$data[$key] = trim($value);
-		}
-		$data["timezone"] = $this->yellow->toolbox->getTimezone();
-		$data["staticUrl"] = $this->yellow->toolbox->getServerUrl();
-		$data["installationMode"] = "0";
-		return $data;
-	}
+    // Update software for multiple languages
+    public function updateSoftwareMultiLanguage($software) {
+        $statusCode = 200;
+        if ($this->yellow->config->get("multiLanguageMode") && !$this->isSoftwareExisting($software)) {
+            $pathsSource = $pathsTarget = array();
+            $pathBase = $this->yellow->config->get("contentDir");
+            $fileExtension = $this->yellow->config->get("contentExtension");
+            $fileRegex = "/^.*\\".$fileExtension."$/";
+            foreach ($this->yellow->toolbox->getDirectoryEntries($pathBase, "/.*/", true, true) as $entry) {
+                if (count($this->yellow->toolbox->getDirectoryEntries($entry, $fileRegex, false, false))) {
+                    array_push($pathsSource, $entry."/");
+                } elseif (count($this->yellow->toolbox->getDirectoryEntries($entry, "/.*/", false, true))) {
+                    array_push($pathsTarget, $entry."/");
+                }
+            }
+            if (count($pathsSource) && count($pathsTarget)) {
+                foreach ($pathsSource as $pathSource) {
+                    foreach ($pathsTarget as $pathTarget) {
+                        $fileNames = $this->yellow->toolbox->getDirectoryEntriesRecursive($pathSource, "/.*/", false, false);
+                        foreach ($fileNames as $fileName) {
+                            $modified = $this->yellow->toolbox->getFileModified($fileName);
+                            $fileNameTarget = $pathTarget.substru($fileName, strlenu($pathBase));
+                            if (!is_file($fileNameTarget)) {
+                                if (!$this->yellow->toolbox->copyFile($fileName, $fileNameTarget, true) ||
+                                    !$this->yellow->toolbox->modifyFile($fileNameTarget, $modified)) {
+                                    $statusCode = 500;
+                                    $this->yellow->page->error(500, "Can't write file '$fileNameTarget'!");
+                                }
+                            }
+                            if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateSoftwareNew file:$fileNameTarget<br/>\n";
+                        }
+                    }
+                    if (!$this->yellow->toolbox->deleteDirectory($pathSource)) {
+                        $statusCode = 500;
+                        $this->yellow->page->error(500, "Can't delete path '$pathSource'!");
+                    }
+                }
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Update software notification for next startup
+    public function updateSoftwareNotification($software) {
+        $statusCode = 200;
+        $startupUpdate = $this->yellow->config->get("startupUpdate");
+        if ($startupUpdate=="none") $startupUpdate = "YellowUpdate";
+        if ($software!="YellowUpdate") $startupUpdate .= ",$software";
+        $fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
+        if (!$this->yellow->config->save($fileNameConfig, array("startupUpdate" => $startupUpdate))) {
+            $statusCode = 500;
+            $this->yellow->page->error(500, "Can't write file '$fileNameConfig'!");
+        }
+        return $statusCode;
+    }
+    
+    // Update installation features
+    public function updateInstallationFeatures($feature) {
+        $statusCode = 200;
+        $path = $this->yellow->config->get("pluginDir");
+        $regex = "/^.*\.installation$/";
+        foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false) as $entry) {
+            if (preg_match("/^(.*?)-(.*?)\./", basename($entry), $matches)) {
+                if (strtoloweru($matches[2])==strtoloweru($feature)) {
+                    $statusCode = max($statusCode, $this->updateSoftwareArchive($entry));
+                    break;
+                }
+            }
+        }
+        if ($statusCode==200) {
+            foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false) as $entry) {
+                $this->yellow->toolbox->deleteFile($entry);
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Update installation page
+    public function updateInstallationPage($fileName, $name, $language) {
+        $statusCode = 200;
+        if ($language!="en") {
+            $fileData = strreplaceu("\r\n", "\n", $this->yellow->toolbox->readFile($fileName));
+            $rawDataOld = strreplaceu("\\n", "\n", $this->yellow->text->getText("editInstallation{$name}Page", "en"));
+            $rawDataNew = strreplaceu("\\n", "\n", $this->yellow->text->getText("editInstallation{$name}Page", $language));
+            if (!$this->yellow->toolbox->createFile($fileName, strreplaceu($rawDataOld, $rawDataNew, $fileData))) {
+                $statusCode = 500;
+                $this->yellow->page->error($statusCode, "Can't write file '$fileName'!");
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Process command to install pending software
+    public function processCommandInstallationPending($args) {
+        $statusCode = 0;
+        if ($this->isSoftwarePending()) {
+            $statusCode = $this->updateSoftware();
+            if ($statusCode!=200) echo "ERROR updating files: ".$this->yellow->page->get("pageError")."\n";
+            echo "Yellow has ".($statusCode!=200 ? "not " : "")."been updated: Please run command again\n";
+        }
+        return $statusCode;
+    }
+    
+    // Process request to install pending software
+    public function processRequestInstallationPending($scheme, $address, $base, $location, $fileName) {
+        $statusCode = 0;
+        if ($this->yellow->lookup->isContentFile($fileName) && !$this->yellow->isCommandLine() && $this->isSoftwarePending()) {
+            $statusCode = $this->updateSoftware();
+            if ($statusCode==200) {
+                $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+                $statusCode = $this->yellow->sendStatus(303, $location);
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Process request to install website
+    public function processRequestInstallationMode($scheme, $address, $base, $location, $fileName) {
+        $statusCode = 0;
+        if ($this->yellow->lookup->isContentFile($fileName) && !$this->yellow->isCommandLine()) {
+            $this->yellow->pages->pages["root/"] = array();
+            $this->yellow->page = new YellowPage($this->yellow);
+            $this->yellow->page->setRequestInformation($scheme, $address, $base, $location, $fileName);
+            $this->yellow->page->parseData($this->getRawDataInstallation(), false, 404);
+            $this->yellow->page->parserSafeMode = false;
+            $this->yellow->page->parseContent();
+            $name = trim(preg_replace("/[^\pL\d\-\. ]/u", "-", $_REQUEST["name"]));
+            $email = trim($_REQUEST["email"]);
+            $password = trim($_REQUEST["password"]);
+            $language = trim($_REQUEST["language"]);
+            $feature = trim($_REQUEST["feature"]);
+            $status = trim($_REQUEST["status"]);
+            if ($status=="install") {
+                $serverVersion = $this->yellow->toolbox->getServerVersion(true);
+                $status = $this->checkServerRewrite($scheme, $address, $base, $location, $fileName) ? "ok" : "error";
+                if ($status=="error") $this->yellow->page->error(500, "Rewrite module not working on $serverVersion web server!");
+            }
+            if ($status=="ok") {
+                if (!empty($email) && !empty($password) && $this->yellow->plugins->isExisting("edit")) {
+                    $fileNameUser = $this->yellow->config->get("configDir").$this->yellow->config->get("editUserFile");
+                    $status = $this->yellow->plugins->get("edit")->users->save($fileNameUser, $email, $password, $name, $language) ? "ok" : "error";
+                    if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameUser'!");
+                }
+            }
+            if ($status=="ok") {
+                if (!empty($feature)) {
+                    $status = $this->updateInstallationFeatures($feature)==200 ? "ok" : "error";
+                    if ($status=="error") $this->yellow->page->error(500, "Can't install feature '$feature'!");
+                }
+            }
+            if ($status=="ok") {
+                $fileNameHome = $this->yellow->lookup->findFileFromLocation("/");
+                $status = $this->updateInstallationPage($fileNameHome, "Home", $language)==200 ? "ok" : "error";
+                if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameHome'!");
+            }
+            if ($status=="ok") {
+                $fileNameAbout = $this->yellow->lookup->findFileFromLocation("/about/");
+                $status = $this->updateInstallationPage($fileNameAbout, "About", $language)==200 ? "ok" : "error";
+                if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameAbout'!");
+            }
+            if ($status=="ok") {
+                if ($this->yellow->config->get("sitename")=="Yellow") $_REQUEST["sitename"] = $name;
+                $fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
+                $status = $this->yellow->config->save($fileNameConfig, $this->getConfigData()) ? "done" : "error";
+                if ($status=="error") $this->yellow->page->error(500, "Can't write file '$fileNameConfig'!");
+            }
+            if ($status=="done") {
+                $location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+                $statusCode = $this->yellow->sendStatus(303, $location);
+            } else {
+                $statusCode = $this->yellow->sendPage();
+            }
+        }
+        return $statusCode;
+    }
+    
+    // Check web server rewrite
+    public function checkServerRewrite($scheme, $address, $base, $location, $fileName) {
+        $curlHandle = curl_init();
+        $location = $this->yellow->config->get("assetLocation").$this->yellow->page->get("theme").".css";
+        $url = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
+        curl_setopt($curlHandle, CURLOPT_URL, $url);
+        curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; YellowCore/".YellowCore::VERSION).")";
+        curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
+        curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30);
+        $rawData = curl_exec($curlHandle);
+        $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
+        curl_close($curlHandle);
+        return !empty($rawData) && $statusCode==200;
+    }
+    
+    // Return raw data for installation page
+    public function getRawDataInstallation() {
+        $language = $this->yellow->toolbox->detectBrowserLanguage($this->yellow->text->getLanguages(), $this->yellow->config->get("language"));
+        $fileName = strreplaceu("(.*)", "installation", $this->yellow->config->get("configDir").$this->yellow->config->get("newFile"));
+        $rawData = $this->yellow->toolbox->readFile($fileName);
+        if (empty($rawData)) {
+            $this->yellow->text->setLanguage($language);
+            $rawData = "---\nTitle:".$this->yellow->text->get("editInstallationTitle")."\nLanguage:$language\nNavigation:navigation\n---\n";
+            $rawData .= "<form class=\"installation-form\" action=\"".$this->yellow->page->getLocation(true)."\" method=\"post\">\n";
+            $rawData .= "<p><label for=\"name\">".$this->yellow->text->get("editSignupName")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"name\" id=\"name\" value=\"\"></p>\n";
+            $rawData .= "<p><label for=\"email\">".$this->yellow->text->get("editSignupEmail")."</label><br /><input class=\"form-control\" type=\"text\" maxlength=\"64\" name=\"email\" id=\"email\" value=\"\"></p>\n";
+            $rawData .= "<p><label for=\"password\">".$this->yellow->text->get("editSignupPassword")."</label><br /><input class=\"form-control\" type=\"password\" maxlength=\"64\" name=\"password\" id=\"password\" value=\"\"></p>\n";
+            if (count($this->yellow->text->getLanguages())>1) {
+                $rawData .= "<p>";
+                foreach ($this->yellow->text->getLanguages() as $language) {
+                    $checked = $language==$this->yellow->text->language ? " checked=\"checked\"" : "";
+                    $rawData .= "<label for=\"$language\"><input type=\"radio\" name=\"language\" id=\"$language\" value=\"$language\"$checked> ".$this->yellow->text->getTextHtml("languageDescription", $language)."</label><br />";
+                }
+                $rawData .= "</p>\n";
+            }
+            if (count($this->getInstallationFeatures())>1) {
+                $rawData .= "<p>".$this->yellow->text->get("editInstallationFeature")."<p>";
+                foreach ($this->getInstallationFeatures() as $feature) {
+                    $checked = $feature=="website" ? " checked=\"checked\"" : "";
+                    $rawData .= "<label for=\"$feature\"><input type=\"radio\" name=\"feature\" id=\"$feature\" value=\"$feature\"$checked> ".ucfirst($feature)."</label><br />";
+                }
+                $rawData .= "</p>\n";
+            }
+            $rawData .= "<input class=\"btn\" type=\"submit\" value=\"".$this->yellow->text->get("editOkButton")."\" />\n";
+            $rawData .= "<input type=\"hidden\" name=\"status\" value=\"install\" />\n";
+            $rawData .= "</form>\n";
+        }
+        return $rawData;
+    }
+    
+    // Return configuration data
+    public function getConfigData() {
+        $data = array();
+        foreach ($_REQUEST as $key=>$value) {
+            if (!$this->yellow->config->isExisting($key)) continue;
+            $data[$key] = trim($value);
+        }
+        $data["timezone"] = $this->yellow->toolbox->getTimezone();
+        $data["staticUrl"] = $this->yellow->toolbox->getServerUrl();
+        $data["installationMode"] = "0";
+        return $data;
+    }
 
-	// Return installation features
-	function getInstallationFeatures()
-	{
-		$data = array("website");
-		$path = $this->yellow->config->get("pluginDir");
-		$regex = "/^.*\.installation$/";
-		foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry)
-		{
-			if(preg_match("/^(.*?)-(.*?)\./", $entry, $matches))
-			{
-				array_push($data, $matches[2]);
-			}
-		}
-		return $data;
-	}
-	
-	// Return software version
-	function getSoftwareVersion($latest = false, $rawFormat = false)
-	{
-		$data = array();
-		if($latest)
-		{
-			$urlPlugins = $this->yellow->config->get("updatePluginsUrl")."/raw/master/".$this->yellow->config->get("updateVersionFile");
-			$urlThemes = $this->yellow->config->get("updateThemesUrl")."/raw/master/".$this->yellow->config->get("updateVersionFile");
-			list($statusCodePlugins, $fileDataPlugins) = $this->getSoftwareFile($urlPlugins);
-			list($statusCodeThemes, $fileDataThemes) = $this->getSoftwareFile($urlThemes);
-			$statusCode = max($statusCodePlugins, $statusCodeThemes);
-			if($statusCode==200)
-			{
-				foreach($this->yellow->toolbox->getTextLines($fileDataPlugins."\n".$fileDataThemes) as $line)
-				{
-					preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-					if(!empty($matches[1]) && !empty($matches[2]))
-					{
-						list($version) = explode(',', $matches[2]);
-						$data[$matches[1]] = $rawFormat ? $matches[2] : $version;
-					}
-				}
-			}
-		} else {
-			$statusCode = 200;
-			$data = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData());
-		}
-		return array($statusCode, $data);
-	}
+    // Return installation features
+    public function getInstallationFeatures() {
+        $data = array("website");
+        $path = $this->yellow->config->get("pluginDir");
+        $regex = "/^.*\.installation$/";
+        foreach ($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false, false) as $entry) {
+            if (preg_match("/^(.*?)-(.*?)\./", $entry, $matches)) array_push($data, $matches[2]);
+        }
+        return $data;
+    }
+    
+    // Return software version
+    public function getSoftwareVersion($latest = false, $rawFormat = false) {
+        $data = array();
+        if ($latest) {
+            $urlPlugins = $this->yellow->config->get("updatePluginsUrl")."/raw/master/".$this->yellow->config->get("updateVersionFile");
+            $urlThemes = $this->yellow->config->get("updateThemesUrl")."/raw/master/".$this->yellow->config->get("updateVersionFile");
+            list($statusCodePlugins, $fileDataPlugins) = $this->getSoftwareFile($urlPlugins);
+            list($statusCodeThemes, $fileDataThemes) = $this->getSoftwareFile($urlThemes);
+            $statusCode = max($statusCodePlugins, $statusCodeThemes);
+            if ($statusCode==200) {
+                foreach ($this->yellow->toolbox->getTextLines($fileDataPlugins."\n".$fileDataThemes) as $line) {
+                    preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+                    if (!empty($matches[1]) && !empty($matches[2])) {
+                        list($version) = explode(",", $matches[2]);
+                        $data[$matches[1]] = $rawFormat ? $matches[2] : $version;
+                    }
+                }
+            }
+        } else {
+            $statusCode = 200;
+            $data = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData());
+        }
+        return array($statusCode, $data);
+    }
 
-	// Return software modification
-	function getSoftwareModified()
-	{
-		$data = array();
-		$dataCurrent = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData());
-		$urlPlugins = $this->yellow->config->get("updatePluginsUrl")."/raw/master/".$this->yellow->config->get("updateResourceFile");
-		$urlThemes = $this->yellow->config->get("updateThemesUrl")."/raw/master/".$this->yellow->config->get("updateResourceFile");
-		list($statusCodePlugins, $fileDataPlugins) = $this->getSoftwareFile($urlPlugins);
-		list($statusCodeThemes, $fileDataThemes) = $this->getSoftwareFile($urlThemes);
-		$statusCode = max($statusCodePlugins, $statusCodeThemes);
-		if($statusCode==200)
-		{
-			foreach($this->yellow->toolbox->getTextLines($fileDataPlugins."\n".$fileDataThemes) as $line)
-			{
-				preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
-				if(!empty($matches[1]) && !empty($matches[2]))
-				{
-					list($softwareNew) = explode('/', $matches[1]);
-					list($fileName, $flags) = explode(',', $matches[2], 2);
-					if($software!=$softwareNew)
-					{
-						$software = $softwareNew;
-						list($fileName, $flags) = explode(',', $matches[2], 2);
-						$lastPublished = $this->yellow->toolbox->getFileModified($fileName);
-					}
-					if($this->yellow->lookup->isValidFile($fileName) && !is_null($dataCurrent[$software]))
-					{
-						$lastModified = $this->yellow->toolbox->getFileModified($fileName);
-						if(preg_match("/update/i", $flags) && preg_match("/careful/i", $flags) && $lastModified!=$lastPublished)
-						{
-							$data[$software] = $dataCurrent[$software];
-							if(defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::getSoftwareModified detected file:$fileName<br/>\n";
-						}
-					}
-				}
-			}
-		}
-		return array($statusCode, $data);
-	}
-	
-	// Return software file
-	function getSoftwareFile($url)
-	{
-		$urlRequest = $url;
-		if(preg_match("#^https://github.com/(.+)/raw/(.+)$#", $url, $matches))
-		{
-			$urlRequest = "https://raw.githubusercontent.com/".$matches[1]."/".$matches[2];
-		}
-		$curlHandle = curl_init();
-		curl_setopt($curlHandle, CURLOPT_URL, $urlRequest);
-		curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; DatenstromYellow/".YellowCore::VERSION."; SoftwareUpdater)");
-		curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
-		curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30);
-		$rawData = curl_exec($curlHandle);
-		$statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
-		curl_close($curlHandle);
-		if($statusCode==200)
-		{
-			$fileData = $rawData;
-		} else if($statusCode==0) {
-			$statusCode = 500;
-			list($scheme, $address) = $this->yellow->lookup->getUrlInformation($url);
-			$this->yellow->page->error($statusCode, "Can't connect to server '$scheme://$address'!");
-		} else {
-			$statusCode = 500;
-			$this->yellow->page->error($statusCode, "Can't download file '$url'!");
-		}
-		if(defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::getSoftwareFile status:$statusCode url:$url<br/>\n";
-		return array($statusCode, $fileData);
-	}
-	
-	// Check if software pending
-	function isSoftwarePending()
-	{
-		$path = $this->yellow->config->get("pluginDir");
-		$foundPlugins = count($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false))>0;
-		$path = $this->yellow->config->get("themeDir");
-		$foundThemes = count($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false))>0;
-		return $foundPlugins || $foundThemes;
-	}
+    // Return software modification
+    public function getSoftwareModified() {
+        $data = array();
+        $dataCurrent = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData());
+        $urlPlugins = $this->yellow->config->get("updatePluginsUrl")."/raw/master/".$this->yellow->config->get("updateResourceFile");
+        $urlThemes = $this->yellow->config->get("updateThemesUrl")."/raw/master/".$this->yellow->config->get("updateResourceFile");
+        list($statusCodePlugins, $fileDataPlugins) = $this->getSoftwareFile($urlPlugins);
+        list($statusCodeThemes, $fileDataThemes) = $this->getSoftwareFile($urlThemes);
+        $statusCode = max($statusCodePlugins, $statusCodeThemes);
+        if ($statusCode==200) {
+            foreach ($this->yellow->toolbox->getTextLines($fileDataPlugins."\n".$fileDataThemes) as $line) {
+                preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+                if (!empty($matches[1]) && !empty($matches[2])) {
+                    list($softwareNew) = explode("/", $matches[1]);
+                    list($fileName, $flags) = explode(",", $matches[2], 2);
+                    if ($software!=$softwareNew) {
+                        $software = $softwareNew;
+                        list($fileName, $flags) = explode(",", $matches[2], 2);
+                        $lastPublished = $this->yellow->toolbox->getFileModified($fileName);
+                    }
+                    if ($this->yellow->lookup->isValidFile($fileName) && !is_null($dataCurrent[$software])) {
+                        $lastModified = $this->yellow->toolbox->getFileModified($fileName);
+                        if (preg_match("/update/i", $flags) && preg_match("/careful/i", $flags) && $lastModified!=$lastPublished) {
+                            $data[$software] = $dataCurrent[$software];
+                            if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::getSoftwareModified detected file:$fileName<br/>\n";
+                        }
+                    }
+                }
+            }
+        }
+        return array($statusCode, $data);
+    }
+    
+    // Return software file
+    public function getSoftwareFile($url) {
+        $urlRequest = $url;
+        if (preg_match("#^https://github.com/(.+)/raw/(.+)$#", $url, $matches)) $urlRequest = "https://raw.githubusercontent.com/".$matches[1]."/".$matches[2];
+        $curlHandle = curl_init();
+        curl_setopt($curlHandle, CURLOPT_URL, $urlRequest);
+        curl_setopt($curlHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; DatenstromYellow/".YellowCore::VERSION."; SoftwareUpdater)");
+        curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
+        curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 30);
+        $rawData = curl_exec($curlHandle);
+        $statusCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
+        curl_close($curlHandle);
+        if ($statusCode==200) {
+            $fileData = $rawData;
+        } elseif ($statusCode==0) {
+            $statusCode = 500;
+            list($scheme, $address) = $this->yellow->lookup->getUrlInformation($url);
+            $this->yellow->page->error($statusCode, "Can't connect to server '$scheme://$address'!");
+        } else {
+            $statusCode = 500;
+            $this->yellow->page->error($statusCode, "Can't download file '$url'!");
+        }
+        if (defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::getSoftwareFile status:$statusCode url:$url<br/>\n";
+        return array($statusCode, $fileData);
+    }
+    
+    // Check if software pending
+    public function isSoftwarePending() {
+        $path = $this->yellow->config->get("pluginDir");
+        $foundPlugins = count($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false))>0;
+        $path = $this->yellow->config->get("themeDir");
+        $foundThemes = count($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false))>0;
+        return $foundPlugins || $foundThemes;
+    }
 
-	// Check if software exists
-	function isSoftwareExisting($software)
-	{
-		$data = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData());
-		return !is_null($data[$software]);
-	}
+    // Check if software exists
+    public function isSoftwareExisting($software) {
+        $data = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData());
+        return !is_null($data[$software]);
+    }
 }
-	
+    
 $yellow->plugins->register("update", "YellowUpdate", YellowUpdate::VERSION, 1);
-?>

+ 503 - 145
system/themes/assets/flatsite.css

@@ -2,184 +2,542 @@
 /* Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se */
 /* This file may be used and distributed under the terms of the public license. */
 
-html, body, div, form, pre, span, tr, th, td, img { margin:0; padding:0; border:0; vertical-align:baseline; }
+html, body, div, form, pre, span, tr, th, td, img {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    vertical-align: baseline;
+}
 @font-face {
-	font-family:'Open Sans';
-	font-style:normal;
-	font-weight:300;
-	src:url(opensans-light.woff) format('woff');
+    font-family: "Open Sans";
+    font-style: normal;
+    font-weight: 300;
+    src: url(opensans-light.woff) format("woff");
 }
 @font-face {
-	font-family:'Open Sans';
-	font-style:normal;
-	font-weight:400;
-	src:url(opensans-regular.woff) format('woff');
+    font-family: "Open Sans";
+    font-style: normal;
+    font-weight: 400;
+    src: url(opensans-regular.woff) format("woff");
 }
 @font-face {
-	font-family:'Open Sans';
-	font-style:normal;
-	font-weight:700;
-	src:url(opensans-bold.woff) format('woff');
+    font-family: "Open Sans";
+    font-style: normal;
+    font-weight: 700;
+    src: url(opensans-bold.woff) format("woff");
 }
 body {
-	margin:1em;
-	background-color:#fff; color:#717171;
-	font-family:'Open Sans',Helvetica,sans-serif;
-	font-size:1em;
-	font-weight:300;
-	line-height:1.5;
-}
-h1, h2, h3, h4, h5, h6 { color:#111; font-weight:normal; }
-h1 { font-size:2.0em; }
-hr { height:1px; background:#ddd; border:0; }
-strong { font-weight:bold; }
-code { font-size:1.1em; }
-a { color:#07d; text-decoration:none; }
-a:hover { color:#07d; text-decoration:underline; }
-.content h1:first-child, .content>*:first-child { margin-top:0; }
-.content h1 a { color:#111; }
-.content h1 a:hover { color:#111; text-decoration:none; }
-.content img { max-width:100%; height:auto; }
-.content form { margin:1em 0; }
-.content table { border-spacing:0; border-collapse:collapse; }
-.content th { text-align:left; padding:0.3em; }
-.content td { text-align:left; padding:0.3em; border-top:1px solid #ddd; border-bottom:1px solid #ddd; }
-.content blockquote { margin-left:0; padding-left:1em; border-left:.5em solid #0a0; }
-.content blockquote blockquote { margin-left:-1.5em; border-left:.5em solid #fb0; }
-.content blockquote blockquote blockquote { border-color:#d00; }
-.content code, pre { font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace; font-size:90%; }
-.content code { padding:0.15em 0.4em; margin:0; background-color:#f7f7f7; border-radius:3px; }
-.content pre>code { padding:0; margin:0; white-space:pre; background:transparent; border:0; font-size:inherit; }
-.content pre { padding:1em; overflow:auto; line-height:1.45; background-color:#f7f7f7; border-radius:3px; }
-.content .flexible { position:relative; padding-top:0; padding-bottom:56.25%; }
-.content .flexible iframe { position:absolute; top:0; left:0; width:100%; height:100%; }
-.content .stretchable ul { margin:0 -0.5em; padding:0; list-style:none; text-align:center; }
-.content .stretchable li { margin:0; padding:1em 0; display:inline-block; text-align:center; vertical-align:top; }
-.content .stretchable a { color:#717171; text-decoration:none; }
-.content .task-list-item { list-style-type:none; }
-.content .task-list-item input { margin:0 0.2em 0.25em -1.75em; vertical-align:middle; }
-.content .toc { margin:0; padding:0; list-style:none; }
-.content .entry-links .previous { margin-right:1em; }
-.content .pagination .previous { margin-right:1em; }
-.content .pagination { margin:1em 0; }
-.content .left { float:left; margin:0 1em 0 0; }
-.content .center { display:block; margin:0 auto; }
-.content .right { float:right; margin:0 0 0 1em; }
-.content .rounded { border-radius:4px; }
+    margin: 1em;
+    background-color: #fff;
+    color: #717171;
+    font-family: "Open Sans", Helvetica, sans-serif;
+    font-size: 1em;
+    font-weight: 300;
+    line-height: 1.5;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+    color: #111;
+    font-weight: normal;
+}
+h1 {
+    font-size: 2em;
+}
+hr {
+    height: 1px;
+    background: #ddd;
+    border: 0;
+}
+strong {
+    font-weight: bold;
+}
+code {
+    font-size: 1.1em;
+}
+a {
+    color: #07d;
+    text-decoration: none;
+}
+a:hover {
+    color: #07d;
+    text-decoration: underline;
+}
+
+/* Content */
+
+.content h1:first-child,
+.content > *:first-child {
+    margin-top: 0;
+}
+.content h1 a {
+    color: #111;
+}
+.content h1 a:hover {
+    color: #111;
+    text-decoration: none;
+}
+.content img {
+    max-width: 100%;
+    height: auto;
+}
+.content form {
+    margin: 1em 0;
+}
+.content table {
+    border-spacing: 0;
+    border-collapse: collapse;
+}
+.content th {
+    text-align: left;
+    padding: 0.3em;
+}
+.content td {
+    text-align: left;
+    padding: 0.3em;
+    border-top: 1px solid #ddd;
+    border-bottom: 1px solid #ddd;
+}
+.content blockquote {
+    margin-left: 0;
+    padding-left: 1em;
+    border-left: 0.5em solid #0a0;
+}
+.content blockquote blockquote {
+    margin-left: -1.5em;
+    border-left: 0.5em solid #fb0;
+}
+.content blockquote blockquote blockquote {
+    border-color: #d00;
+}
+.content code,
+.content pre {
+    font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+    font-size: 90%;
+}
+.content code {
+    padding: 0.15em 0.4em;
+    margin: 0;
+    background-color: #f7f7f7;
+    border-radius: 3px;
+}
+.content pre > code {
+    padding: 0;
+    margin: 0;
+    white-space: pre;
+    background: transparent;
+    border: 0;
+    font-size: inherit;
+}
+.content pre {
+    padding: 1em;
+    overflow: auto;
+    line-height: 1.45;
+    background-color: #f7f7f7;
+    border-radius: 3px;
+}
+.content .flexible {
+    position: relative;
+    padding-top: 0;
+    padding-bottom: 56.25%;
+}
+.content .flexible iframe {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+}
+.content .stretchable ul {
+    margin: 0 -0.5em;
+    padding: 0;
+    list-style: none;
+    text-align: center;
+}
+.content .stretchable li {
+    margin: 0;
+    padding: 1em 0;
+    display: inline-block;
+    text-align: center;
+    vertical-align: top;
+}
+.content .stretchable a {
+    color: #717171;
+    text-decoration: none;
+}
+.content .task-list-item {
+    list-style-type: none;
+}
+.content .task-list-item input {
+    margin: 0 0.2em 0.25em -1.75em;
+    vertical-align: middle;
+}
+.content .toc {
+    margin: 0;
+    padding: 0;
+    list-style: none;
+}
+.content .entry-links .previous {
+    margin-right: 1em;
+}
+.content .pagination .previous {
+    margin-right: 1em;
+}
+.content .pagination {
+    margin: 1em 0;
+}
+.content .left {
+    float: left;
+    margin: 0 1em 0 0;
+}
+.content .center {
+    display: block;
+    margin: 0 auto;
+}
+.content .right {
+    float: right;
+    margin: 0 0 0 1em;
+}
+.content .rounded {
+    border-radius: 4px;
+}
 
 /* Header */
 
-.header .sitename { display:block; float:left; }
-.header .sitename h1 { margin:0; }
-.header .sitename h1 a { color:#111; text-decoration:none; }
-.header .sitename h2 { margin-top:0; color:#717171; font-size:1.0em; font-weight:300; }
+.header .sitename {
+    display: block;
+    float: left;
+}
+.header .sitename h1 {
+    margin: 0;
+}
+.header .sitename h1 a {
+    color: #111;
+    text-decoration: none;
+}
+.header .sitename h2 {
+    margin-top: 0;
+    color: #717171;
+    font-size: 1em;
+    font-weight: 300;
+}
 
 /* Navigation */
 
-.navigation { display:block; float:right; }
-.navigation { margin-top:0.9em; margin-bottom:0.9em; line-height:2; }
-.navigation a { padding:0 0.3em; }
-.navigation ul { margin:0 -0.3em; padding:0; list-style:none; }
-.navigation li { display:inline; }
-.navigation-tree { display:block; float:right; }
-.navigation-tree { margin-top:0.9em; margin-bottom:0.9em; line-height:2; }
-.navigation-tree a { padding:0 0.3em; }
-.navigation-tree ul { margin:0 -0.3em; padding:0; list-style:none; }
-.navigation-tree li { display:inline; }
-.navigation-tree ul li { display:inline-block; position:relative; cursor:pointer; margin:0; }
-.navigation-tree ul li ul { padding:0.3em; position:absolute; width:13em; background:#fff; z-index:100; display:none; }
-.navigation-tree ul li ul { border:1px solid #bbb; border-radius:4px; box-shadow:2px 4px 10px rgba(0, 0, 0, 0.2); }
-.navigation-tree ul li ul li { display:block; }
-.navigation-tree>ul>li:hover>ul { display:block; }
-.navigation-banner { clear:both; }
-.navigation-search { padding-bottom:1em; }
-.navigation-search .search-form { position:relative; }
-.navigation-search .search-text { font-family:inherit; font-size:inherit; font-weight:inherit; }
-.navigation-search .search-text { padding:0.5em; border:1px solid #bbb; border-radius:4px; width:100%; box-sizing:border-box; }
-.navigation-search .search-text { background-color:#fff; background-image:linear-gradient(to bottom, #fff, #fff); }
-.navigation-search .search-button { position:absolute; top:0; right:0; }
-.navigation-search .search-button { font-family:inherit; font-size:inherit; font-weight:inherit; }
-.navigation-search .search-button { margin:5px; padding:0.3em; border:none; background-color:transparent; }
+.navigation {
+    display: block;
+    float: right;
+}
+.navigation {
+    margin-top: 0.9em;
+    margin-bottom: 0.9em;
+    line-height: 2;
+}
+.navigation a {
+    padding: 0 0.3em;
+}
+.navigation ul {
+    margin: 0 -0.3em;
+    padding: 0;
+    list-style: none;
+}
+.navigation li {
+    display: inline;
+}
+.navigation-tree {
+    display: block;
+    float: right;
+}
+.navigation-tree {
+    margin-top: 0.9em;
+    margin-bottom: 0.9em;
+    line-height: 2;
+}
+.navigation-tree a {
+    padding: 0 0.3em;
+}
+.navigation-tree ul {
+    margin: 0 -0.3em;
+    padding: 0;
+    list-style: none;
+}
+.navigation-tree li {
+    display: inline;
+}
+.navigation-tree ul li {
+    display: inline-block;
+    position: relative;
+    cursor: pointer;
+    margin: 0;
+}
+.navigation-tree ul li ul {
+    padding: 0.3em;
+    position: absolute;
+    width: 13em;
+    background: #fff;
+    z-index: 100;
+    display: none;
+}
+.navigation-tree ul li ul {
+    border: 1px solid #bbb;
+    border-radius: 4px;
+    box-shadow: 2px 4px 10px rgba(0, 0, 0, 0.2);
+}
+.navigation-tree ul li ul li {
+    display: block;
+}
+.navigation-tree > ul > li:hover > ul {
+    display: block;
+}
+.navigation-banner {
+    clear: both;
+}
+.navigation-search {
+    padding-bottom: 1em;
+}
+.navigation-search .search-form {
+    position: relative;
+}
+.navigation-search .search-text {
+    font-family: inherit;
+    font-size: inherit;
+    font-weight: inherit;
+}
+.navigation-search .search-text {
+    padding: 0.5em;
+    border: 1px solid #bbb;
+    border-radius: 4px;
+    width: 100%;
+    box-sizing: border-box;
+}
+.navigation-search .search-text {
+    background-color: #fff;
+    background-image: linear-gradient(to bottom, #fff, #fff);
+}
+.navigation-search .search-button {
+    position: absolute;
+    top: 0;
+    right: 0;
+}
+.navigation-search .search-button {
+    font-family: inherit;
+    font-size: inherit;
+    font-weight: inherit;
+}
+.navigation-search .search-button {
+    margin: 5px;
+    padding: 0.3em;
+    border: none;
+    background-color: transparent;
+}
 
 /* Footer */
 
-.footer { margin-top:2em; }
-.footer .siteinfo a { color:#07d; }
-.footer .siteinfo a:hover { color:#07d; text-decoration:underline; }
-.footer .siteinfo a.language img { vertical-align:middle; margin-top:-5px; margin-right:0.75em; }
-.footer .siteinfo-left { float:left; }
-.footer .siteinfo-right { float:right; }
-.footer .siteinfo-banner { clear:both; }
+.footer {
+    margin-top: 2em;
+}
+.footer .siteinfo a {
+    color: #07d;
+}
+.footer .siteinfo a:hover {
+    color: #07d;
+    text-decoration: underline;
+}
+.footer .siteinfo a.language img {
+    vertical-align: middle;
+    margin-top: -5px;
+    margin-right: 0.75em;
+}
+.footer .siteinfo-left {
+    float: left;
+}
+.footer .siteinfo-right {
+    float: right;
+}
+.footer .siteinfo-banner {
+    clear: both;
+}
 
 /* Sidebar */
 
-.with-sidebar .main { margin-right:15em; }
-.with-sidebar .sidebar { float:right; width:13em; margin-top:3.2em; padding:2px; overflow:hidden; text-align:right; }
-.with-sidebar .sidebar ul { padding:0; list-style:none; }
-.with-sidebar .sidebar .search-form input { width:100%; box-sizing:border-box; }
-.with-sidebar .content:after { content:""; display:table; clear:both; }
+.with-sidebar .main {
+    margin-right: 15em;
+}
+.with-sidebar .sidebar {
+    float: right;
+    width: 13em;
+    margin-top: 3.2em;
+    padding: 2px;
+    overflow: hidden;
+    text-align: right;
+}
+.with-sidebar .sidebar ul {
+    padding: 0;
+    list-style: none;
+}
+.with-sidebar .sidebar .search-form input {
+    width: 100%;
+    box-sizing: border-box;
+}
+.with-sidebar .content:after {
+    content: "";
+    display: table;
+    clear: both;
+}
 
 /* Forms and buttons */
 
 .form-control {
-	margin:0; padding:2px 4px;
-	display:inline-block; min-width:7em;
-	background-color:#fff; color:#555;
-	background-image:linear-gradient(to bottom, #fff, #fff);
-	border:1px solid #bbb;
-	border-radius:4px;
-	font-size:0.9em; font-family:inherit; font-weight:normal; line-height:normal;
+    margin: 0;
+    padding: 2px 4px;
+    display: inline-block;
+    min-width: 7em;
+    background-color: #fff;
+    color: #555;
+    background-image: linear-gradient(to bottom, #fff, #fff);
+    border: 1px solid #bbb;
+    border-radius: 4px;
+    font-size: 0.9em;
+    font-family: inherit;
+    font-weight: normal;
+    line-height: normal;
 }
 .btn {
-	margin:0; padding:4px 22px;
-	display:inline-block; min-width:7em;
-	background-color:#eaeaea; color:#333333;
-	background-image:linear-gradient(to bottom, #f8f8f8, #e1e1e1);
-	border:1px solid #bbb;
-	border-color:#c1c1c1 #c1c1c1 #aaaaaa;
-	border-radius:4px;
-	outline-offset:-2px;
-	font-size:0.9em; font-family:inherit; font-weight:normal; line-height:1;
-	text-align:center; text-decoration:none;
-	box-sizing:border-box;
-}
-.btn:hover, .btn:focus, .btn:active {
-	color:#333333;
-	background-image:none; 
-	text-decoration:none;
-}
-.btn:active { box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.1); }
+    margin: 0;
+    padding: 4px 22px;
+    display: inline-block;
+    min-width: 7em;
+    background-color: #eaeaea;
+    color: #333333;
+    background-image: linear-gradient(to bottom, #f8f8f8, #e1e1e1);
+    border: 1px solid #bbb;
+    border-color: #c1c1c1 #c1c1c1 #aaaaaa;
+    border-radius: 4px;
+    outline-offset: -2px;
+    font-size: 0.9em;
+    font-family: inherit;
+    font-weight: normal;
+    line-height: 1;
+    text-align: center;
+    text-decoration: none;
+    box-sizing: border-box;
+}
+.btn:hover,
+.btn:focus,
+.btn:active {
+    color: #333333;
+    background-image: none;
+    text-decoration: none;
+}
+.btn:active {
+    box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
+}
 
 /* Misc */
 
-.template-default .content img.screenshot { margin:0 -0.5em; }
-.template-language .content div.language { font-size:1.2em; text-align:left; width:9em; margin:0 auto; }
-.template-language .content div.language p { margin:1.5em 0em; }
-.template-language .content div.language img { vertical-align:middle; margin-top:-5px; margin-right:1.5em; }
-.hljs-meta, .hljs-keyword, .hljs-literal { color:#b0b; }
-.hljs-attr, .hljs-attribute, .hljs-selector-id, .hljs-selector-class, .hljs-selector-pseudo { color:#b0b; }
-.hljs-type, .hljs-built_in, .hljs-builtin-name, .hljs-params { color:#b0b; }
-.hljs-string { color:#717171; }
-.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-number { color:#717171; }
+.template-default .content img.screenshot {
+    margin: 0 -0.5em;
+}
+.template-language .content div.language {
+    font-size: 1.2em;
+    text-align: left;
+    width: 9em;
+    margin: 0 auto;
+}
+.template-language .content div.language p {
+    margin: 1.5em 0em;
+}
+.template-language .content div.language img {
+    vertical-align: middle;
+    margin-top: -5px;
+    margin-right: 1.5em;
+}
+.hljs-meta,
+.hljs-keyword,
+.hljs-literal {
+    color: #b0b;
+}
+.hljs-attr,
+.hljs-attribute,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-selector-pseudo {
+    color: #b0b;
+}
+.hljs-type,
+.hljs-built_in,
+.hljs-builtin-name,
+.hljs-params {
+    color: #b0b;
+}
+.hljs-string {
+    color: #717171;
+}
+.hljs-symbol,
+.hljs-bullet,
+.hljs-link,
+.hljs-number {
+    color: #717171;
+}
 
 /* Responsive and print */
 
-.page { margin:0 auto; max-width:1000px; }
+.page {
+    margin: 0 auto;
+    max-width: 1000px;
+}
 
-@media screen and (min-width:62em) {
-	body { width:60em; margin:1em auto; }
-	.page{ margin:0; max-width:none; }
+@media screen and (min-width: 62em) {
+    body {
+        width: 60em;
+        margin: 1em auto;
+    }
+    .page {
+        margin: 0;
+        max-width: none;
+    }
 }
-@media screen and (max-width:32em) {
-	body { margin:0.5em; font-size:0.9em; }
-	.header .sitename h1, .content h1, .content h2 { font-size:1.3em; }
-	.header .sitename h1, .header .sitename h2, .footer, .page { margin:0; padding:0; }
-	.header .sitename, .navigation, .navigation-tree { float:none; }
-	.navigation { margin-top:0.5em; margin-bottom:0.5em; }
-	.navigation-search { padding-bottom:1em; }
-	.with-sidebar .main { margin-right:0; }
-	.with-sidebar .sidebar { display:none; }
+@media screen and (max-width: 32em) {
+    body {
+        margin: 0.5em;
+        font-size: 0.9em;
+    }
+    .header .sitename h1,
+    .content h1,
+    .content h2 {
+        font-size: 1.3em;
+    }
+    .header .sitename h1,
+    .header .sitename h2,
+    .footer,
+    .page {
+        margin: 0;
+        padding: 0;
+    }
+    .header .sitename,
+    .navigation,
+    .navigation-tree {
+        float: none;
+    }
+    .navigation {
+        margin-top: 0.5em;
+        margin-bottom: 0.5em;
+    }
+    .navigation-search {
+        padding-bottom: 1em;
+    }
+    .with-sidebar .main {
+        margin-right: 0;
+    }
+    .with-sidebar .sidebar {
+        display: none;
+    }
 }
 @media print {
-	.page { border:none !important; }
+    .page {
+        border: none !important;
+    }
 }

+ 2 - 4
system/themes/assets/flatsite.php

@@ -3,10 +3,8 @@
 // Copyright (c) 2013-2018 Datenstrom, https://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
-class YellowThemeFlatsite
-{
-	const VERSION = "0.7.6";	
+class YellowThemeFlatsite {
+    const VERSION = "0.7.6";
 }
 
 $yellow->themes->register("flatsite", "YellowThemeFlatsite", YellowThemeFlatsite::VERSION);
-?>