浏览代码

System update (better software update)

markseu 8 年之前
父节点
当前提交
fabd68c66a

+ 11 - 12
system/config/config.ini

@@ -4,17 +4,20 @@ Sitename: Yellow
 Author: Yellow
 Email: webmaster
 Language: en
+Timezone: UTC
 Theme: flatsite
 
-# ServerScheme: http
-# ServerName: your.domain.name
-# ServerBase:
-# ServerTime: UTC
-
+StaticUrl:
+StaticDefaultFile: index.html
+StaticErrorFile: 404.html
+StaticDir: cache/
+MediaLocation: /media/
 ImageLocation: /media/images/
 PluginLocation: /media/plugins/
 ThemeLocation: /media/themes/
 AssetLocation: /media/themes/assets/
+MediaDir: media/
+ImageDir: media/images/
 SystemDir: system/
 ConfigDir: system/config/
 PluginDir: system/plugins/
@@ -23,15 +26,10 @@ AssetDir: system/themes/assets/
 SnippetDir: system/themes/snippets/
 TemplateDir: system/themes/templates/
 TrashDir: system/trash/
-MediaDir: media/
-ImageDir: media/images/
-StaticDir: cache/
-StaticDefaultFile: index.html
-StaticErrorFile: 404.html
-ContentPagination: page
 ContentDir: content/
 ContentRootDir: default/
 ContentHomeDir: home/
+ContentPagination: page
 ContentDefaultFile: page.txt
 ContentExtension: .txt
 ConfigExtension: .ini
@@ -50,11 +48,12 @@ Parser: markdown
 ParserSafeMode: 0
 MultiLanguageMode: 0
 InstallationMode: 1
+StartupUpdateNotification: none
 UpdatePluginsUrl: https://github.com/datenstrom/yellow-plugins
 UpdateThemesUrl: https://github.com/datenstrom/yellow-themes
 UpdateInformationFile: update.ini
 UpdateVersionFile: version.ini
-UpdateNotification: none
+UpdateResourceFile: resource.ini
 WebinterfaceLocation: /edit/
 WebinterfaceNewFile: page-new-(.*).txt
 WebinterfaceMetaFilePrefix: published

+ 32 - 34
system/plugins/commandline.php

@@ -5,7 +5,7 @@
 // Command line plugin
 class YellowCommandline
 {
-	const VERSION = "0.6.17";
+	const VERSION = "0.6.18";
 	var $yellow;					//access to API
 	var $files;						//number of files
 	var $errors;					//number of errors
@@ -65,8 +65,7 @@ class YellowCommandline
 				$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 ServerScheme, ServerName, ServerBase, ServerTime in file '$fileName'!\n";
-				echo "ERROR building files: Open your website in a web browser, if you want to see your server settings!\n";
+				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' : '');
@@ -132,7 +131,9 @@ class YellowCommandline
 		if(!is_readable($this->yellow->page->fileName))
 		{
 			ob_start();
-			$statusCode = $this->requestStaticFile($location);
+			$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();
@@ -165,9 +166,9 @@ class YellowCommandline
 				$this->yellow->page->set("pageError", "Can't write file '$fileName'!");
 			}
 		}
-		if($statusCode==200 && $analyse) $this->analyseStaticFile($fileData);
-		if($statusCode==404 && $error) $statusCode = 200;
+		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)
 		{
@@ -179,35 +180,39 @@ class YellowCommandline
 	}
 	
 	// Request static file
-	function requestStaticFile($location)
+	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"] = $this->yellow->config->get("serverName");
-		$_SERVER["REQUEST_URI"] = $this->yellow->config->get("serverBase").$location;
-		$_SERVER["SCRIPT_NAME"] = $this->yellow->config->get("serverBase")."/yellow.php";
+		$_SERVER["SERVER_NAME"] = $serverName;
+		$_SERVER["SERVER_PORT"] = $serverPort;
+		$_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($rawData)
+	function analyseStaticFile($scheme, $address, $base, $rawData)
 	{
-		$serverName = $this->yellow->config->get("serverName");
-		$serverBase = $this->yellow->config->get("serverBase");
 		$pagination = $this->yellow->config->get("contentPagination");
 		preg_match_all("/<(.*?)href=\"([^\"]+)\"(.*?)>/i", $rawData, $matches);
 		foreach($matches[2] as $match)
 		{
-			if(preg_match("/^(.*?)#(.*)$/", $match, $tokens)) $match = $tokens[1];
-			if(preg_match("/^\w+:\/+(.*?)(\/.*)$/", $match, $tokens))
+			$location = rawurldecode($match);
+			if(preg_match("/^(.*?)#(.*)$/", $location, $tokens)) $location = $tokens[1];
+			if(preg_match("/^(\w+):\/\/([^\/]+)(.*)$/", $location, $tokens))
 			{
-				if($tokens[1]!=$serverName) continue;
-				$match = $tokens[2];
+				if($tokens[1]!=$scheme) continue;
+				if($tokens[2]!=$address) continue;
+				$location = $tokens[3];
 			}
-			if(!$this->yellow->toolbox->isLocationArgs($match)) continue;
-			if(substru($match, 0, strlenu($serverBase))!=$serverBase) continue;
-			$location = rawurldecode(substru($match, strlenu($serverBase)));
+			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, '/').'/';
@@ -308,9 +313,8 @@ class YellowCommandline
 	// Show software version and updates
 	function versionCommand($args)
 	{
-		$statusCode = 0;
-		$serverSoftware = $this->yellow->toolbox->getServerSoftware();
-		echo "Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverSoftware\n";
+		$serverVersion = $this->yellow->toolbox->getServerVersion();
+		echo "Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion\n";
 		list($command) = $args;
 		list($statusCode, $dataCurrent) = $this->getSoftwareVersion();
 		list($statusCode, $dataLatest) = $this->getSoftwareVersion(true);
@@ -321,22 +325,17 @@ class YellowCommandline
 				echo "$key $value\n";
 			} else {
 				echo "$key $dataLatest[$key] - Update available\n";
-				++$updates;
 			}
 		}
 		if($statusCode!=200) echo "ERROR checking updates: ".$this->yellow->page->get("pageError")."\n";
-		if($updates) echo "Yellow $command: $updates update".($updates==1 ? "":"s")." available\n";
 		return $statusCode;
 	}
 	
 	// Check static configuration
 	function checkStaticConfig()
 	{
-		$serverScheme = $this->yellow->config->get("serverScheme");
-		$serverName = $this->yellow->config->get("serverName");
-		$serverBase = $this->yellow->config->get("serverBase");
-		return !empty($serverScheme) && !empty($serverName) &&
-			$this->yellow->lookup->isValidLocation($serverBase) && $serverBase!="/";
+		$staticUrl = $this->yellow->config->get("staticUrl");
+		return !empty($staticUrl);
 	}
 	
 	// Check static directory
@@ -380,10 +379,9 @@ class YellowCommandline
 	function getContentLocations()
 	{
 		$locations = array();
-		$serverScheme = $this->yellow->config->get("serverScheme");
-		$serverName = $this->yellow->config->get("serverName");
-		$serverBase = $this->yellow->config->get("serverBase");
-		$this->yellow->page->setRequestInformation($serverScheme, $serverName, $serverBase, "", "");
+		$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")

+ 308 - 206
system/plugins/core.php

@@ -5,7 +5,7 @@
 // Yellow core
 class YellowCore
 {
-	const VERSION = "0.6.7";
+	const VERSION = "0.6.8";
 	var $page;				//current page
 	var $pages;				//pages from file system
 	var $files;				//files from file system
@@ -31,15 +31,20 @@ class YellowCore
 		$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("serverScheme", $this->toolbox->getServerScheme());
-		$this->config->setDefault("serverName", $this->toolbox->getServerName());
-		$this->config->setDefault("serverBase", $this->toolbox->getServerBase());
-		$this->config->setDefault("serverTime", $this->toolbox->getServerTime());
+		$this->config->setDefault("serverUrl", "");
+		$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("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("imageDir", "media/images/");
 		$this->config->setDefault("systemDir", "system/");
 		$this->config->setDefault("configDir", "system/config/");
 		$this->config->setDefault("pluginDir", "system/plugins/");
@@ -48,15 +53,10 @@ class YellowCore
 		$this->config->setDefault("snippetDir", "system/themes/snippets/");
 		$this->config->setDefault("templateDir", "system/themes/templates/");
 		$this->config->setDefault("trashDir", "system/trash/");
-		$this->config->setDefault("mediaDir", "media/");
-		$this->config->setDefault("imageDir", "media/images/");
-		$this->config->setDefault("staticDir", "cache/");
-		$this->config->setDefault("staticDefaultFile", "index.html");
-		$this->config->setDefault("staticErrorFile", "404.html");
-		$this->config->setDefault("contentPagination", "page");
 		$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");
@@ -76,6 +76,12 @@ class YellowCore
 		$this->config->setDefault("parserSafeMode", "0");
 		$this->config->setDefault("multiLanguageMode", "0");
 		$this->config->setDefault("installationMode", "0");
+		$this->config->setDefault("startupUpdateNotification", "none");
+	}
+	
+	function __destruct()
+	{
+		$this->shutdown();
 	}
 	
 	// Handle initialisation
@@ -83,19 +89,35 @@ class YellowCore
 	{
 		if(defined("DEBUG") && DEBUG>=2)
 		{
-			$serverSoftware = $this->toolbox->getServerSoftware();
-			echo "Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverSoftware<br>\n";
+			$serverVersion = $this->toolbox->getServerVersion();
+			echo "Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverVersion<br/>\n";
 		}
-		$this->pages->requestHandler = "none";
 		$this->config->load($this->config->get("configDir").$this->config->get("configFile"));
 		$this->text->load($this->config->get("pluginDir").$this->config->get("textFile"));
 		$this->lookup->load();
-		$this->plugins->load();
 		$this->themes->load();
+		$this->plugins->load();
+		$this->startup();
+	}
+	
+	// Handle startup
+	function startup()
+	{
+		$tokens = explode(',', $this->config->get("startupUpdateNotification"));
 		foreach($this->plugins->plugins as $key=>$value)
 		{
-			if(method_exists($value["obj"], "onUpdate")) $value["obj"]->onUpdate("");
+			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("startupUpdateNotification")!="none")
+		{
+			$fileNameConfig = $this->config->get("configDir").$this->config->get("configFile");
+			$this->config->update($fileNameConfig, array("startupUpdateNotification" => "none"));
+		}
+		if(defined("DEBUG") && DEBUG>=2) echo "YellowCore::startup<br/>\n";
 	}
 	
 	// Handle request
@@ -105,21 +127,21 @@ class YellowCore
 		$statusCode = 0;
 		$this->toolbox->timerStart($time);
 		$this->toolbox->normaliseRequest();
-		list($serverScheme, $serverName, $base, $location, $fileName) = $this->getRequestInformation();
-		$this->page->setRequestInformation($serverScheme, $serverName, $base, $location, $fileName);
+		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->pages->requestHandler = $key;
-				$statusCode = $value["obj"]->onRequest($serverScheme, $serverName, $base, $location, $fileName);
+				$this->lookup->requestHandler = $key;
+				$statusCode = $value["obj"]->onRequest($scheme, $address, $base, $location, $fileName);
 				if($statusCode!=0) break;
 			}
 		}
 		if($statusCode==0)
 		{
-			$this->pages->requestHandler = "core";
-			$statusCode = $this->processRequest($serverScheme, $serverName, $base, $location, $fileName, true);
+			$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);
@@ -127,14 +149,13 @@ class YellowCore
 		if(defined("DEBUG") && DEBUG>=1)
 		{
 			$handler = $this->getRequestHandler();
-			echo "YellowCore::request status:$statusCode location:$location handler:$handler<br/>\n";
-			echo "YellowCore::request time:$time ms<br/>\n";
+			echo "YellowCore::request status:$statusCode handler:$handler time:$time ms<br/>\n";
 		}
 		return $statusCode;
 	}
 	
 	// Process request
-	function processRequest($serverScheme, $serverName, $base, $location, $fileName, $cacheable)
+	function processRequest($scheme, $address, $base, $location, $fileName, $cacheable)
 	{
 		$statusCode = 0;
 		if(is_readable($fileName))
@@ -143,7 +164,7 @@ class YellowCore
 			{
 				$statusCode = 303;
 				$location = $location.$this->getRequestLocationArgsClean();
-				$location = $this->lookup->normaliseUrl($serverScheme, $serverName, $base, $location);
+				$location = $this->lookup->normaliseUrl($scheme, $address, $base, $location);
 				$this->sendStatus($statusCode, $location);
 			}
 		} else {
@@ -151,16 +172,16 @@ class YellowCore
 			{
 				$statusCode = 301;
 				$location = $this->lookup->isFileLocation($location) ? "$location/" : "/".$this->getRequestLanguage()."/";
-				$location = $this->lookup->normaliseUrl($serverScheme, $serverName, $base, $location);
+				$location = $this->lookup->normaliseUrl($scheme, $address, $base, $location);
 				$this->sendStatus($statusCode, $location);
 			}
 		}
 		if($statusCode==0)
 		{
-			$fileName = $this->lookup->findFileStatic($location, $fileName, $cacheable);
+			$fileName = $this->lookup->findFileStatic($location, $fileName, $cacheable && !$this->isCommandLine());
 			if($this->lookup->isContentFile($fileName) || !is_readable($fileName))
 			{
-				$fileName = $this->readPage($serverScheme, $serverName, $base, $location, $fileName, $cacheable,
+				$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 {
@@ -175,7 +196,7 @@ class YellowCore
 	function processRequestError()
 	{
 		ob_clean();
-		$fileName = $this->readPage($this->page->serverScheme, $this->page->serverName, $this->page->base,
+		$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();
@@ -184,7 +205,7 @@ class YellowCore
 	}
 	
 	// Read page
-	function readPage($serverScheme, $serverName, $base, $location, $fileName, $cacheable, $statusCode, $pageError)
+	function readPage($scheme, $address, $base, $location, $fileName, $cacheable, $statusCode, $pageError)
 	{
 		if($statusCode>=400)
 		{
@@ -193,7 +214,7 @@ class YellowCore
 			$cacheable = false;
 		}
 		$this->page = new YellowPage($this);
-		$this->page->setRequestInformation($serverScheme, $serverName, $base, $location, $fileName);
+		$this->page->setRequestInformation($scheme, $address, $base, $location, $fileName);
 		$this->page->parseData($this->toolbox->readFile($fileName), $cacheable, $statusCode, $pageError);
 		$this->text->setLanguage($this->page->get("language"));
 		$this->page->parseContent();
@@ -265,52 +286,71 @@ class YellowCore
 		{
 			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) echo "YellowCore::command time:$time ms<br/>\n";
+		if(defined("DEBUG") && DEBUG>=1)
+		{
+			$handler = $this->getCommandHandler();
+			echo "YellowCore::command status:$statusCode handler:$handler time:$time ms<br/>\n";
+		}
 		return $statusCode;
 	}
 	
+	// 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();
+		}
+		if(defined("DEBUG") && DEBUG>=2) echo "YellowCore::shutdown<br/>\n";
+	}
+	
 	// Parse snippet
 	function snippet($name, $args = null)
 	{
-		$this->pages->snippetArgs = func_get_args();
+		$this->lookup->snippetArgs = func_get_args();
 		$this->page->parseSnippet($name);
 	}
 	
+	// Return snippet arguments
+	function getSnippetArgs()
+	{
+		return $this->lookup->snippetArgs;
+	}
+	
 	// Return request information
-	function getRequestInformation($serverScheme = "", $serverName = "", $base = "")
+	function getRequestInformation($scheme = "", $address = "", $base = "")
 	{
-		$serverScheme = empty($serverScheme) ? $this->config->get("serverScheme") : $serverScheme;
-		$serverName = empty($serverName) ? $this->config->get("serverName") : $serverName;
-		$base = empty($base) ? $this->config->get("serverBase") : $base;
-		$location = $this->toolbox->getLocation();
-		$location = substru($location, strlenu($base));
-		if(preg_match("/\.(css|ico|js|jpg|png|svg|txt|woff)$/", $location))
+		if(empty($scheme) && empty($address) && empty($base))
 		{
-			$pluginLocationLength = strlenu($this->config->get("pluginLocation"));
-			$themeLocationLength = strlenu($this->config->get("themeLocation"));
-			if(substru($location, 0, $pluginLocationLength)==$this->config->get("pluginLocation")) {
-				$fileName = $this->config->get("pluginDir").substru($location, $pluginLocationLength);
-			} else if(substru($location, 0, $themeLocationLength)==$this->config->get("themeLocation")) {
-				$fileName = $this->config->get("themeDir").substru($location, $themeLocationLength);
-			} else if($location=="/".$this->config->get("robotsFile")) {
-				$fileName = $this->config->get("configDir").$this->config->get("robotsFile");
-			} else if($location=="/".$this->config->get("faviconFile")) {
-				$fileName = $this->config->get("assetDir").$this->config->get("siteicon").".png";
-			}
+			$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);
 		}
+		$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($serverScheme, $serverName, $base, $location, $fileName);
+		return array($scheme, $address, $base, $location, $fileName);
 	}
 	
 	// Return request location
@@ -328,13 +368,19 @@ class YellowCore
 	// Return request handler
 	function getRequestHandler()
 	{
-		return $this->pages->requestHandler;
+		return $this->lookup->requestHandler;
+	}
+
+	// Return command handler
+	function getCommandHandler()
+	{
+		return $this->lookup->commandHandler;
 	}
 	
-	// Return snippet arguments
-	function getSnippetArgs()
+	// Check if running at command line
+	function isCommandLine()
 	{
-		return $this->pages->snippetArgs;
+		return !empty($this->lookup->commandHandler);
 	}
 }
 	
@@ -342,8 +388,8 @@ class YellowCore
 class YellowPage
 {
 	var $yellow;				//access to API
-	var $serverScheme;			//server scheme
-	var $serverName;			//server name
+	var $scheme;				//server scheme
+	var $address;				//server address
 	var $base;					//base location
 	var $location;				//page location
 	var $fileName;				//content file name
@@ -374,10 +420,10 @@ class YellowPage
 	}
 
 	// Set request information
-	function setRequestInformation($serverScheme, $serverName, $base, $location, $fileName)
+	function setRequestInformation($scheme, $address, $base, $location, $fileName)
 	{
-		$this->serverScheme = $serverScheme;
-		$this->serverName = $serverName;
+		$this->scheme = $scheme;
+		$this->address = $address;
 		$this->base = $base;
 		$this->location = $location;
 		$this->fileName = $fileName;
@@ -420,7 +466,7 @@ class YellowPage
 			$this->set("language", $this->yellow->lookup->findLanguageFromFile($this->fileName,
 				$this->yellow->config->get("language")));
 			$this->set("theme", $this->yellow->lookup->findNameFromFile($this->fileName,
-				$this->yellow->config->get("themeDir"), $this->yellow->config->get("theme"), ".css"));
+				$this->yellow->config->get("assetDir"), $this->yellow->config->get("theme"), ".css"));
 			$this->set("template", $this->yellow->lookup->findNameFromFile($this->fileName,
 				$this->yellow->config->get("templateDir"), $this->yellow->config->get("template"), ".html"));
 			$this->set("modified", date("Y-m-d H:i:s", $this->yellow->toolbox->getFileModified($this->fileName)));
@@ -433,11 +479,11 @@ class YellowPage
 			if($this->get("status")=="hidden") $this->available = false;
 			$this->set("pageRead", $this->yellow->lookup->normaliseUrl(
 				$this->yellow->config->get("serverScheme"),
-				$this->yellow->config->get("serverName"),
+				$this->yellow->config->get("serverAddress"),
 				$this->yellow->config->get("serverBase"), $this->location));
 			$this->set("pageEdit", $this->yellow->lookup->normaliseUrl(
-				$this->yellow->config->get("webinterfaceServerScheme"),
-				$this->yellow->config->get("webinterfaceServerName"),
+				$this->yellow->config->get("serverScheme"),
+				$this->yellow->config->get("serverAddress"),
 				$this->yellow->config->get("serverBase"),
 				rtrim($this->yellow->config->get("webinterfaceLocation"), '/').$this->location));
 			$this->set("pageFile", $this->yellow->lookup->normaliseFile($this->fileName));
@@ -538,21 +584,12 @@ class YellowPage
 					$output = "<span class=\"".htmlspecialchars($name)."\">\n";
 					if($text=="version")
 					{
-						$serverSoftware = $this->yellow->toolbox->getServerSoftware();
-						$output .= "Yellow ".YellowCore::VERSION.", PHP ".PHP_VERSION.", $serverSoftware<br />\n";
-						foreach($this->yellow->plugins->getData() as $key=>$value)
+						$serverVersion = $this->yellow->toolbox->getServerVersion();
+						$output .= "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";
 						}
-						foreach($this->yellow->themes->getData() as $key=>$value)
-						{
-							$output .= htmlspecialchars("$key $value")."<br />\n";
-						}
-					} else {
-						foreach($this->yellow->config->getData($text) as $key=>$value)
-						{
-							$output .= htmlspecialchars(ucfirst($key).": ".$value)."<br />\n";
-						}
 					}
 					$output .= "</span>\n";
 					if($this->parserSafeMode) $this->error(500, "Yellow '$text' is not available in safe mode!");
@@ -597,12 +634,15 @@ class YellowPage
 		{
 			$this->error(500, "Parser '".$this->get("parser")."' does not exist!");
 		}
-		if($this->yellow->lookup->isNestedLocation($this->location, true)) $this->error(500, "Home folder may not contain subfolders!");
+		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()) $this->error(500, "Rewrite module not enabled on this server!");
 		if($this->yellow->getRequestHandler()=="core" && $this->isExisting("redirect") && $this->statusCode==200)
 		{
-			$location = $this->yellow->lookup->normaliseLocation($this->get("redirect"), $this->base, $this->location);
-			$location = $this->yellow->lookup->normaliseUrl($this->serverScheme, $this->serverName, "", $location);
+			$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)
@@ -759,10 +799,10 @@ class YellowPage
 		return $absoluteLocation ? $this->base.$this->location : $this->location;
 	}
 	
-	// Return page URL with server scheme and server name
+	// Return page URL
 	function getUrl()
 	{
-		return $this->yellow->lookup->normaliseUrl($this->serverScheme, $this->serverName, $this->base, $this->location);
+		return $this->yellow->lookup->normaliseUrl($this->scheme, $this->address, $this->base, $this->location);
 	}
 	
 	// Return page extra HTML data
@@ -779,10 +819,10 @@ class YellowPage
 		}
 		if($name=="header")
 		{
-			if(is_file($this->yellow->config->get("themeDir").$this->get("theme").".css"))
+			if(is_file($this->yellow->config->get("assetDir").$this->get("theme").".css"))
 			{
 				$location = $this->yellow->config->get("serverBase").
-					$this->yellow->config->get("themeLocation").$this->get("theme").".css";
+					$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"))
@@ -1252,8 +1292,6 @@ class YellowPages
 {
 	var $yellow;			//access to API
 	var $pages;				//scanned pages
-	var $requestHandler;	//request handler name
-	var $snippetArgs;		//requested snippet arguments
 	
 	function __construct($yellow)
 	{
@@ -1268,8 +1306,8 @@ class YellowPages
 		{
 			if(defined("DEBUG") && DEBUG>=2) echo "YellowPages::scanLocation location:$location<br/>\n";
 			$this->pages[$location] = array();
-			$serverScheme = $this->yellow->page->serverScheme;
-			$serverName = $this->yellow->page->serverName;
+			$scheme = $this->yellow->page->scheme;
+			$address = $this->yellow->page->address;
 			$base = $this->yellow->page->base;
 			if(empty($location))
 			{
@@ -1278,7 +1316,7 @@ class YellowPages
 				{
 					list($rootLocation, $fileName) = explode(' ', $rootLocation, 2);
 					$page = new YellowPage($this->yellow);
-					$page->setRequestInformation($serverScheme, $serverName, $base, $rootLocation, $fileName);
+					$page->setRequestInformation($scheme, $address, $base, $rootLocation, $fileName);
 					$page->parseData("", false, 0);
 					array_push($this->pages[$location], $page);
 				}
@@ -1287,7 +1325,7 @@ class YellowPages
 				foreach($fileNames as $fileName)
 				{
 					$page = new YellowPage($this->yellow);
-					$page->setRequestInformation($serverScheme, $serverName, $base,
+					$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;
@@ -1475,8 +1513,8 @@ class YellowFiles
 		{
 			if(defined("DEBUG") && DEBUG>=2) echo "YellowFiles::scanLocation location:$location<br/>\n";
 			$this->files[$location] = array();
-			$serverScheme = $this->yellow->page->serverScheme;
-			$serverName = $this->yellow->page->serverName;
+			$scheme = $this->yellow->page->scheme;
+			$address = $this->yellow->page->address;
 			$base = $this->yellow->config->get("serverBase");
 			if(empty($location))
 			{
@@ -1496,7 +1534,7 @@ class YellowFiles
 			foreach($fileNames as $fileName)
 			{
 				$file = new YellowPage($this->yellow);
-				$file->setRequestInformation($serverScheme, $serverName, $base, "/".$fileName, $fileName);
+				$file->setRequestInformation($scheme, $address, $base, "/".$fileName, $fileName);
 				$file->parseData(null, false, 0);
 				array_push($this->files[$location], $file);
 			}
@@ -1507,7 +1545,7 @@ class YellowFiles
 	// Return page with media file information, null if not found
 	function find($location, $absoluteLocation = false)
 	{
-		if($absoluteLocation) $location = substru($location, strlenu($this->yellow->page->base));
+		if($absoluteLocation) $location = substru($location, strlenu($this->yellow->config->get("serverBase")));
 		foreach($this->scanLocation($this->getParentLocation($location)) as $file)
 		{
 			if($file->location==$location)
@@ -1566,13 +1604,13 @@ class YellowFiles
 	// Return home location
 	function getHomeLocation($location)
 	{
-		return "/".$this->yellow->config->get("mediaDir");
+		return $this->yellow->config->get("mediaLocation");
 	}
 
 	// Return parent location
 	function getParentLocation($location)
 	{
-		$token = rtrim("/".$this->yellow->config->get("mediaDir"), '/');
+		$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];
@@ -1584,7 +1622,7 @@ class YellowFiles
 	// Return top-level location
 	function getParentTopLocation($location)
 	{
-		$token = rtrim("/".$this->yellow->config->get("mediaDir"), '/');
+		$token = rtrim($this->yellow->config->get("mediaLocation"), '/');
 		if(preg_match("#^($token.+?\/)#", $location, $matches)) $parentTopLocation = $matches[1];
 		if(empty($parentTopLocation)) $parentTopLocation = "$token/";
 		return $parentTopLocation;
@@ -1606,16 +1644,17 @@ class YellowPlugins
 	}
 	
 	// Load plugins
-	function load()
+	function load($path = "")
 	{
-		if(empty($this->yellow->pages->requestHandler))	//TODO: remove later, guard for old version
+		if(count($this->yellow->config->config)==0) //TODO: remove later, backwards compability for old Yellow version
 		{
 			$this->yellow->load();
 			return;
 		}
-		$path = $this->yellow->config->get("pluginDir");
+		$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);
@@ -1628,7 +1667,6 @@ class YellowPlugins
 		foreach($this->plugins as $key=>$value)
 		{
 			$this->plugins[$key]["obj"] = new $value["plugin"];
-			if(defined("DEBUG") && DEBUG>=3) echo "YellowPlugins::load $value[plugin]:$value[version]<br/>\n";
 			if(method_exists($this->plugins[$key]["obj"], "onLoad")) $this->plugins[$key]["obj"]->onLoad($yellow);
 		}
 	}
@@ -1657,7 +1695,11 @@ class YellowPlugins
 	{
 		$data = array();
 		$data["YellowCore"] = YellowCore::VERSION;
-		foreach($this->plugins as $key=>$value) $data[$value["plugin"]] = $value["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;
 	}
@@ -1690,29 +1732,45 @@ class YellowThemes
 	}
 	
 	// Load themes
-	function load()
+	function load($path = "")
 	{
-		$path = $this->yellow->config->get("themeDir");
+		$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)
 		{
-			$name = $this->yellow->lookup->normaliseName(basename($entry), true, true);
-			$theme = $version = "";
+			if(defined("DEBUG") && DEBUG>=3) echo "YellowThemes::load file:$entry<br/>\n";
 			$this->modified = max($this->modified, filemtime($entry));
-			$fileData = $this->yellow->toolbox->readFile($entry, 4096);
-			foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
-			{
-				preg_match("/^\/\*\s*(.*?)\s*:\s*(.*?)\s*\*\/$/", $line, $matches);
-				if(lcfirst($matches[1])=="theme" && !strempty($matches[2])) $theme = $matches[2];
-				if(lcfirst($matches[1])=="version" && !strempty($matches[2])) $version = $matches[2];
-				if(!empty($line) && $line[0]!='/') break;
-			}
-			if(!empty($theme) && !empty($version))
-			{
-				$this->themes[$name] = array();
-				$this->themes[$name]["theme"] = $theme;
-				$this->themes[$name]["version"] = $version;
-				if(defined("DEBUG") && DEBUG>=3) echo "YellowThemes::load $theme:$version<br/>\n";
-			}
+			$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;
 		}
 	}
 	
@@ -1720,7 +1778,11 @@ class YellowThemes
 	function getData()
 	{
 		$data = array();
-		foreach($this->themes as $key=>$value) $data[$value["theme"]] = $value["version"];
+		foreach($this->themes as $key=>$value)
+		{
+			if(empty($value["theme"]) || empty($value["version"])) continue;
+			$data[$value["theme"]] = $value["version"];
+		}
 		uksort($data, strnatcasecmp);
 		return $data;
 	}
@@ -2021,6 +2083,9 @@ class YellowText
 class YellowLookup
 {
 	var $yellow;		//access to API
+	var $requestHandler;	//request handler name
+	var $commandHandler;	//command handler name
+	var $snippetArgs;		//snippet arguments
 	
 	function __construct($yellow)
 	{
@@ -2033,7 +2098,7 @@ class YellowLookup
 		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("serverTime"));
+		date_default_timezone_set($this->yellow->config->get("timezone"));
 	}
 	
 	// Detect file system
@@ -2302,6 +2367,53 @@ class YellowLookup
 		return dirname($fileName)."/".$fileNamePrefix.$fileNameText;
 	}
 	
+	// Return file path for 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 for system location
+	function findFileFromSystem($location)
+	{
+		if(preg_match("/\.(css|ico|js|jpg|png|svg|txt|woff)$/", $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;
+	}
+	
+	// Return file path for 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 file path for new page
 	function findFilePageNew($fileName, $prefix = "")
 	{
@@ -2328,19 +2440,6 @@ class YellowLookup
 		return $path;
 	}
 
-	// Return file path for static file, if possible
-	function findFileStatic($location, $fileName, $cacheable)
-	{
-		if($cacheable && PHP_SAPI!="cli")
-		{
-			$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;
-	}
-	
 	// Normalise file/directory/other name
 	function normaliseName($text, $removePrefix = true, $removeExtension = false, $filterStrict = false)
 	{
@@ -2362,23 +2461,36 @@ class YellowLookup
 		return $fileName;
 	}
 	
+	// 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, $pageBase, $pageLocation, $staticLocation = "", $filterStrict = true)
+	function normaliseLocation($location, $pageLocation, $filterStrict = true)
 	{
 		if(!preg_match("/^\w+:/", trim(html_entity_decode($location, ENT_QUOTES, "UTF-8"))))
 		{
-			if(empty($staticLocation) || !preg_match("#^$staticLocation#", $location))
+			$pageBase = $this->yellow->page->base;
+			$mediaBase = $this->yellow->config->get("serverBase").$this->yellow->config->get("mediaLocation");
+			if(preg_match("/^\#/", $location))
 			{
-				if(preg_match("/^\#/", $location))
-				{
-					$location = $pageBase.$pageLocation.$location;
-				} else if(!preg_match("/^\//", $location)) {
-					$location = $this->getDirectoryLocation($pageBase.$pageLocation).$location;
-				} else if(!preg_match("#^$pageBase#", $location)) {
-					$location = $pageBase.$location;
-				}
-				$location = strreplaceu(':', $this->yellow->toolbox->getLocationArgsSeparator(), $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(':', $this->yellow->toolbox->getLocationArgsSeparator(), $location);
 		} else {
 			if($filterStrict && !preg_match("/^(http|https|ftp|mailto):/", $location)) $location = "error-xss-filter";
 		}
@@ -2386,17 +2498,30 @@ class YellowLookup
 	}
 	
 	// Normalise URL, make absolute URL
-	function normaliseUrl($serverScheme, $serverName, $base, $location)
+	function normaliseUrl($scheme, $address, $base, $location, $filterStrict = true)
 	{
 		if(!preg_match("/^\w+:/", $location))
 		{
-			$url = "$serverScheme://$serverName$base$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)
 	{
@@ -2429,12 +2554,12 @@ class YellowLookup
 	}
 	
 	// Check if location contains nested directories
-	function isNestedLocation($location, $checkHomeLocation = false)
+	function isNestedLocation($location, $fileName, $checkHomeLocation = false)
 	{
 		$nested = false;
 		if(!$checkHomeLocation || $location==$this->yellow->pages->getHomeLocation($location))
 		{
-			$path = $this->yellow->lookup->findFileFromLocation($location, true);
+			$path = dirname($fileName);
 			if(count($this->yellow->toolbox->getDirectoryEntries($path, "/.*/", true, true, false))) $nested = true;
 		}
 		return $nested;
@@ -2476,15 +2601,6 @@ class YellowLookup
 		return $active;
 	}
 	
-	// Check if location is valid
-	function isValidLocation($location)
-	{
-		$string = "";
-		$tokens = explode('/', $location);
-		for($i=1; $i<count($tokens); ++$i) $string .= '/'.$this->normaliseName($tokens[$i]);
-		return $location==$string;
-	}
-	
 	// Check if file is valid
 	function isValidFile($fileName)
 	{
@@ -2521,50 +2637,50 @@ class YellowLookup
 // Yellow toolbox with helpers
 class YellowToolbox
 {
-	// Return server software
-	function getServerSoftware()
+	// Return server version from current HTTP request
+	function getServerVersion()
 	{
-		$serverSoftware = strtoupperu(PHP_SAPI);
-		if(preg_match("/^(\S+)/", $_SERVER["SERVER_SOFTWARE"], $matches)) $serverSoftware = $matches[1];
-		return $serverSoftware." ".PHP_OS;
+		$serverVersion = strtoupperu(PHP_SAPI)." ".PHP_OS;
+		if(preg_match("/^(\S+)/", $_SERVER["SERVER_SOFTWARE"], $matches)) $serverVersion = $matches[1]." ".PHP_OS;
+		return $serverVersion;
 	}
 	
 	// Return server URL from current HTTP request
 	function getServerUrl()
 	{
-		$serverScheme = $this->getServerScheme();
-		$serverName = $this->getServerName();
-		$serverBase = $this->getServerBase();
-		return "$serverScheme://$serverName$serverBase/";
+		$scheme = $this->getScheme();
+		$address = $this->getAddress();
+		$base = $this->getBase();
+		return "$scheme://$address$base/";
 	}
 	
-	// Return server scheme from current HTTP request
-	function getServerScheme()
+	// Return scheme from current HTTP request
+	function getScheme()
 	{
-		$serverScheme = "";
+		$scheme = "";
 		if(preg_match("/^HTTP\//", $_SERVER["SERVER_PROTOCOL"]))
 		{
 			$secure = isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"]!="off";
-			$serverScheme = $secure ? "https" : "http";
+			$scheme = $secure ? "https" : "http";
 		}
-		return $serverScheme;
+		return $scheme;
 	}
 	
-	// Return server name from current HTTP request
-	function getServerName()
+	// Return address from current HTTP request
+	function getAddress()
 	{
-		$serverName = $_SERVER["SERVER_NAME"];
-		$serverPort = $_SERVER["SERVER_PORT"];
-		if($serverPort!=80 && $serverPort!=443) $serverName .= ":$serverPort";
-		return $serverName;
+		$address = $_SERVER["SERVER_NAME"];
+		$port = $_SERVER["SERVER_PORT"];
+		if($port!=80 && $port!=443) $address .= ":$port";
+		return $address;
 	}
 	
-	// Return server base from current HTTP request
-	function getServerBase()
+	// Return base from current HTTP request
+	function getBase()
 	{
-		$serverBase = "";
-		if(preg_match("/^(.*)\//", $_SERVER["SCRIPT_NAME"], $matches)) $serverBase = rtrim($matches[1], '/');
-		return $serverBase;
+		$base = "";
+		if(preg_match("/^(.*)\//", $_SERVER["SCRIPT_NAME"], $matches)) $base = rtrim($matches[1], '/');
+		return $base;
 	}
 	
 	// Return location from current HTTP request
@@ -2747,30 +2863,16 @@ class YellowToolbox
 		}
 		return $text;
 	}
-
-	// Normalise array elements with different upper and 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;
-	}
 	
-	// Return server time zone
-	function getServerTime()
+	// Return timezone
+	function getTimezone()
 	{
-		$serverTime = @date_default_timezone_get();
-		if(PHP_OS=="Darwin" && $serverTime=="UTC")
+		$timezone = @date_default_timezone_get();
+		if(PHP_OS=="Darwin" && $timezone=="UTC")
 		{
-			if(preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $serverTime = $matches[1];
+			if(preg_match("#zoneinfo/(.*)#", @readlink("/etc/localtime"), $matches)) $timezone = $matches[1];
 		}
-		return $serverTime;
+		return $timezone;
 	}
 	
 	// Return human readable HTTP server status

+ 4 - 3
system/plugins/image.php

@@ -1,11 +1,11 @@
 <?php
-// Copyright (c) 2013-2016 Datenstrom, http://datenstrom.se
+// Copyright (c) 2013-2017 Datenstrom, http://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
 // Image plugin
 class YellowImage
 {
-	const VERSION = "0.6.7";
+	const VERSION = "0.6.8";
 	var $yellow;			//access to API
 	var $graphicsLibrary;	//graphics library support? (boolean)
 
@@ -39,7 +39,8 @@ class YellowImage
 				if(empty($height)) $height = $width;
 				list($src, $width, $height) = $this->getImageInfo($this->yellow->config->get("imageDir").$name, $width, $height);
 			} else {
-				$src = $this->yellow->lookup->normaliseLocation($name, $page->base, $page->location);
+				if(empty($alt)) $alt = $this->yellow->config->get("imageAlt");
+				$src = $this->yellow->lookup->normaliseUrl("", "", "", $name);
 				$width = $height = 0;
 			}
 			$output = "<img src=\"".htmlspecialchars($src)."\"";

+ 3 - 4
system/plugins/markdown.php

@@ -1,11 +1,11 @@
 <?php
-// Copyright (c) 2013-2016 Datenstrom, http://datenstrom.se
+// Copyright (c) 2013-2017 Datenstrom, http://datenstrom.se
 // This file may be used and distributed under the terms of the public license.
 
 // Markdown plugin
 class YellowMarkdown
 {
-	const VERSION = "0.6.4";
+	const VERSION = "0.6.5";
 	var $yellow;			//access to API
 	
 	// Handle initialisation
@@ -37,8 +37,7 @@ class YellowMarkdownParser extends MarkdownExtraParser
 		$this->no_markup = $page->parserSafeMode;
 		$this->url_filter_func = function($url) use ($yellow, $page)
 		{
-			return $yellow->lookup->normaliseLocation($url, $page->base, $page->location,
-				$yellow->config->get("serverBase").$yellow->config->get("imageLocation"),
+			return $yellow->lookup->normaliseLocation($url, $page->location,
 				$page->parserSafeMode && $page->statusCode==200);
 		};
 		parent::__construct();

+ 207 - 104
system/plugins/update.php

@@ -5,8 +5,9 @@
 // Update plugin
 class YellowUpdate
 {
-	const VERSION = "0.6.13";
+	const VERSION = "0.6.14";
 	var $yellow;					//access to API
+	var $updates;					//number of updates
 	
 	// Handle initialisation
 	function onLoad($yellow)
@@ -16,24 +17,67 @@ class YellowUpdate
 		$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("updateNotification", "none");
+		$this->yellow->config->setDefault("updateResourceFile", "resource.ini");
 	}
 	
-	// Handle update
-	function onUpdate($name)
+	// Handle startup
+	function onStartup($update)
 	{
-		if(empty($name)) $this->processUpdateNotification();
+		if($this->yellow->config->isExisting("updateNotification")) //TODO: remove later, cleans old config
+		{
+			$update = true;
+			$fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
+			$fileData = $this->yellow->toolbox->readFile($fileNameConfig);
+			foreach($this->yellow->toolbox->getTextLines($fileData) as $line)
+			{
+				preg_match("/^\s*(.*?)\s*:\s*(.*?)\s*$/", $line, $matches);
+				if(!empty($matches[1]) && is_null($this->yellow->config->configDefaults[$matches[1]]) &&
+				   $line[0]!='#' && substru($line, 0, 5)!="Login")
+				{
+					$fileDataNew .= "# $line";
+				} else {
+					$fileDataNew .= $line;
+				}
+			}
+			$this->yellow->toolbox->createFile($fileNameConfig, $fileDataNew);
+		}
+		if($update)	//TODO: remove later, converts old Yellow version
+		{
+			$fileNameScript = "yellow.php";
+			if(filesize($fileNameScript)==591)
+			{
+				$fileData = $this->yellow->toolbox->readFile($fileNameScript);
+				$fileData = preg_replace("#yellow->plugins->load\(\)#", "yellow->load()", $fileData);
+				$this->yellow->toolbox->createFile($fileNameScript, $fileData);
+			}
+		}
+		if($update)	//TODO: remove later, imports old file format
+		{
+			$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"] = "software-has-been-updated";
+			}
+		}
 	}
 	
 	// Handle request
-	function onRequest($serverScheme, $serverName, $base, $location, $fileName)
+	function onRequest($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
-		if($this->isInstallationMode())
+		if($this->yellow->config->get("installationMode"))
 		{
-			$statusCode = $this->processRequestInstallationMode($serverScheme, $serverName, $base, $location, $fileName);
+			$statusCode = $this->processRequestInstallationMode($scheme, $address, $base, $location, $fileName);
 		} else {
-			$statusCode = $this->processRequestInstallationPending($serverScheme, $serverName, $base, $location, $fileName);
+			$statusCode = $this->processRequestInstallationPending($scheme, $address, $base, $location, $fileName);
 		}
 		return $statusCode;
 	}
@@ -54,7 +98,7 @@ class YellowUpdate
 	// Handle command help
 	function onCommandHelp()
 	{
-		return "update [FEATURE]";
+		return "update [OPTION FEATURE]";
 	}
 	
 	// Clean downloads
@@ -78,30 +122,82 @@ class YellowUpdate
 	// Update website
 	function updateCommand($args)
 	{
-		list($command, $feature, $option) = $args;
-		list($statusCode, $data) = $this->getSoftwareUpdates($feature);
-		if(!empty($data))
+		list($command, $option, $feature) = $args;
+		if(empty($option) || $option=="normal" || $option=="force")
 		{
-			foreach($data as $key=>$value)
+			$force = $option=="force";
+			list($statusCode, $data) = $this->detectSoftware($force, $feature);
+			if($statusCode!=200 || !empty($data))
 			{
-				list($version) = explode(',', $value);
-				echo "$key $version\n";
+				$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";
 			}
-			if($statusCode==200) $statusCode = $this->downloadSoftware($data);
-			if($statusCode==200) $statusCode = $this->updateSoftware($option);
-			if($statusCode!=200) echo "ERROR updating files: ".$this->yellow->page->get("pageError")."\n";
-			echo "Yellow $command: Website ".($statusCode!=200 ? "not " : "")."updated\n";
 		} else {
-			if($statusCode!=200) echo "ERROR updating files: ".$this->yellow->page->get("pageError")."\n";
-			echo "Yellow $command: No updates available\n";
+			$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 = 0;
+		$statusCode = 200;
 		$path = $this->yellow->config->get("pluginDir");
 		$fileExtension = $this->yellow->config->get("downloadExtension");
 		foreach($data as $key=>$value)
@@ -132,31 +228,40 @@ class YellowUpdate
 	}
 
 	// Update software
-	function updateSoftware($option = "")
+	function updateSoftware($force = false)
 	{
-		$statusCode = 0;
+		$statusCode = 200;
 		$path = $this->yellow->config->get("pluginDir");
 		foreach($this->yellow->toolbox->getDirectoryEntries($path, "/^.*\.zip$/", true, false) as $entry)
 		{
-			$statusCode = max($statusCode, $this->updateSoftwareArchive($entry, $option));
+			$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, $option));
+			$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, $option = "")
+	function updateSoftwareArchive($path, $force = false)
 	{
-		$statusCode = 0;
+		$statusCode = 200;
 		$zip = new ZipArchive();
 		if($zip->open($path)===true)
 		{
 			if(defined("DEBUG") && DEBUG>=2) echo "YellowUpdate::updateSoftwareArchive file:$path<br/>\n";
-			if(strtoloweru($option)=="force") $force = true;
 			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)
@@ -193,11 +298,10 @@ class YellowUpdate
 			$zip->close();
 			if($statusCode==200) $statusCode = $this->updateSoftwareNew($software);
 			if($statusCode==200) $statusCode = $this->updateSoftwareNotification($software);
-		}
-		if(!$this->yellow->toolbox->deleteFile($path))
-		{
+			++$this->updates;
+		} else {
 			$statusCode = 500;
-			$this->yellow->page->error($statusCode, "Can't delete file '$path'!");
+			$this->yellow->page->error(500, "Can't open file '$path'!");
 		}
 		return $statusCode;
 	}
@@ -309,12 +413,12 @@ class YellowUpdate
 	function updateSoftwareNotification($software)
 	{
 		$statusCode = 200;
-		$updateNotification = $this->yellow->config->get("updateNotification");
-		if($updateNotification=="none") $updateNotification = "";
-		if(!empty($updateNotification)) $updateNotification .= ",";
-		$updateNotification .= $software;
+		$startupUpdateNotification = $this->yellow->config->get("startupUpdateNotification");
+		if($startupUpdateNotification=="none") $startupUpdateNotification = "";
+		if(!empty($startupUpdateNotification)) $startupUpdateNotification .= ",";
+		$startupUpdateNotification .= $software;
 		$fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
-		if(!$this->yellow->config->update($fileNameConfig, array("updateNotification" => $updateNotification)))
+		if(!$this->yellow->config->update($fileNameConfig, array("startupUpdateNotification" => $startupUpdateNotification)))
 		{
 			$statusCode = 500;
 			$this->yellow->page->error(500, "Can't write file '$fileNameConfig'!");
@@ -322,17 +426,21 @@ class YellowUpdate
 		return $statusCode;
 	}
 	
-	// Update software features
-	function updateSoftwareFeatures($feature)
+	// Update installation features
+	function updateInstallationFeatures($feature)
 	{
 		$statusCode = 200;
 		$path = $this->yellow->config->get("pluginDir");
 		$regex = "/^.*\\".$this->yellow->config->get("installationExtension")."$/";
 		foreach($this->yellow->toolbox->getDirectoryEntries($path, $regex, true, false) as $entry)
 		{
-			if(stristr(basename($entry), $feature))
+			if(preg_match("/^(.*?)-(.*?)\./", basename($entry), $matches))
 			{
-				$statusCode = max($statusCode, $this->updateSoftwareArchive($entry));
+				if(strtoloweru($matches[2])==strtoloweru($feature))
+				{
+					$statusCode = max($statusCode, $this->updateSoftwareArchive($entry));
+					break;
+				}
 			}
 		}
 		if($statusCode==200)
@@ -345,21 +453,6 @@ class YellowUpdate
 		return $statusCode;
 	}
 	
-	// Process update notification for recently installed software
-	function processUpdateNotification()
-	{
-		if($this->yellow->config->get("updateNotification")!="none")
-		{
-			$tokens = explode(',', $this->yellow->config->get("updateNotification"));
-			foreach($this->yellow->plugins->plugins as $key=>$value)
-			{
-				if(in_array($value["plugin"], $tokens) && method_exists($value["obj"], "onUpdate")) $value["obj"]->onUpdate($key);
-			}
-			$fileNameConfig = $this->yellow->config->get("configDir").$this->yellow->config->get("configFile");
-			$this->yellow->config->update($fileNameConfig, array("updateNotification" => "none"));
-		}
-	}
-	
 	// Process command to install pending software
 	function processCommandInstallationPending($args)
 	{
@@ -367,26 +460,23 @@ class YellowUpdate
 		if($this->isSoftwarePending())
 		{
 			$statusCode = $this->updateSoftware();
-			if($statusCode!=0)
-			{
-				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";
-			}
+			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($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestInstallationPending($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
-		if($this->yellow->lookup->isContentFile($fileName) && $this->isSoftwarePending())
+		if($this->yellow->lookup->isContentFile($fileName) && !$this->yellow->isCommandLine() && $this->isSoftwarePending())
 		{
 			$statusCode = $this->updateSoftware();
 			if($statusCode==200)
 			{
 				$statusCode = 303;
-				$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location);
+				$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
 				$this->yellow->sendStatus($statusCode, $location);
 			}
 		}
@@ -394,14 +484,14 @@ class YellowUpdate
 	}
 	
 	// Process request to install website
-	function processRequestInstallationMode($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestInstallationMode($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
-		if($this->yellow->lookup->isContentFile($fileName) && $this->isInstallationMode())
+		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($serverScheme, $serverName, $base, $location, $fileName);
+			$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();
@@ -435,7 +525,7 @@ class YellowUpdate
 			{
 				if(!empty($feature))
 				{
-					$status = $this->updateSoftwareFeatures($feature)==200 ? "ok" : "error";
+					$status = $this->updateInstallationFeatures($feature)==200 ? "ok" : "error";
 					if($status=="error") $this->yellow->page->error(500, "Can't install feature '$feature'!");
 				}
 			}
@@ -449,7 +539,7 @@ class YellowUpdate
 			if($status=="done")
 			{
 				$statusCode = 303;
-				$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location);
+				$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
 				$this->yellow->sendStatus($statusCode, $location);
 			} else {
 				$statusCode = $this->yellow->sendPage();
@@ -482,10 +572,10 @@ class YellowUpdate
 				}
 				$rawData .= "</p>\n";
 			}
-			if(count($this->getSoftwareFeatures())>1)
+			if(count($this->getInstallationFeatures())>1)
 			{
 				$rawData .= "<p>".$this->yellow->text->get("webinterfaceInstallationFeature")."<p>";
-				foreach($this->getSoftwareFeatures() as $feature)
+				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 />";
@@ -515,16 +605,14 @@ class YellowUpdate
 			if(!$this->yellow->config->isExisting($key)) continue;
 			$data[$key] = trim($value);
 		}
-		$data["# serverScheme"] = $this->yellow->toolbox->getServerScheme();
-		$data["# serverName"] = $this->yellow->toolbox->getServerName();
-		$data["# serverBase"] = $this->yellow->toolbox->getServerBase();
-		$data["# serverTime"] = $this->yellow->toolbox->getServerTime();
+		$data["timezone"] = $this->yellow->toolbox->getTimezone();
+		$data["staticUrl"] = $this->yellow->toolbox->getServerUrl();
 		$data["installationMode"] = "0";
 		return $data;
 	}
 
-	// Return software features
-	function getSoftwareFeatures()
+	// Return installation features
+	function getInstallationFeatures()
 	{
 		$data = array("website");
 		$path = $this->yellow->config->get("pluginDir");
@@ -539,25 +627,6 @@ class YellowUpdate
 		return $data;
 	}
 	
-	// Return software updates
-	function getSoftwareUpdates($feature)
-	{
-		$data = array();
-		list($statusCode, $dataCurrent) = $this->getSoftwareVersion();
-		list($statusCode, $dataLatest) = $this->getSoftwareVersion(true, true);
-		foreach($dataCurrent as $key=>$value)
-		{
-			list($version) = explode(',', $dataLatest[$key]);
-			if(empty($feature))
-			{
-				if(strnatcasecmp($dataCurrent[$key], $version)<0) $data[$key] = $dataLatest[$key];
-			} else {
-				if(stristr($key, $feature) && $version) $data[$key] = $dataLatest[$key];
-			}
-		}
-		return array($statusCode, $data);
-	}
-
 	// Return software version
 	function getSoftwareVersion($latest = false, $rawFormat = false)
 	{
@@ -587,6 +656,46 @@ class YellowUpdate
 		}
 		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)
@@ -641,12 +750,6 @@ class YellowUpdate
 		$data = array_merge($this->yellow->plugins->getData(), $this->yellow->themes->getData());
 		return !is_null($data[$software]);
 	}
-
-	// Check if installation mode
-	function isInstallationMode()
-	{
-		return $this->yellow->config->get("installationMode") && PHP_SAPI!="cli";
-	}
 }
 	
 $yellow->plugins->register("update", "YellowUpdate", YellowUpdate::VERSION, 1);

+ 1 - 1
system/plugins/webinterface.css

@@ -1,4 +1,4 @@
-/* Yellow web interface 0.6.19 */
+/* Yellow web interface 0.6.20 */
 
 .yellow-bar { position:relative; overflow:hidden; height:2em; margin-bottom:10px; }
 .yellow-bar-left { display:block; float:left; }

+ 28 - 16
system/plugins/webinterface.js

@@ -4,8 +4,8 @@
 // Yellow API
 var yellow =
 {
-	version: "0.6.19",
-	action: function(action) { yellow.webinterface.action(action, "none"); },
+	version: "0.6.20",
+	action: function(action, status, args) { yellow.webinterface.action(action, status, args); },
 	onLoad: function() { yellow.webinterface.loadInterface(); },
 	onClick: function(e) { yellow.webinterface.hidePanesOnClick(yellow.toolbox.getEventElement(e)); },
 	onKeydown: function(e) { yellow.webinterface.hidePanesOnKeydown(yellow.toolbox.getEventKeycode(e)); },
@@ -22,9 +22,10 @@ yellow.webinterface =
 	intervalId: 0,		//timer interval ID
 
 	// Handle action
-	action: function(action, status)
+	action: function(action, status, args)
 	{
-		if(yellow.config.debug) console.log("yellow.webinterface.action action:"+action+" status:"+status);
+		status = status ? status : "none";
+		args = args ? args : "none";
 		switch(action)
 		{
 			case "login":		this.showPane("yellow-pane-login", action, status); break;
@@ -37,7 +38,7 @@ yellow.webinterface =
 			case "reconfirm":	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-version", action); break;
+			case "update":		this.sendPane("yellow-pane-update", action, status, args); 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;
@@ -430,7 +431,7 @@ yellow.webinterface =
 	},
 	
 	// Send pane
-	sendPane: function(paneId, paneAction)
+	sendPane: function(paneId, paneAction, paneStatus, paneArgs)
 	{
 		if(yellow.config.debug) console.log("yellow.webinterface.sendPane id:"+paneId);
 		if(paneId=="yellow-pane-edit")
@@ -438,16 +439,27 @@ yellow.webinterface =
 			paneAction = this.getPaneAction(paneId, paneAction);
 			if(paneAction)
 			{
-				var params = {};
-				params.action = paneAction;
-				params.rawdatasource = yellow.page.rawDataSource;
-				params.rawdataedit = document.getElementById("yellow-pane-edit-page").value;
-				yellow.toolbox.submitForm(params, true);
+				var args = {};
+				args.action = paneAction;
+				args.rawdatasource = yellow.page.rawDataSource;
+				args.rawdataedit = document.getElementById("yellow-pane-edit-page").value;
+				yellow.toolbox.submitForm(args, true);
 			} else {
 				this.hidePane(paneId);
 			}
 		} else {
-			yellow.toolbox.submitForm({"action":paneAction});
+			var args = {"action":paneAction};
+			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);
 		}
 	},
 	
@@ -777,14 +789,14 @@ yellow.toolbox =
 	},
 	
 	// Submit form with post method
-	submitForm: function(params, encodeNewline)
+	submitForm: function(args, encodeNewline)
 	{
 		var elementForm = document.createElement("form");
 		elementForm.setAttribute("method", "post");
-		for(var key in params)
+		for(var key in args)
 		{
-			if(!params.hasOwnProperty(key)) continue;
-			var value = encodeNewline ? this.encodeNewline(params[key]) : params[key];
+			if(!args.hasOwnProperty(key)) continue;
+			var value = encodeNewline ? this.encodeNewline(args[key]) : args[key];
 			var elementInput = document.createElement("input");
 			elementInput.setAttribute("type", "hidden");
 			elementInput.setAttribute("name", key);

+ 156 - 133
system/plugins/webinterface.php

@@ -5,7 +5,7 @@
 // Web interface plugin
 class YellowWebinterface
 {
-	const VERSION = "0.6.19";
+	const VERSION = "0.6.20";
 	var $yellow;			//access to API
 	var $response;			//web interface response
 	var $users;				//web interface users
@@ -18,8 +18,6 @@ class YellowWebinterface
 		$this->response = new YellowResponse($yellow);
 		$this->users = new YellowUsers($yellow);
 		$this->merge = new YellowMerge($yellow);
-		$this->yellow->config->setDefault("webinterfaceServerScheme", $this->yellow->config->get("serverScheme"));
-		$this->yellow->config->setDefault("webinterfaceServerName", $this->yellow->config->get("serverName"));
 		$this->yellow->config->setDefault("webinterfaceLocation", "/edit/");
 		$this->yellow->config->setDefault("webinterfaceNewFile", "page-new-(.*).txt");
 		$this->yellow->config->setDefault("webinterfaceMetaFilePrefix", "published");
@@ -32,24 +30,24 @@ class YellowWebinterface
 		$this->users->load($this->yellow->config->get("configDir").$this->yellow->config->get("webinterfaceUserFile"));
 	}
 
-	// Handle update
-	function onUpdate($name)
+	// Handle startup
+	function onStartup($update)
 	{
-		if($name=="webinterface") $this->cleanCommand(array("clean", "all"));
+		if($update) $this->cleanCommand(array("clean", "all"));
 	}
 	
 	// Handle request
-	function onRequest($serverScheme, $serverName, $base, $location, $fileName)
+	function onRequest($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
 		if($this->checkRequest($location))
 		{
-			$serverScheme = $this->yellow->config->get("webinterfaceServerScheme");
-			$serverName = $this->yellow->config->get("webinterfaceServerName");
+			$scheme = $this->yellow->config->get("serverScheme");
+			$address = $this->yellow->config->get("serverAddress");
 			$base = rtrim($this->yellow->config->get("serverBase").$this->yellow->config->get("webinterfaceLocation"), '/');
-			list($serverScheme, $serverName, $base, $location, $fileName) = $this->yellow->getRequestInformation($serverScheme, $serverName, $base);
-			$this->yellow->page->setRequestInformation($serverScheme, $serverName, $base, $location, $fileName);
-			$statusCode = $this->processRequest($serverScheme, $serverName, $base, $location, $fileName);
+			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;
 	}
@@ -91,9 +89,9 @@ class YellowWebinterface
 		$output = null;
 		if($name=="header" && $this->response->isActive())
 		{
-			$location = $this->yellow->config->get("serverBase").$this->yellow->config->get("pluginLocation")."webinterface";
-			$output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"".htmlspecialchars($location).".css\" />\n";
-			$output .= "<script type=\"text/javascript\" src=\"".htmlspecialchars($location).".js\"></script>\n";
+			$pluginLocation = $this->yellow->config->get("serverBase").$this->yellow->config->get("pluginLocation");
+			$output = "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"{$pluginLocation}webinterface.css\" />\n";
+			$output .= "<script type=\"text/javascript\" src=\"{$pluginLocation}webinterface.js\"></script>\n";
 			$output .= "<script type=\"text/javascript\">\n";
 			$output .= "// <![CDATA[\n";
 			$output .= "yellow.page = ".json_encode($this->response->getPageData()).";\n";
@@ -176,40 +174,40 @@ class YellowWebinterface
 	}
 	
 	// Process request
-	function processRequest($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequest($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
-		if($this->checkUser($serverScheme, $serverName, $base, $location, $fileName))
+		if($this->checkUser($scheme, $address, $base, $location, $fileName))
 		{
 			switch($_REQUEST["action"])
 			{
-				case "":			$statusCode = $this->processRequestShow($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "login":		$statusCode = $this->processRequestLogin($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "logout":		$statusCode = $this->processRequestLogout($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "signup":		$statusCode = $this->processRequestSignup($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "confirm":		$statusCode = $this->processRequestConfirm($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "approve":		$statusCode = $this->processRequestApprove($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "recover":		$statusCode = $this->processRequestRecover($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "settings":	$statusCode = $this->processRequestSettings($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "reconfirm":	$statusCode = $this->processRequestReconfirm($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "change":		$statusCode = $this->processRequestChange($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "version":		$statusCode = $this->processRequestVersion($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "update":		$statusCode = $this->processRequestUpdate($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "create":		$statusCode = $this->processRequestCreate($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "edit":		$statusCode = $this->processRequestEdit($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "delete":		$statusCode = $this->processRequestDelete($serverScheme, $serverName, $base, $location, $fileName); break;
+				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 "signup":		$statusCode = $this->processRequestSignup($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 "settings":	$statusCode = $this->processRequestSettings($scheme, $address, $base, $location, $fileName); break;
+				case "reconfirm":	$statusCode = $this->processRequestReconfirm($scheme, $address, $base, $location, $fileName); break;
+				case "change":		$statusCode = $this->processRequestChange($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 "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;
 			}
 		} else {
-			$this->yellow->pages->requestHandler = "core";
+			$this->yellow->lookup->requestHandler = "core";
 			switch($_REQUEST["action"])
 			{
-				case "":			$statusCode = $this->processRequestShow($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "signup":		$statusCode = $this->processRequestSignup($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "confirm":		$statusCode = $this->processRequestConfirm($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "approve":		$statusCode = $this->processRequestApprove($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "recover":		$statusCode = $this->processRequestRecover($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "reconfirm":	$statusCode = $this->processRequestReconfirm($serverScheme, $serverName, $base, $location, $fileName); break;
-				case "change":		$statusCode = $this->processRequestChange($serverScheme, $serverName, $base, $location, $fileName); break;
+				case "":			$statusCode = $this->processRequestShow($scheme, $address, $base, $location, $fileName); break;
+				case "signup":		$statusCode = $this->processRequestSignup($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 "reconfirm":	$statusCode = $this->processRequestReconfirm($scheme, $address, $base, $location, $fileName); break;
+				case "change":		$statusCode = $this->processRequestChange($scheme, $address, $base, $location, $fileName); break;
 			}
 			if($this->response->action=="fail") $this->yellow->page->error(500, "Login failed, [please log in](javascript:yellow.action('login');)!");
 		}
@@ -217,22 +215,22 @@ class YellowWebinterface
 	}
 	
 	// Process request to show file
-	function processRequestShow($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestShow($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
 		if(is_readable($fileName))
 		{
-			$statusCode = $this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+			$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 		} else {
 			if($this->yellow->lookup->isRedirectLocation($location))
 			{
 				$statusCode = 301;
 				$location = $this->yellow->lookup->isFileLocation($location) ? "$location/" : "/".$this->yellow->getRequestLanguage()."/";
-				$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location);
+				$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
 				$this->yellow->sendStatus($statusCode, $location);
 			} else {
 				$statusCode = $this->response->isUserRestrictions() ? 404 : 424;
-				$this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+				$this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 				$this->yellow->page->error($statusCode);
 			}
 		}
@@ -240,39 +238,39 @@ class YellowWebinterface
 	}
 
 	// Process request for user login
-	function processRequestLogin($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestLogin($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
 		$home = $this->users->getHome($this->response->userEmail);
 		if(substru($location, 0, strlenu($home))==$home)
 		{
 			$statusCode = 303;
-			$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location);
+			$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
 			$this->yellow->sendStatus($statusCode, $location);
 		} else {
 			$statusCode = 302;
-			$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $home);
+			$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $home);
 			$this->yellow->sendStatus($statusCode, $location);
 		}
 		return $statusCode;
 	}
 	
 	// Process request for user logout
-	function processRequestLogout($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestLogout($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 302;
 		$this->response->userEmail = "";
-		$this->response->destroyCookie($serverScheme, $serverName, $base);
+		$this->response->destroyCookie($scheme, $address, $base);
 		$location = $this->yellow->lookup->normaliseUrl(
 			$this->yellow->config->get("serverScheme"),
-			$this->yellow->config->get("serverName"),
+			$this->yellow->config->get("serverAddress"),
 			$this->yellow->config->get("serverBase"), $location);
 		$this->yellow->sendStatus($statusCode, $location);
 		return $statusCode;
 	}
 
 	// Process request for user signup
-	function processRequestSignup($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestSignup($scheme, $address, $base, $location, $fileName)
 	{
 		$this->response->action = "signup";
 		$this->response->status = "ok";
@@ -291,15 +289,15 @@ class YellowWebinterface
 		}
 		if($this->response->status=="ok")
 		{
-			$this->response->status = $this->response->sendMail($serverScheme, $serverName, $base, $email, "confirm") ? "next" : "error";
+			$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($serverScheme, $serverName, $base, $location, $fileName, false);
+		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 		return $statusCode;
 	}
 	
 	// Process request to confirm user signup
-	function processRequestConfirm($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestConfirm($scheme, $address, $base, $location, $fileName)
 	{
 		$this->response->action = "confirm";
 		$this->response->status = "ok";
@@ -313,15 +311,15 @@ class YellowWebinterface
 		}
 		if($this->response->status=="ok")
 		{
-			$this->response->status = $this->response->sendMail($serverScheme, $serverName, $base, $email, "approve") ? "done" : "error";
+			$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($serverScheme, $serverName, $base, $location, $fileName, false);
+		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 		return $statusCode;
 	}
 	
 	// Process request to approve user signup
-	function processRequestApprove($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestApprove($scheme, $address, $base, $location, $fileName)
 	{
 		$this->response->action = "approve";
 		$this->response->status = "ok";
@@ -335,15 +333,15 @@ class YellowWebinterface
 		}
 		if($this->response->status=="ok")
 		{
-			$this->response->status = $this->response->sendMail($serverScheme, $serverName, $base, $email, "welcome") ? "done" : "error";
+			$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($serverScheme, $serverName, $base, $location, $fileName, false);
+		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 		return $statusCode;
 	}
 	
 	// Process request to recover password
-	function processRequestRecover($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestRecover($scheme, $address, $base, $location, $fileName)
 	{
 		$this->response->action = "recover";
 		$this->response->status = "ok";
@@ -356,7 +354,7 @@ class YellowWebinterface
 			if($this->response->status=="ok" && !$this->users->isExisting($email)) $this->response->status = "next";
 			if($this->response->status=="ok")
 			{
-				$this->response->status = $this->response->sendMail($serverScheme, $serverName, $base, $email, "recover") ? "next" : "error";
+				$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!");
 			}
 		} else {
@@ -374,18 +372,18 @@ class YellowWebinterface
 				if($this->response->status=="ok")
 				{
 					$this->response->userEmail = "";
-					$this->response->destroyCookie($serverScheme, $serverName, $base);
-					$this->response->status = $this->response->sendMail($serverScheme, $serverName, $base, $email, "information") ? "done" : "error";
+					$this->response->destroyCookie($scheme, $address, $base);
+					$this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "information") ? "done" : "error";
 					if($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
 				}
 			}
 		}
-		$statusCode = $this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 		return $statusCode;
 	}
 	
 	// Process request to change settings
-	function processRequestSettings($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestSettings($scheme, $address, $base, $location, $fileName)
 	{
 		$this->response->action = "settings";
 		$this->response->status = "ok";
@@ -416,7 +414,7 @@ class YellowWebinterface
 			if($this->response->status=="ok")
 			{
 				$action = $email!=$emailSource ? "reconfirm" : "change";
-				$this->response->status = $this->response->sendMail($serverScheme, $serverName, $base, $email, $action) ? "next" : "error";
+				$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 {
@@ -430,16 +428,16 @@ class YellowWebinterface
 		if($this->response->status=="done")
 		{
 			$statusCode = 303;
-			$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location);
+			$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
 			$this->yellow->sendStatus($statusCode, $location);
 		} else {
-			$statusCode = $this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+			$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 		}
 		return $statusCode;
 	}
 
 	// Process request to reconfirm email
-	function processRequestReconfirm($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestReconfirm($scheme, $address, $base, $location, $fileName)
 	{
 		$this->response->action = "reconfirm";
 		$this->response->status = "ok";
@@ -458,15 +456,15 @@ class YellowWebinterface
 		}
 		if($this->response->status=="ok")
 		{
-			$this->response->status = $this->response->sendMail($serverScheme, $serverName, $base, $emailSource, "change") ? "done" : "error";
+			$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($serverScheme, $serverName, $base, $location, $fileName, false);
+		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 		return $statusCode;
 	}
 	
 	// Process request to change settings
-	function processRequestChange($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestChange($scheme, $address, $base, $location, $fileName)
 	{
 		$this->response->action = "change";
 		$this->response->status = "ok";
@@ -495,51 +493,75 @@ class YellowWebinterface
 		if($this->response->status=="ok")
 		{
 			$this->response->userEmail = "";
-			$this->response->destroyCookie($serverScheme, $serverName, $base);
-			$this->response->status = $this->response->sendMail($serverScheme, $serverName, $base, $email, "information") ? "done" : "error";
+			$this->response->destroyCookie($scheme, $address, $base);
+			$this->response->status = $this->response->sendMail($scheme, $address, $base, $email, "information") ? "done" : "error";
 			if($this->response->status=="error") $this->yellow->page->error(500, "Can't send email on this server!");
 		}
-		$statusCode = $this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 		return $statusCode;
 	}
 	
 	// Process request to show software version
-	function processRequestVersion($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestVersion($scheme, $address, $base, $location, $fileName)
 	{
 		$this->response->action = "version";
 		$this->response->status = "ok";
 		if($this->yellow->plugins->isExisting("update"))
 		{
-			list($statusCode, $dataCurrent) = $this->yellow->plugins->get("update")->getSoftwareVersion();
-			list($statusCode, $dataLatest) = $this->yellow->plugins->get("update")->getSoftwareVersion(true);
-			foreach($dataCurrent as $key=>$value)
+			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())
 			{
-				if(strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0)
+				foreach($dataCurrent as $key=>$value)
 				{
-					if(!empty($this->response->rawDataOutput)) $this->response->rawDataOutput .= "<br />\n";
-					$this->response->rawDataOutput .= "$key $dataLatest[$key]";
-					++$updates;
-					++$count; if($count>=4) { $this->response->rawDataOutput .= "…"; break; }
+					if(strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0)
+					{
+						++$updates;
+						if(!empty($this->response->rawDataOutput)) $this->response->rawDataOutput .= "<br />\n";
+						$this->response->rawDataOutput .= htmlspecialchars("$key $dataLatest[$key]");
+					}
+				}
+				if($updates==0)
+				{
+					foreach($dataCurrent as $key=>$value)
+					{
+						if(!is_null($dataModified[$key]) && !is_null($dataLatest[$key]))
+						{
+							$rawData = $this->yellow->text->getTextHtml("webinterfaceVersionUpdateModified", $this->response->language)." - <a href=\"#\" onclick=\"yellow.action('update','update','".$this->yellow->toolbox->normaliseArgs("option:force/feature:$key")."'); return false;\">".$this->yellow->text->getTextHtml("webinterfaceVersionUpdateForce", $this->response->language)."</a>";
+							$rawData = preg_replace("/@software/i", htmlspecialchars("$key $dataLatest[$key]"), $rawData);
+							if(!empty($this->response->rawDataOutput)) $this->response->rawDataOutput .= "<br />\n";
+							$this->response->rawDataOutput .= $rawData;
+						}
+					}
+				}
+			} else {
+				foreach($dataCurrent as $key=>$value)
+				{
+					if(strnatcasecmp($dataCurrent[$key], $dataLatest[$key])<0) ++$updates;
 				}
 			}
 			$this->response->status = $updates ? "updates" : "done";
 			if($statusCode!=200) $this->response->status = "error";
 		}
-		$statusCode = $this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+		$statusCode = $this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 		return $statusCode;
 	}
 	
 	// Process request to update website
-	function processRequestUpdate($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestUpdate($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
 		if($this->yellow->plugins->isExisting("update") && $this->response->isUserWebmaster())
 		{
-			$statusCode = $this->yellow->command("update");
+			$option = trim($_REQUEST["option"]);
+			$feature = trim($_REQUEST["feature"]);
+			$statusCode = $this->yellow->command("update", $option, $feature);
 			if($statusCode==200)
 			{
 				$statusCode = 303;
-				$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location);
+				$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
 				$this->yellow->sendStatus($statusCode, $location);
 			}
 		}
@@ -547,29 +569,29 @@ class YellowWebinterface
 	}
 	
 	// Process request to create page
-	function processRequestCreate($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestCreate($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
 		if(!$this->response->isUserRestrictions() && !empty($_POST["rawdataedit"]))
 		{
 			$this->response->rawDataSource = $this->response->rawDataEdit = rawurldecode($_POST["rawdatasource"]);
 			$rawData = $this->response->normaliseText(rawurldecode($_POST["rawdataedit"]));
-			$page = $this->response->getPageNew($serverScheme, $serverName, $base, $location, $fileName, $rawData);
+			$page = $this->response->getPageNew($scheme, $address, $base, $location, $fileName, $rawData);
 			if(!$page->isError())
 			{
 				if($this->yellow->toolbox->createFile($page->fileName, $page->rawData, true))
 				{
 					$statusCode = 303;
-					$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $page->location);
+					$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location);
 					$this->yellow->sendStatus($statusCode, $location);
 				} else {
 					$statusCode = 500;
-					$this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+					$this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 					$this->yellow->page->error($statusCode, "Can't write file '$page->fileName'!");
 				}
 			} else {
 				$statusCode = 500;
-				$this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+				$this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 				$this->yellow->page->error($statusCode, $page->get("pageError"));
 			}
 		}
@@ -577,14 +599,14 @@ class YellowWebinterface
 	}
 	
 	// Process request to edit page
-	function processRequestEdit($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestEdit($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
 		if(!$this->response->isUserRestrictions() && !empty($_POST["rawdataedit"]))
 		{
 			$this->response->rawDataSource = rawurldecode($_POST["rawdatasource"]);
 			$this->response->rawDataEdit = $this->response->normaliseText(rawurldecode($_POST["rawdataedit"]));
-			$page = $this->response->getPageUpdate($serverScheme, $serverName, $base, $location, $fileName,
+			$page = $this->response->getPageUpdate($scheme, $address, $base, $location, $fileName,
 				$this->response->rawDataSource, $this->response->rawDataEdit, $this->yellow->toolbox->readFile($fileName));
 			if(!$page->isError())
 			{
@@ -592,16 +614,16 @@ class YellowWebinterface
 				   $this->yellow->toolbox->createFile($page->fileName, $page->rawData))
 				{
 					$statusCode = 303;
-					$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $page->location);
+					$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $page->location);
 					$this->yellow->sendStatus($statusCode, $location);
 				} else {
 					$statusCode = 500;
-					$this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+					$this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 					$this->yellow->page->error($statusCode, "Can't write file '$page->fileName'!");
 				}
 			} else {
 				$statusCode = 500;
-				$this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+				$this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 				$this->yellow->page->error($statusCode, $page->get("pageError"));
 			}
 		}
@@ -609,7 +631,7 @@ class YellowWebinterface
 	}
 
 	// Process request to delete page
-	function processRequestDelete($serverScheme, $serverName, $base, $location, $fileName)
+	function processRequestDelete($scheme, $address, $base, $location, $fileName)
 	{
 		$statusCode = 0;
 		if(!$this->response->isUserRestrictions() && is_file($fileName))
@@ -620,22 +642,22 @@ class YellowWebinterface
 				if($this->yellow->toolbox->deleteFile($fileName, $this->yellow->config->get("trashDir")))
 				{
 					$statusCode = 303;
-					$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location);
+					$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
 					$this->yellow->sendStatus($statusCode, $location);
 				} else {
 					$statusCode = 500;
-					$this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+					$this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 					$this->yellow->page->error($statusCode, "Can't delete file '$fileName'!");
 				}
 			} else {
 				if($this->yellow->toolbox->deleteDirectory(dirname($fileName), $this->yellow->config->get("trashDir")))
 				{
 					$statusCode = 303;
-					$location = $this->yellow->lookup->normaliseUrl($serverScheme, $serverName, $base, $location);
+					$location = $this->yellow->lookup->normaliseUrl($scheme, $address, $base, $location);
 					$this->yellow->sendStatus($statusCode, $location);
 				} else {
 					$statusCode = 500;
-					$this->yellow->processRequest($serverScheme, $serverName, $base, $location, $fileName, false);
+					$this->yellow->processRequest($scheme, $address, $base, $location, $fileName, false);
 					$this->yellow->page->error($statusCode, "Can't delete file '$fileName'!");
 				}
 			}
@@ -646,17 +668,13 @@ class YellowWebinterface
 	// Check web interface request
 	function checkRequest($location)
 	{
-		if($this->yellow->toolbox->getServerScheme()==$this->yellow->config->get("webinterfaceServerScheme") &&
-		   $this->yellow->toolbox->getServerName()==$this->yellow->config->get("webinterfaceServerName"))
-		{
-			$locationLength = strlenu($this->yellow->config->get("webinterfaceLocation"));
-			$this->response->active = substru($location, 0, $locationLength)==$this->yellow->config->get("webinterfaceLocation");
-		}
+		$locationLength = strlenu($this->yellow->config->get("webinterfaceLocation"));
+		$this->response->active = substru($location, 0, $locationLength)==$this->yellow->config->get("webinterfaceLocation");
 		return $this->response->isActive();
 	}
 	
 	// Check web interface user
-	function checkUser($serverScheme, $serverName, $base, $location, $fileName)
+	function checkUser($scheme, $address, $base, $location, $fileName)
 	{
 		if($_POST["action"]=="login")
 		{
@@ -664,7 +682,7 @@ class YellowWebinterface
 			$password = $_POST["password"];
 			if($this->users->checkUser($email, $password))
 			{
-				$this->response->createCookie($serverScheme, $serverName, $base, $email);
+				$this->response->createCookie($scheme, $address, $base, $email);
 				$this->response->userEmail = $email;
 				$this->response->userRestrictions = $this->getUserRestrictions($email, $location, $fileName);
 				$this->response->language = $this->response->getLanguage($email);
@@ -749,10 +767,10 @@ class YellowResponse
 	}
 	
 	// Return new page
-	function getPageNew($serverScheme, $serverName, $base, $location, $fileName, $rawData)
+	function getPageNew($scheme, $address, $base, $location, $fileName, $rawData)
 	{
 		$page = new YellowPage($this->yellow);
-		$page->setRequestInformation($serverScheme, $serverName, $base, $location, $fileName);
+		$page->setRequestInformation($scheme, $address, $base, $location, $fileName);
 		$page->parseData($rawData, false, 0);
 		if($this->yellow->lookup->isFileLocation($location) || is_file($fileName))
 		{
@@ -793,16 +811,16 @@ class YellowResponse
 	}
 	
 	// Return modified page
-	function getPageUpdate($serverScheme, $serverName, $base, $location, $fileName, $rawDataSource, $rawDataEdit, $rawDataFile)
+	function getPageUpdate($scheme, $address, $base, $location, $fileName, $rawDataSource, $rawDataEdit, $rawDataFile)
 	{
 		$page = new YellowPage($this->yellow);
-		$page->setRequestInformation($serverScheme, $serverName, $base, $location, $fileName);
+		$page->setRequestInformation($scheme, $address, $base, $location, $fileName);
 		$page->parseData($this->webinterface->merge->merge($rawDataSource, $rawDataEdit, $rawDataFile), false, 0);
 		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($serverScheme, $serverName, $base, $location, $fileName);
+			$pageSource->setRequestInformation($scheme, $address, $base, $location, $fileName);
 			$pageSource->parseData($rawDataSource, false, 0);
 			$prefix = $this->yellow->config->get("webinterfaceMetaFilePrefix");
 			if($pageSource->get($prefix)!=$page->get($prefix) || $pageSource->get("title")!=$page->get("title"))
@@ -840,7 +858,10 @@ class YellowResponse
 			$data["rawDataEdit"] = $this->rawDataEdit;
 			$data["rawDataNew"] = $this->getRawDataNew();
 			$data["rawDataOutput"] = strval($this->rawDataOutput);
-			$data["pageFile"] = $this->yellow->page->get("pageFile");
+			$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());
@@ -864,15 +885,14 @@ class YellowResponse
 			$data["userRestrictions"] = intval($this->isUserRestrictions());
 			$data["userWebmaster"] = intval($this->isUserWebmaster());
 			$data["pluginUpdate"] = intval($this->yellow->plugins->isExisting("update"));
-			$data["serverScheme"] = $this->yellow->config->get("serverScheme");
-			$data["serverName"] = $this->yellow->config->get("serverName");
-			$data["serverBase"] = $this->yellow->config->get("serverBase");
-			$data["serverTime"] = $this->yellow->config->get("serverTime");
 			$data["serverLanguages"] = array();
 			foreach($this->yellow->text->getLanguages() as $language)
 			{
 				$data["serverLanguages"][$language] = $this->yellow->text->getTextHtml("languageDescription", $language);
 			}
+			$data["serverScheme"] = $this->yellow->config->get("serverScheme");
+			$data["serverAddress"] = $this->yellow->config->get("serverAddress");
+			$data["serverBase"] = $this->yellow->config->get("serverBase");
 			$data["serverVersion"] = "Yellow ".YellowCore::VERSION;
 		} else {
 			$data["loginEmail"] = $this->yellow->config->get("loginEmail");
@@ -898,9 +918,9 @@ class YellowResponse
 	// Return text strings
 	function getTextData()
 	{
-		$textLanguage = array_merge($this->yellow->text->getData("language", $this->language));
-		$textWebinterface = array_merge($this->yellow->text->getData("webinterface", $this->language));
-		$textYellow = array_merge($this->yellow->text->getData("yellow", $this->language));
+		$textLanguage = $this->yellow->text->getData("language", $this->language);
+		$textWebinterface = $this->yellow->text->getData("webinterface", $this->language);
+		$textYellow = $this->yellow->text->getData("yellow", $this->language);
 		return array_merge($textLanguage, $textWebinterface, $textYellow);
 	}
 	
@@ -968,28 +988,28 @@ class YellowResponse
 	}
 
 	// Create browser cookie
-	function createCookie($serverScheme, $serverName, $base, $email)
+	function createCookie($scheme, $address, $base, $email)
 	{
 		$session = $this->webinterface->users->createSession($email);
-		setcookie("login", "$email,$session", time()+60*60*24*365, "$base/", "", $serverScheme=="https");
+		setcookie("login", "$email,$session", time()+60*60*24*365, "$base/", "", $scheme=="https");
 	}
 	
 	// Destroy browser cookie
-	function destroyCookie($serverScheme, $serverName, $base)
+	function destroyCookie($scheme, $address, $base)
 	{
-		setcookie("login", "", time()-60*60, "$base/", "", $serverScheme=="https");
+		setcookie("login", "", time()-60*60, "$base/", "", $scheme=="https");
 	}
 	
 	// Send mail to user
-	function sendMail($serverScheme, $serverName, $base, $email, $action)
+	function sendMail($scheme, $address, $base, $email, $action)
 	{
 		if($action=="welcome" || $action=="information")
 		{
-			$url = "$serverScheme://$serverName$base/";
+			$url = "$scheme://$address$base/";
 		} else {
 			$expire = time()+60*60*24;
 			$id = $this->webinterface->users->createRequestId($email, $action, $expire);
-			$url = "$serverScheme://$serverName$base"."/action:$action/email:$email/expire:$expire/id:$id/";
+			$url = "$scheme://$address$base"."/action:$action/email:$email/expire:$expire/id:$id/";
 		}
 		if($action=="approve")
 		{
@@ -1012,7 +1032,7 @@ class YellowResponse
 		$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: $serverScheme://$serverName$base")."\r\n";
+		$mailHeaders .= mb_encode_mimeheader("X-Request-Url: $scheme://$address$base")."\r\n";
 		$mailHeaders .= mb_encode_mimeheader("X-Remote-Addr: $_SERVER[REMOTE_ADDR]")."\r\n";
 		$mailHeaders .= "Mime-Version: 1.0\r\n";
 		$mailHeaders .= "Content-Type: text/plain; charset=utf-8\r\n";
@@ -1274,7 +1294,10 @@ class YellowUsers
 		$data = array();
 		foreach($this->users as $key=>$value)
 		{
-			$data[$key] = "$value[email] password $value[name] $value[language] $value[status]";
+			$name = $value["name"]; if(preg_match("/\s/", $name)) $name = "\"$name\"";
+			$language = $value["language"]; if(preg_match("/\s/", $language)) $language = "\"$language\"";
+			$status = $value["status"]; if(preg_match("/\s/", $status)) $status = "\"$status\"";
+			$data[$key] = "$value[email] - $name $language $status";
 			if($value["home"]!="/") $data[$key] .= " restrictions";
 		}
 		usort($data, strnatcasecmp);

+ 21 - 4
system/themes/flatsite.css → system/themes/assets/flatsite.css

@@ -1,9 +1,26 @@
-/* Theme: Flatsite theme */
-/* Version: 0.6.11 */
-/* Designer: Mark Mayberg */
+/* Flatsite theme, https://github.com/datenstrom/yellow-themes/tree/master/flatsite */
+/* Copyright (c) 2013-2017 Datenstrom, http://datenstrom.se */
+/* This file may be used and distributed under the terms of the public license. */
 
-@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,700);
 html, body, div, form, pre, span, tr, th, td, img { margin:0; padding:0; border:0; vertical-align:baseline; }
+@font-face {
+	font-family:'Open Sans';
+	font-style:normal;
+	font-weight:300;
+	src:url(opensans-light.woff) format('woff');
+}
+@font-face {
+	font-family:'Open Sans';
+	font-style:normal;
+	font-weight:400;
+	src:url(opensans-regular.woff) format('woff');
+}
+@font-face {
+	font-family:'Open Sans';
+	font-style:normal;
+	font-weight:700;
+	src:url(opensans-bold.woff) format('woff');
+}
 body {
 	margin:1em;
 	background-color:#fff; color:#717171;

+ 12 - 0
system/themes/assets/flatsite.php

@@ -0,0 +1,12 @@
+<?php
+// Flatsite theme, https://github.com/datenstrom/yellow-themes/tree/master/flatsite
+// Copyright (c) 2013-2017 Datenstrom, http://datenstrom.se
+// This file may be used and distributed under the terms of the public license.
+
+class YellowThemeFlatsite
+{
+	const VERSION = "0.6.12";	
+}
+
+$yellow->themes->register("flatsite", "YellowThemeFlatsite", YellowThemeFlatsite::VERSION);
+?>

二进制
system/themes/assets/opensans-bold.woff


二进制
system/themes/assets/opensans-light.woff


二进制
system/themes/assets/opensans-regular.woff