Explorar o código

Hello web interface

markseu %!s(int64=12) %!d(string=hai) anos
pai
achega
2474166d27

+ 8 - 4
.htaccess

@@ -4,10 +4,14 @@ RewriteEngine on
 
 
 # RewriteBase /yellow
 # RewriteBase /yellow
 
 
-RewriteRule ^content/(.*) error [R=301,L]
-RewriteRule ^system/(.*) error [R=301,L]
+RewriteCond %{ENV:REDIRECT_STATUS} ^$
+RewriteRule ^(content|system)/ error404 [L]
+RewriteCond %{REQUEST_URI} \.(css|js|png)$
+RewriteRule ^media/plugins/(core_.+) system/core/$1 [L]
+RewriteCond %{REQUEST_URI} \.(css|js|png)$
+RewriteRule ^media/plugins/(.+) system/plugins/$1 [L]
 
 
-RewriteCond %{REQUEST_FILENAME} !-d
 RewriteCond %{REQUEST_FILENAME} !-f
 RewriteCond %{REQUEST_FILENAME} !-f
-RewriteRule ^(.+) index.php [L]
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^ index.php [L]
 </IfModule>
 </IfModule>

+ 13 - 1
README.md

@@ -1,4 +1,16 @@
 Yellow
 Yellow
 ======
 ======
 
 
-Yellow is a CMS for people, it's web-based and flat-file.
+Yellow is a CMS for people, it's web-based and flat-file.
+
+How do I install this?
+----------------------
+[Download Yellow](https://github.com/markseu/yellowcms/archive/master.zip) and unzip it.  
+Copy all files to your web server hosting.  
+Open your website in a browser, that's it!
+
+Installation requirements are Apache, mod_rewrite and PHP 5.3.
+
+Need help? Have a question?
+---------------------------
+Visit [Yellow on Reddit](http://www.reddit.com/r/yellowcms/), follow [Yellow on Twitter](https://twitter.com/yellowcms).

+ 2 - 2
content/1-home/page.txt

@@ -2,6 +2,6 @@
 Title: Home
 Title: Home
 ---
 ---
 Yes, it works! Your Yellow installation was successful.  
 Yes, it works! Your Yellow installation was successful.  
-You can now [edit this page](wiki/) or use your favorite text editor.
+You can now [edit this page](@baselocation/wiki/) or use your favorite text editor.
 
 
-[Visit Yellow on Github](https://github.com/markseu/yellowcms).
+Visit [Yellow on Github](https://github.com/markseu/yellowcms).

+ 6 - 5
system/config/config.ini

@@ -1,5 +1,5 @@
-// Yellow CMS configuration
-// All directories and locations have to end with a slash
+// Yellow site configuration
+// All locations and directories have to end with a slash
 
 
 sitename = Website
 sitename = Website
 author = Website
 author = Website
@@ -10,7 +10,6 @@ template = default
 stylesLocation = /media/styles/
 stylesLocation = /media/styles/
 imagesLocation = /media/images/
 imagesLocation = /media/images/
 pluginsLocation = /media/plugins/
 pluginsLocation = /media/plugins/
-
 systemDir = system/
 systemDir = system/
 configDir = system/config/
 configDir = system/config/
 pluginDir = system/plugins/
 pluginDir = system/plugins/
@@ -18,7 +17,9 @@ snippetDir = system/snippets/
 templateDir = system/templates/
 templateDir = system/templates/
 contentDir = content/
 contentDir = content/
 contentHomeDir = 1-home/
 contentHomeDir = 1-home/
-contentDefaultFile = page
+contentDefaultFile = page.txt
 contentExtension = .txt
 contentExtension = .txt
 configExtension = .ini
 configExtension = .ini
-systemExtension = .php
+systemExtension = .php
+errorPageFile = error(.*).txt
+textStringFile = text_(.*).ini

+ 4 - 0
system/config/error401.txt

@@ -0,0 +1,4 @@
+---
+Title: Unauthorised
+---
+You are not authorised on this server. Please log in.

+ 4 - 0
system/config/error424.txt

@@ -0,0 +1,4 @@
+---
+Title: Page does not exist
+---
+You can [create this page](javascript:showPane('cmspaneeditor');).

+ 19 - 0
system/config/text_english.ini

@@ -0,0 +1,19 @@
+// Yellow text strings
+// Format: key = text string
+
+language = en
+languageDescription = English
+languageAuthor = Mark Seuffert
+
+webinterfaceLoginText		= Yellow login
+webinterfaceLoginEmail		= Email:
+webinterfaceLoginPassword	= Password:
+webinterfaceLoginButton		= Login
+webinterfaceSaveButton		= Save
+webinterfaceCancelButton	= Cancel
+webinterfaceEdit			= Edit
+webinterfaceShow			= Show
+webinterfaceUser			= User
+webinterfaceUserLogout		= Logout
+webinterface424Title		= New page
+webinterface424Text			= Write text here

+ 4 - 0
system/config/user.ini

@@ -0,0 +1,4 @@
+// Yellow user accounts
+// Format: Email, password (sha256 with email prefix as salt), name, language
+
+// user@user.com,e1e2f704225259e91c44fb259c7db595e6cfd1a2e85a2b56508ab5aa9217d265,User,en

+ 772 - 599
system/core/core.php

@@ -5,27 +5,30 @@
 // Yellow main class
 // Yellow main class
 class Yellow
 class Yellow
 {
 {
-    const Version = "0.0.0"; //Hello world!
-    
-	var $page;          //current page data
-	var $pages;         //current page tree, top level
-    var $toolbox;       //toolbox with helpers
- 	var $config;        //site configuration
- 	var $plugins;       //site plugins
+	const Version = "0.1.0";
+	var $page;				//current page data
+	var $pages;				//current page tree, top level
+	var $toolbox;			//toolbox with helpers
+	var $config;			//site configuration
+	var $text;				//site text strings
+	var $plugins;			//site plugins
 
 
 	function __construct()
 	function __construct()
 	{
 	{
-        $this->toolbox = new Yellow_Toolbox();
+		$this->toolbox = new Yellow_Toolbox();
 		$this->config = new Yellow_Config();
 		$this->config = new Yellow_Config();
-        $this->config->setDefault("sitename", "Yellow");
-        $this->config->setDefault("author", "Yellow");
-        $this->config->setDefault("language", "en");
+		$this->text = new Yellow_Text();
+		$this->plugins = new Yellow_Plugins();
+		$this->config->setDefault("sitename", "Yellow");
+		$this->config->setDefault("author", "Yellow");
+		$this->config->setDefault("language", "en");
 		$this->config->setDefault("parser", "markdown");
 		$this->config->setDefault("parser", "markdown");
 		$this->config->setDefault("template", "default");
 		$this->config->setDefault("template", "default");
-        $this->config->setDefault("baseLocation", $this->toolbox->getBaseLocation());
+		$this->config->setDefault("yellowVersion", Yellow::Version);
+		$this->config->setDefault("baseLocation", $this->toolbox->getBaseLocation());
 		$this->config->setDefault("stylesLocation", "/media/styles/");
 		$this->config->setDefault("stylesLocation", "/media/styles/");
 		$this->config->setDefault("imagesLocation", "/media/images/");
 		$this->config->setDefault("imagesLocation", "/media/images/");
-        $this->config->setDefault("pluginsLocation", "media/plugins/");
+		$this->config->setDefault("pluginsLocation", "media/plugins/");
 		$this->config->setDefault("systemDir", "system/");
 		$this->config->setDefault("systemDir", "system/");
 		$this->config->setDefault("configDir", "system/config/");
 		$this->config->setDefault("configDir", "system/config/");
 		$this->config->setDefault("pluginDir", "system/plugins/");
 		$this->config->setDefault("pluginDir", "system/plugins/");
@@ -33,700 +36,870 @@ class Yellow
 		$this->config->setDefault("templateDir", "system/templates/");
 		$this->config->setDefault("templateDir", "system/templates/");
 		$this->config->setDefault("contentDir", "content/");
 		$this->config->setDefault("contentDir", "content/");
 		$this->config->setDefault("contentHomeDir", "1-home/");
 		$this->config->setDefault("contentHomeDir", "1-home/");
-		$this->config->setDefault("contentDefaultFile", "page");
+		$this->config->setDefault("contentDefaultFile", "page.txt");
 		$this->config->setDefault("contentExtension", ".txt");
 		$this->config->setDefault("contentExtension", ".txt");
-        $this->config->setDefault("configExtension", ".ini");
+		$this->config->setDefault("configExtension", ".ini");
 		$this->config->setDefault("systemExtension", ".php");
 		$this->config->setDefault("systemExtension", ".php");
 		$this->config->setDefault("configFile", "config.ini");
 		$this->config->setDefault("configFile", "config.ini");
-        $this->config->setDefault("yellowVersion", Yellow::Version);
-        $this->config->load($this->config->get("configDir").$this->config->get("configFile"));
-        $this->plugins = new Yellow_Plugins();
- 	}
+		$this->config->setDefault("errorPageFile", "error(.*).txt");
+		$this->config->setDefault("textStringFile", "text_(.*).ini");
+		$this->config->load($this->config->get("configDir").$this->config->get("configFile"));
+		$this->text->load($this->config->get("configDir").$this->config->get("textStringFile"), $this->toolbox);
+	}
 
 
-    // Start CMS and handle request
-    function request()
-    {
-        $this->toolbox->timerStart($time);
-        $this->plugins->load();
-        $this->processRequest();
-        $this->toolbox->timerStop($time);
-        if(defined("DEBUG") && DEBUG>=1) echo "Yellow::request time:$time ms<br>\n";
-    }
+	// Start and handle request
+	function request()
+	{
+		$this->toolbox->timerStart($time);
+		$this->plugins->load();
+		$this->processRequest();
+		$this->toolbox->timerStop($time);
+		if(defined("DEBUG") && DEBUG>=1) echo "Yellow::request time:$time ms<br>\n";
+	}
 
 
-    // Process request
+	// Process request
 	function processRequest()
 	function processRequest()
 	{
 	{
-        $statusCode = 0;
-        $baseLocation = $this->config->get("baseLocation");
-        $location = $this->getRelativeLocation($baseLocation);
+		$statusCode = 0;
+		$baseLocation = $this->config->get("baseLocation");
+		$location = $this->getRelativeLocation($baseLocation);
 		$fileName = $this->getContentFileName($location);
 		$fileName = $this->getContentFileName($location);
- 		foreach($this->plugins->plugins as $key=>$value)
-        {
+		foreach($this->plugins->plugins as $key=>$value)
+		{
 			if(method_exists($value["obj"], "onRequest"))
 			if(method_exists($value["obj"], "onRequest"))
-            {
+			{
 				$statusCode = $value["obj"]->onRequest($baseLocation, $location, $fileName);
 				$statusCode = $value["obj"]->onRequest($baseLocation, $location, $fileName);
 				if($statusCode) break;
 				if($statusCode) break;
-            }
-        }
-        if($statusCode == 0) $statusCode = $this->processRequestFile($baseLocation, $location, $fileName, $statusCode);
-        if(defined("DEBUG") && DEBUG>=1) echo "Yellow::processRequest status:$statusCode, location:$location<br>\n";
-    }
+			}
+		}
+		if($statusCode == 0) $statusCode = $this->processRequestFile($baseLocation, $location, $fileName, $statusCode);
+		if(defined("DEBUG") && DEBUG>=1) echo "Yellow::processRequest status:$statusCode location:$location<br>\n";
+	}
 
 
-    // Process request for a file
-    function processRequestFile($baseLocation, $location, $fileName, $statusCode, $cache = true)
-    {
-        if($statusCode == 0)
-        {
-            if(is_readable($fileName))
-            {
-                $time = gmdate("D, d M Y H:i:s", filemtime($fileName))." GMT";
-                if(isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && $_SERVER["HTTP_IF_MODIFIED_SINCE"]==$time && $cache)
-                {
-                    $statusCode = 304;
-                    $this->sendStatus($statusCode);
-                } else {
-                    $statusCode = 200;
-                    header("Content-Type: text/html; charset=UTF-8");
-                    if($cache)
-                    {
-                        header("Last-Modified: ".$time);
-                    } else {
-                        header("Cache-Control: no-cache");
-                        header("Pragma: no-cache");
-                        header("Expires: 0");
-                    }
-                    $fileHandle = @fopen($fileName, "r");
-                    if($fileHandle)
-                    {
-                        $fileData = fread($fileHandle, filesize($fileName));
-                        fclose($fileHandle);
-                    } else {
-                        die("Server problem: Can't read file '$fileName'!");
-                    }
-                }
-            } else {
-                if($this->toolbox->isFileLocation($location) && is_dir($this->getContentDirectory("$location/")))
-                {
-                    $statusCode = 301;
-                    $this->sendStatus($statusCode, "Location: http://$_SERVER[SERVER_NAME]$baseLocation$location/");
-                } else {
-                    $statusCode = 404;
-                }
-            }
-        }
-        if($statusCode >= 400)
-        {
-            header($this->toolbox->getHttpStatusFormated($statusCode));
-            header("Content-Type: text/html; charset=UTF-8");
-            $fileName = $this->config->get("configDir").sprintf("error%d.txt", $statusCode);
-            $fileHandle = @fopen($fileName, "r");
-            if($fileHandle)
-            {
-                $fileData = fread($fileHandle, filesize($fileName));
-                fclose($fileHandle);
-            } else {
-                die("Configuration problem: Can't open file '$fileName'!");
-            }
-        }
-        if($fileData != "") $this->sendPage($baseLocation, $location, $fileName, $fileData, $statusCode);
-        if(defined("DEBUG") && DEBUG>=1) echo "Yellow::processRequestFile base:$baseLocation, file:$fileName<br>\n";
-        return $statusCode;
-    }
-    
-    // Send status response
-    function sendStatus($statusCode, $text = "")
-    {
-        header($this->toolbox->getHttpStatusFormated($statusCode));
-        if($text != "") header($text);
-    }
-    
-    // Send page response
-    function sendPage($baseLocation, $location, $fileName, $fileData, $statusCode)
-    {
-        $this->pages = new Yellow_Pages($baseLocation, $this->toolbox, $this->config);
+	// Process request for a file
+	function processRequestFile($baseLocation, $location, $fileName, $statusCode, $cache = true)
+	{
+		if($statusCode == 0)
+		{
+			if(is_readable($fileName))
+			{
+				$time = gmdate("D, d M Y H:i:s", filemtime($fileName))." GMT";
+				if(isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && $_SERVER["HTTP_IF_MODIFIED_SINCE"]==$time && $cache)
+				{
+					$statusCode = 304;
+					$this->sendStatus($statusCode);
+				} else {
+					$statusCode = 200;
+					header("Content-Type: text/html; charset=UTF-8");
+					if($cache)
+					{
+						header("Last-Modified: ".$time);
+					} else {
+						header("Cache-Control: no-cache");
+						header("Pragma: no-cache");
+						header("Expires: 0");
+					}
+					$fileHandle = @fopen($fileName, "r");
+					if($fileHandle)
+					{
+						$fileData = fread($fileHandle, filesize($fileName));
+						fclose($fileHandle);
+					} else {
+						die("Server problem: Can't read file '$fileName'!");
+					}
+				}
+			} else {
+				if($this->toolbox->isFileLocation($location) && is_dir($this->getContentDirectory("$location/")))
+				{
+					$statusCode = 301;
+					$this->sendStatus($statusCode, "Location: http://$_SERVER[SERVER_NAME]$baseLocation$location/");
+				} else {
+					$statusCode = 404;
+				}
+			}
+		}
+		if($statusCode >= 400)
+		{
+			header($this->toolbox->getHttpStatusFormated($statusCode));
+			header("Content-Type: text/html; charset=UTF-8");
+			$fileName = str_replace("(.*)", $statusCode, $this->config->get("configDir").$this->config->get("errorPageFile"));
+			$fileHandle = @fopen($fileName, "r");
+			if($fileHandle)
+			{
+				$fileData = fread($fileHandle, filesize($fileName));
+				fclose($fileHandle);
+			} else {
+				die("Configuration problem: Can't open file '$fileName'!");
+			}
+		}
+		if($fileData != "") $this->sendPage($baseLocation, $location, $fileName, $fileData, $statusCode);
+		if(defined("DEBUG") && DEBUG>=1) echo "Yellow::processRequestFile base:$baseLocation file:$fileName<br>\n";
+		return $statusCode;
+	}
+	
+	// Send status response
+	function sendStatus($statusCode, $text = "")
+	{
+		header($this->toolbox->getHttpStatusFormated($statusCode));
+		if($text != "") header($text);
+	}
+	
+	// Send page response
+	function sendPage($baseLocation, $location, $fileName, $fileData, $statusCode)
+	{
+		$this->pages = new Yellow_Pages($baseLocation, $this->toolbox, $this->config);
 		$this->page = new Yellow_Page($baseLocation, $location, $fileName, $fileData, $this->toolbox, $this->config);
 		$this->page = new Yellow_Page($baseLocation, $location, $fileName, $fileData, $this->toolbox, $this->config);
-        
-        $text = $this->page->getContentRawText();
+		$this->text->setLanguage($this->page->get("language"));
+		
+		$text = $this->page->getContentRawText();
 		foreach($this->plugins->plugins as $key=>$value)
 		foreach($this->plugins->plugins as $key=>$value)
-        {
+		{
 			if(method_exists($value["obj"], "onParseBefore"))
 			if(method_exists($value["obj"], "onParseBefore"))
-            {
+			{
 				$text = $value["obj"]->onParseBefore($text, $statusCode);
 				$text = $value["obj"]->onParseBefore($text, $statusCode);
-            }
-        }
-        if(is_null($this->plugins->plugins[$this->page->get("parser")])) die("Parser '".$this->page->get("parser")."' does not exist!");
+			}
+		}
+		if(!$this->plugins->isExisting($this->page->get("parser"))) die("Parser '".$this->page->get("parser")."' does not exist!");
 		$this->page->parser = $this->plugins->plugins[$this->page->get("parser")]["obj"];
 		$this->page->parser = $this->plugins->plugins[$this->page->get("parser")]["obj"];
 		$text = $this->page->parser->parse($text);
 		$text = $this->page->parser->parse($text);
 		foreach($this->plugins->plugins as $key=>$value)
 		foreach($this->plugins->plugins as $key=>$value)
-        {
+		{
 			if(method_exists($value["obj"], "onParseAfter"))
 			if(method_exists($value["obj"], "onParseAfter"))
-            {
+			{
 				$text = $value["obj"]->onParseAfter($text, $statusCode);
 				$text = $value["obj"]->onParseAfter($text, $statusCode);
-            }
-        }
-        $this->page->setContent($text);
+			}
+		}
+		$this->page->setContent($text);
 
 
 		if(!$this->page->isExisting("description"))
 		if(!$this->page->isExisting("description"))
-        {
-            $this->page->set("description", $this->toolbox->createTextDescription($this->page->getContent(), 150));
+		{
+			$this->page->set("description", $this->toolbox->createTextDescription($this->page->getContent(), 150));
+		}
+		if(!$this->page->isExisting("keywords"))
+		{
+			$this->page->set("keywords", $this->toolbox->createTextKeywords($this->page->get("title"), 10));
 		}
 		}
-        if(!$this->page->isExisting("keywords"))
-        {
-            $this->page->set("keywords", $this->toolbox->createTextKeywords($this->page->get("title"), 10));
-        }
-        
-        $fileName = $this->config->get("templateDir").$this->page->get("template").$this->config->get("systemExtension");
-        if(!is_file($fileName)) die("Template '".$this->page->get("template")."' does not exist!");
+		
+		$fileName = $this->config->get("templateDir").$this->page->get("template").$this->config->get("systemExtension");
+		if(!is_file($fileName)) die("Template '".$this->page->get("template")."' does not exist!");
 		global $yellow;
 		global $yellow;
-        require($fileName);
+		require($fileName);
 	}
 	}
 
 
-    // Execute a template snippet
+	// Execute a template snippet
 	function snippet($snippet)
 	function snippet($snippet)
 	{
 	{
-        $fileName = $this->config->get("snippetDir").$snippet.$this->config->get("systemExtension");
-        if(!is_file($fileName)) die("Snippet '$snippet' does not exist!");
+		$fileName = $this->config->get("snippetDir").$snippet.$this->config->get("systemExtension");
+		if(!is_file($fileName)) die("Snippet '$snippet' does not exist!");
 		global $yellow;
 		global $yellow;
 		require($fileName);
 		require($fileName);
 	}
 	}
-    
-    // Return extra HTML header lines generated from plugins
+	
+	// Return extra HTML header lines generated from plugins
 	function getHeaderExtra()
 	function getHeaderExtra()
 	{
 	{
-        $header = "";
+		$header = "";
 		foreach($this->plugins->plugins as $key=>$value)
 		foreach($this->plugins->plugins as $key=>$value)
-        {
+		{
 			if(method_exists($value["obj"], "onHeaderExtra")) $header .= $value["obj"]->onHeaderExtra();
 			if(method_exists($value["obj"], "onHeaderExtra")) $header .= $value["obj"]->onHeaderExtra();
-        }
-        return $header;
-	}
-    
-    // Return content location for current HTTP request, without base location
-    function getRelativeLocation($baseLocation)
-    {
-        $location = $this->toolbox->getRequestLocation();
-        $location = $this->toolbox->normaliseLocation($location);
-        $position = strlen($baseLocation);
-        return substr($location, $position);
-    }
+		}
+		return $header;
+	}
+	
+	// Return content location for current HTTP request, without base location
+	function getRelativeLocation($baseLocation)
+	{
+		$location = $this->toolbox->getRequestLocation();
+		$location = $this->toolbox->normaliseLocation($location);
+		$position = strlen($baseLocation);
+		return substr($location, $position);
+	}
 
 
 	// Return content file name from location
 	// Return content file name from location
 	function getContentFileName($location)
 	function getContentFileName($location)
 	{
 	{
-        return $this->toolbox->findFileFromLocation($location,
-            $this->config->get("contentDir"), $this->config->get("contentHomeDir"),
-            $this->config->get("contentDefaultFile"), $this->config->get("contentExtension"));
+		return $this->toolbox->findFileFromLocation($location,
+					$this->config->get("contentDir"), $this->config->get("contentHomeDir"),
+					$this->config->get("contentDefaultFile"), $this->config->get("contentExtension"));
 	}
 	}
-    
+	
 	// Return content directory from location
 	// Return content directory from location
 	function getContentDirectory($location)
 	function getContentDirectory($location)
 	{
 	{
-        return $this->toolbox->findFileFromLocation($location,
-            $this->config->get("contentDir"), $this->config->get("contentHomeDir"), "", "");
+		return $this->toolbox->findFileFromLocation($location,
+					$this->config->get("contentDir"), $this->config->get("contentHomeDir"), "", "");
 	}
 	}
-    
-    // Register plugin
+	
+	// Register plugin
 	function registerPlugin($name, $class, $version)
 	function registerPlugin($name, $class, $version)
 	{
 	{
-        $this->plugins->register($name, $class, $version);
+		$this->plugins->register($name, $class, $version);
 	}
 	}
 }
 }
 
 
 // Yellow page data
 // Yellow page data
 class Yellow_Page
 class Yellow_Page
 {
 {
-    var $baseLocation;      //base location
-    var $location;          //page location
-    var $fileName;          //content file name
-    var $parser;            //content parser
-	var $metaData;          //meta data of page
-	var $rawData;           //raw data of page (unparsed)
-    var $rawTextPos;        //raw text of page (unparsed)
-    var $active;            //page is active?
+	var $baseLocation;		//base location
+	var $location;			//page location
+	var $fileName;			//content file name
+	var $parser;			//content parser
+	var $metaData;			//meta data of page
+	var $rawData;			//raw data of page (unparsed)
+	var $rawTextPos;		//raw text of page (unparsed)
+	var $active;			//page is active?
+	var $hidden;			//page is hidden?
 
 
-    function __construct($baseLocation, $location, $fileName, $rawData, $toolbox, $config)
-    {
-        $this->baseLocation = $baseLocation;
-        $this->location = $location;
-        $this->fileName = $fileName;
-        $this->setRawData($rawData, $toolbox, $config);
-        $this->active = $toolbox->isActiveLocation($baseLocation, $location);
-    }
-    
-    // Set page raw data
+	function __construct($baseLocation, $location, $fileName, $rawData, $toolbox, $config)
+	{
+		$this->baseLocation = $baseLocation;
+		$this->location = $location;
+		$this->fileName = $fileName;
+		$this->setRawData($rawData, $toolbox, $config);
+		$this->active = $toolbox->isActiveLocation($baseLocation, $location);
+		$this->hidden = $toolbox->isHiddenLocation($baseLocation, $location, $fileName, $config->get("contentDir"));
+	}
+	
+	// Set page raw data
 	function setRawData($rawData, $toolbox, $config)
 	function setRawData($rawData, $toolbox, $config)
 	{
 	{
-        $this->metaData = array();
-        $this->rawData = $rawData;
-        $this->rawTextPos = 0;
-        $this->set("title", $toolbox->createTextTitle($this->location));
+		$this->metaData = array();
+		$this->rawData = $rawData;
+		$this->rawTextPos = 0;
+		$this->set("title", $toolbox->createTextTitle($this->location));
 		$this->set("author", $config->get("author"));
 		$this->set("author", $config->get("author"));
-        $this->set("language", $config->get("language"));
+		$this->set("language", $config->get("language"));
 		$this->set("parser", $config->get("parser"));
 		$this->set("parser", $config->get("parser"));
 		$this->set("template", $config->get("template"));
 		$this->set("template", $config->get("template"));
-        
+		
 		if(preg_match("/^(\-\-\-[\r\n]+)(.+?)([\r\n]+\-\-\-[\r\n]+)/s", $rawData, $parsed))
 		if(preg_match("/^(\-\-\-[\r\n]+)(.+?)([\r\n]+\-\-\-[\r\n]+)/s", $rawData, $parsed))
-        {
-            $this->rawTextPos = strlen($parsed[1]) + strlen($parsed[2]) + strlen($parsed[3]);
-            preg_match_all("/([^\:\r\n]+)\s*\:\s*([^\r\n]+)/s", $parsed[2], $matches, PREG_SET_ORDER);
-            foreach($matches as $match)
-            {
-                $this->set(strtolower($match[1]), $match[2]);
-            }
-        } else if(preg_match("/^([^\r\n]+)([\r\n]+=+[\r\n]+)/", $rawData, $parsed)) {
-            $this->rawTextPos = strlen($parsed[1]) + strlen($parsed[2]);
-            $this->set("title", $parsed[1]);
-        }
+		{
+			$this->rawTextPos = strlen($parsed[1]) + strlen($parsed[2]) + strlen($parsed[3]);
+			preg_match_all("/([^\:\r\n]+)\s*\:\s*([^\r\n]+)/s", $parsed[2], $matches, PREG_SET_ORDER);
+			foreach($matches as $match)
+			{
+				$this->set(strtolower($match[1]), $match[2]);
+			}
+		} else if(preg_match("/^([^\r\n]+)([\r\n]+=+[\r\n]+)/", $rawData, $parsed)) {
+			$this->rawTextPos = strlen($parsed[1]) + strlen($parsed[2]);
+			$this->set("title", $parsed[1]);
+		}
 	}
 	}
 
 
-    // Set page meta data
+	// Set page meta data
 	function set($key, $value)
 	function set($key, $value)
 	{
 	{
 		$this->metaData[$key] = $value;
 		$this->metaData[$key] = $value;
 	}
 	}
 
 
-    // Return page meta data
+	// Return page meta data
 	function get($key)
 	function get($key)
 	{
 	{
-		return is_null($this->metaData[$key]) ? "" : $this->metaData[$key];
+		return $this->IsExisting($key) ? $this->metaData[$key] : "";
 	}
 	}
 
 
-    // Return page meta data, HTML encoded
+	// Return page meta data, HTML encoded
 	function getHtml($key)
 	function getHtml($key)
 	{
 	{
 		return htmlspecialchars($this->get($key));
 		return htmlspecialchars($this->get($key));
 	}
 	}
 
 
-    // Return page title, HTML encoded
-    function getTitle()
-    {
-        return $this->getHtml("title");
-    }
-    
-    // Set page content, HTML encoded
-    function setContent($html)
-    {
-        $this->parser->html = $html;
-    }
-    
-    // Return page content, HTML encoded
-    function getContent()
-    {
-        return $this->parser->html;
-    }
-    
-    // Return page content, raw text
-    function getContentRawText()
-    {
-        return substr($this->rawData, $this->rawTextPos);
-    }
-    
-    // Return absolut page location
-    function getLocation()
-    {
-        return $this->baseLocation.$this->location;
-    }
-    
-    // Check if meta data exists
-    function isExisting($key)
-    {
-        return !is_null($this->metaData[$key]);
-    }
-    
-    // Check if page is active
-    function isActive()
-    {
-        return $this->active;
-    }
+	// Return page title, HTML encoded
+	function getTitle()
+	{
+		return $this->getHtml("title");
+	}
+	
+	// Set page content, HTML encoded
+	function setContent($html)
+	{
+		$this->parser->html = $html;
+	}
+	
+	// Return page content, HTML encoded
+	function getContent()
+	{
+		return $this->parser->html;
+	}
+	
+	// Return page content, raw text
+	function getContentRawText()
+	{
+		return substr($this->rawData, $this->rawTextPos);
+	}
+	
+	// Return absolut page location
+	function getLocation()
+	{
+		return $this->baseLocation.$this->location;
+	}
+	
+	// Check if meta data exists
+	function isExisting($key)
+	{
+		return !is_null($this->metaData[$key]);
+	}
+	
+	// Check if page is active
+	function isActive()
+	{
+		return $this->active;
+	}
+
+	// Check if page is active
+	function isHidden()
+	{
+		return $this->hidden;
+	}
 }
 }
 
 
 // Yellow page tree from file system
 // Yellow page tree from file system
 class Yellow_Pages
 class Yellow_Pages
 {
 {
-    var $pages;     //scanned pages
-    
-    function __construct($baseLocation, $toolbox, $config)
-	{
-        $this->scan($baseLocation, $toolbox, $config);
-    }
-    
-    // Scan top-level pages
-    function scan($baseLocation, $toolbox, $config)
-    {
-        $this->pages = array();
-        foreach($toolbox->getDirectoryEntries($config->get("contentDir"), "/^[\d\-\.]+(.*)$/", true) as $entry)
-        {
-            $fileName = $config->get("contentDir").$entry."/".$config->get("contentDefaultFile").$config->get("contentExtension");
-            $location = $toolbox->findLocationFromFile($fileName, $config->get("contentDir"), $config->get("contentHomeDir"),
-                                                       $config->get("contentDefaultFile"), $config->get("contentExtension"));
-            $fileHandle = @fopen($fileName, "r");
-            if($fileHandle)
-            {
-                $fileData = fread($fileHandle, 4096);
-                fclose($fileHandle);
-            } else {
-                $fileData = "";
-            }
-            $page = new Yellow_Page($baseLocation, $location, $fileName, $fileData, $toolbox, $config);
-            array_push($this->pages, $page);
-        }
-    }
-    
-    // Return top-level pages
-    function root()
-    {
-        return $this->pages;
-    }
+	var $pages;		//scanned pages
+	
+	function __construct($baseLocation, $toolbox, $config)
+	{
+		$this->scan($baseLocation, $toolbox, $config);
+	}
+	
+	// Scan top-level pages
+	function scan($baseLocation, $toolbox, $config)
+	{
+		$this->pages = array();
+		foreach($toolbox->getDirectoryEntries($config->get("contentDir"), "/.*/", true) as $entry)
+		{
+			$fileName = $config->get("contentDir").$entry."/".$config->get("contentDefaultFile");
+			$location = $toolbox->findLocationFromFile($fileName, $config->get("contentDir"), $config->get("contentHomeDir"),
+													   $config->get("contentDefaultFile"), $config->get("contentExtension"));
+			$fileHandle = @fopen($fileName, "r");
+			if($fileHandle)
+			{
+				$fileData = fread($fileHandle, 4096);
+				fclose($fileHandle);
+			} else {
+				$fileData = "";
+			}
+			$page = new Yellow_Page($baseLocation, $location, $fileName, $fileData, $toolbox, $config);
+			array_push($this->pages, $page);
+		}
+	}
+	
+	// Return top-level pages
+	function root($showHidden = false)
+	{
+		$pages = array();
+		foreach($this->pages as $page)
+		{
+			if($showHidden || !$page->isHidden()) array_push($pages, $page);
+		}
+		return $pages;
+	}
 }
 }
-    
+	
 // Yellow toolbox with helpers
 // Yellow toolbox with helpers
 class Yellow_Toolbox
 class Yellow_Toolbox
 {
 {
-    // Return location from current HTTP request
-    static function getRequestLocation()
-    {
-        $uri = $_SERVER["REQUEST_URI"];
-        return ($pos = strpos($uri, '?')) ? substr($uri, 0, $pos) : $uri;
-    }
+	// Return location from current HTTP request
+	static function getRequestLocation()
+	{
+		$uri = $_SERVER["REQUEST_URI"];
+		return ($pos = strpos($uri, '?')) ? substr($uri, 0, $pos) : $uri;
+	}
 
 
-    // Return arguments from current HTTP request
-    static function getRequestLocationArguments()
-    {
-        $uri = $_SERVER["REQUEST_URI"];
-        return ($pos = strpos($uri, '?')) ? substr($uri, $pos+1) : "";
-    }
-    
-    // Return base location
-    static function getBaseLocation()
-    {
-        $baseLocation = "/";
-        if(preg_match("/^(.*)\//", $_SERVER["SCRIPT_NAME"], $matches)) $baseLocation = $matches[1];
-        return $baseLocation;
-    }
-    
-    // Normalise location and remove incorrect path tokens
-    static function normaliseLocation($location)
-    {
-        $str = str_replace('\\', '/', rawurldecode($location));
-        $location = ($str[0]=='/') ? '' : '/';
-        for($pos=0; $pos<strlen($str); ++$pos)
-        {
-            if($str[$pos] == '/')
-            {
-                if($str[$pos+1] == '/') continue;
-                if($str[$pos+1] == '.')
-                {
-                    $posNew = $pos+1; while($str[$posNew] == '.') ++$posNew;
-                    if($str[$posNew]=='/' || $str[$posNew]=='')
-                    {
-                        $pos = $posNew-1; 
-                        continue;
-                    }
-                }
-            }
-            $location .= $str[$pos];
-        }
-        return $location;
-    }
-    
-    // Check if location is specifying file or directory
-    static function isFileLocation($location)
-    {
-        return substr($location,-1,1) != "/";
-    }
-    
-    // Check if location is within current HTTP request
-    static function isActiveLocation($baseLocation, $location)
-    {
-        $currentLocation = substr(self::getRequestLocation(), strlen($baseLocation));
-        if($location != "/")
-        {
-            $active = substr($currentLocation, 0, strlen($location))==$location;
-        } else {
-            $active = $currentLocation==$location;
-        }
-        return $active;
-    }
-    
-    // Find file path from location
-    static function findFileFromLocation($location, $pathBase, $pathHome, $fileDefault, $fileExtension)
-    {
-        $path = $pathBase;
-        if($location != "/")
-        {
-            $tokens = explode('/', $location);
-            for($i=1; $i<count($tokens)-1; ++$i)
-            {
-                $entries = self::getDirectoryEntries($path, "/^[\d\-\.]+".$tokens[$i]."$/");
-                if(!empty($entries)) $tokens[$i] = $entries[0];
-                $path .= "$tokens[$i]/";
-            }
-            if($tokens[$i] != "")
-            {
-                $path .= $tokens[$i].$fileExtension;
-            } else {
-                $path .= $fileDefault.$fileExtension;
-            }
-        } else {
-            $path .= $pathHome.$fileDefault.$fileExtension;
-        }
-        return $path;
-    }
-    
-    // Find location from file path
-    static function findLocationFromFile($fileName, $pathBase, $pathHome, $fileDefault, $fileExtension)
-    {
-        $location = "/";
-        if(substr($fileName, 0, strlen($pathBase)) == $pathBase) $fileName = substr($fileName, strlen($pathBase));
-        if(substr($fileName, 0, strlen($pathHome)) != $pathHome)
-        {
-            $tokens = explode('/', $fileName);
-            for($i=0; $i<count($tokens)-1; ++$i)
-            {
-                if(preg_match("/^[\d\-\.]+(.*)$/", $tokens[$i], $matches)) $tokens[$i] = $matches[1];
-                $location .= "$tokens[$i]/";
-            }
-            if($tokens[$i] != $fileDefault.$fileExtension)
-            {
-                $location .= substr($tokens[$i], 0, -strlen($fileExtension)-1);
-            }
-        } else {
-            if($fileName != $pathHome.$fileDefault.$fileExtension)
-            {
-                $location .= substr($fileName, $pathHome, -strlen($fileExtension)-1);
-            }
-        }
-        return $location;
-    }
-    
-    // Return human readable HTTP server status
-    static function getHttpStatusFormated($statusCode)
-    {
-        switch($statusCode)
-        {
-            case 301: $text = "$_SERVER[SERVER_PROTOCOL] 301 Moved permanently"; break;
-            case 302: $text = "$_SERVER[SERVER_PROTOCOL] 302 Moved temporarily"; break;
-            case 304: $text = "$_SERVER[SERVER_PROTOCOL] 304 Not modified"; break;
-            case 404: $text = "$_SERVER[SERVER_PROTOCOL] 404 Not found"; break;
-            case 424: $text = "$_SERVER[SERVER_PROTOCOL] 424 Does not exist"; break;
-            default: die("Unknown HTTP status $statusCode!");
-        }
-        return $text;
-    }
-    
-    // Return files and directories
-    static function getDirectoryEntries($path, $regex = ".*", $sort = false, $directories = true)
-    {
-        $entries = array();
-        $dirHandle = @opendir($path);
-        if($dirHandle)
-        {
-            while(($entry = readdir($dirHandle)) !== false)
-            {
-                if(substr($entry, 0, 1) == ".") continue;
-                if(preg_match($regex, $entry))
-                {
-                    if($directories)
-                    {
-                        if(is_dir("$path/$entry")) array_push($entries, $entry);
-                    } else {
-                        if(is_file("$path/$entry")) array_push($entries, $entry);
-                    }
-                }
-            }
-            if($sort) natsort($entries);
-            closedir($dirHandle);
-        }
-        return $entries;
-    }
+	// Return arguments from current HTTP request
+	static function getRequestLocationArguments()
+	{
+		$uri = $_SERVER["REQUEST_URI"];
+		return ($pos = strpos($uri, '?')) ? substr($uri, $pos+1) : "";
+	}
+	
+	// Return base location
+	static function getBaseLocation()
+	{
+		$baseLocation = "/";
+		if(preg_match("/^(.*)\//", $_SERVER["SCRIPT_NAME"], $matches)) $baseLocation = $matches[1];
+		return $baseLocation;
+	}
+	
+	// Normalise location and remove incorrect path tokens
+	static function normaliseLocation($location)
+	{
+		$str = str_replace('\\', '/', rawurldecode($location));
+		$location = ($str[0]=='/') ? '' : '/';
+		for($pos=0; $pos<strlen($str); ++$pos)
+		{
+			if($str[$pos] == '/')
+			{
+				if($str[$pos+1] == '/') continue;
+				if($str[$pos+1] == '.')
+				{
+					$posNew = $pos+1; while($str[$posNew] == '.') ++$posNew;
+					if($str[$posNew]=='/' || $str[$posNew]=='')
+					{
+						$pos = $posNew-1; 
+						continue;
+					}
+				}
+			}
+			$location .= $str[$pos];
+		}
+		return $location;
+	}
+	
+	// Check if location is specifying file or directory
+	static function isFileLocation($location)
+	{
+		return substr($location,-1,1) != "/";
+	}
+	
+	// Check if location is within current HTTP request
+	static function isActiveLocation($baseLocation, $location)
+	{
+		$currentLocation = substr(self::getRequestLocation(), strlen($baseLocation));
+		if($location != "/")
+		{
+			$active = substr($currentLocation, 0, strlen($location))==$location;
+		} else {
+			$active = $currentLocation==$location;
+		}
+		return $active;
+	}
+	
+	// Check if location is within visible collection
+	static function isHiddenLocation($baseLocation, $location, $fileName, $pathBase)
+	{
+		$hidden = false;
+		if(substr($fileName, 0, strlen($pathBase)) == $pathBase) $fileName = substr($fileName, strlen($pathBase));
+		$tokens = explode('/', $fileName);
+		for($i=0; $i<count($tokens)-1; ++$i)
+		{
+			if(!preg_match("/^[\d\-\.]+(.*)$/", $tokens[$i]))
+			{
+				$hidden = true;
+				break;
+			}
+		}
+		return $hidden;
+	}
+	
+	// Find file path from location
+	static function findFileFromLocation($location, $pathBase, $pathHome, $fileDefault, $fileExtension)
+	{
+		$path = $pathBase;
+		if($location != "/")
+		{
+			$tokens = explode('/', $location);
+			for($i=1; $i<count($tokens)-1; ++$i)
+			{
+				$entries = self::getDirectoryEntries($path, "/^[\d\-\.]+".$tokens[$i]."$/");
+				if(!empty($entries)) $tokens[$i] = $entries[0];
+				$path .= "$tokens[$i]/";
+			}
+			if($tokens[$i] != "")
+			{
+				$path .= $tokens[$i].$fileExtension;
+			} else {
+				$path .= $fileDefault;
+			}
+		} else {
+			$path .= $pathHome.$fileDefault;
+		}
+		return $path;
+	}
+	
+	// Find location from file path
+	static function findLocationFromFile($fileName, $pathBase, $pathHome, $fileDefault, $fileExtension)
+	{
+		$location = "/";
+		if(substr($fileName, 0, strlen($pathBase)) == $pathBase) $fileName = substr($fileName, strlen($pathBase));
+		if(substr($fileName, 0, strlen($pathHome)) != $pathHome)
+		{
+			$tokens = explode('/', $fileName);
+			for($i=0; $i<count($tokens)-1; ++$i)
+			{
+				if(preg_match("/^[\d\-\.]+(.*)$/", $tokens[$i], $matches)) $tokens[$i] = $matches[1];
+				$location .= "$tokens[$i]/";
+			}
+			if($tokens[$i] != $fileDefault)
+			{
+				$location .= substr($tokens[$i], 0, -strlen($fileExtension)-1);
+			}
+		} else {
+			if($fileName != $pathHome.$fileDefault)
+			{
+				$location .= substr($fileName, $pathHome, -strlen($fileExtension)-1);
+			}
+		}
+		return $location;
+	}
+	
+	// Return human readable HTTP server status
+	static function getHttpStatusFormated($statusCode)
+	{
+		switch($statusCode)
+		{
+			case 301: $text = "$_SERVER[SERVER_PROTOCOL] $statusCode Moved permanently"; break;
+			case 302: $text = "$_SERVER[SERVER_PROTOCOL] $statusCode Moved temporarily"; break;
+			case 304: $text = "$_SERVER[SERVER_PROTOCOL] $statusCode Not modified"; break;
+			case 401: $text = "$_SERVER[SERVER_PROTOCOL] $statusCode Unauthorised"; break;
+			case 404: $text = "$_SERVER[SERVER_PROTOCOL] $statusCode Not found"; break;
+			case 424: $text = "$_SERVER[SERVER_PROTOCOL] $statusCode Does not exist"; break;
+			default: die("Unknown HTTP status $statusCode!");
+		}
+		return $text;
+	}
+	
+	// Return files and directories
+	static function getDirectoryEntries($path, $regex = "/.*/", $sort = false, $directories = true)
+	{
+		$entries = array();
+		$dirHandle = @opendir($path);
+		if($dirHandle)
+		{
+			while(($entry = readdir($dirHandle)) !== false)
+			{
+				if(substr($entry, 0, 1) == ".") continue;
+				if(preg_match($regex, $entry))
+				{
+					if($directories)
+					{
+						if(is_dir("$path/$entry")) array_push($entries, $entry);
+					} else {
+						if(is_file("$path/$entry")) array_push($entries, $entry);
+					}
+				}
+			}
+			if($sort) natsort($entries);
+			closedir($dirHandle);
+		}
+		return $entries;
+	}
 
 
-    // Create description from text
-    static function createTextDescription($text, $lengthMax)
-    {
-        $description = "";
-        preg_match_all("/\<p\>(.+?\<\/p\>)/s", $text, $parsedDescription, PREG_SET_ORDER);
-        foreach($parsedDescription as $matches)
-        {
-            preg_match_all("/([^\<]*)[^\>]+./s", $matches[1], $parsedUndoTag, PREG_SET_ORDER);
-            if(count($parsedUndoTag) > 0)
-            {
-                if(!empty($description)) $description .= " ";
-                foreach($parsedUndoTag as $matchTag)
-                {
-                    $description .= preg_replace("/[\\x00-\\x1f]+/s", " ", $matchTag[1]);
-                }
-            }
-            if(strlen($description) > $lengthMax)
-            {
-                $description = substr($description, 0, $lengthMax-3)."...";
-                break;
-            }
-        }
-        return $description;
-    }
+	// Create description from text
+	static function createTextDescription($text, $lengthMax)
+	{
+		$description = "";
+		preg_match_all("/\<p\>(.+?\<\/p\>)/s", $text, $parsedDescription, PREG_SET_ORDER);
+		foreach($parsedDescription as $matches)
+		{
+			preg_match_all("/([^\<]*)[^\>]+./s", $matches[1], $parsedUndoTag, PREG_SET_ORDER);
+			if(count($parsedUndoTag) > 0)
+			{
+				if(!empty($description)) $description .= " ";
+				foreach($parsedUndoTag as $matchTag)
+				{
+					$description .= preg_replace("/[\\x00-\\x1f]+/s", " ", $matchTag[1]);
+				}
+			}
+			if(strlen($description) > $lengthMax)
+			{
+				$description = substr($description, 0, $lengthMax-3)."...";
+				break;
+			}
+		}
+		return $description;
+	}
 
 
-    // Create keywords from text string
-    static function createTextKeywords($text, $keywordsMax)
-    {
-        $tokens = preg_split("/[,\s\(\)]/", strtolower($text));
-        foreach($tokens as $key => $value) if(strlen($value) < 3) unset($tokens[$key]);
-        return implode(", ", array_slice(array_unique($tokens), 0, $keywordsMax));
-    }
-    
-    // Create title from text string
-    static function createTextTitle($text)
-    {
-        if(preg_match("/^.*\/(\w*)/", $text, $matches)) $text = ucfirst($matches[1]);
-        return $text;
-    }
+	// Create keywords from text string
+	static function createTextKeywords($text, $keywordsMax)
+	{
+		$tokens = preg_split("/[,\s\(\)]/", strtolower($text));
+		foreach($tokens as $key => $value) if(strlen($value) < 3) unset($tokens[$key]);
+		return implode(", ", array_slice(array_unique($tokens), 0, $keywordsMax));
+	}
+	
+	// Create title from text string
+	static function createTextTitle($text)
+	{
+		if(preg_match("/^.*\/(\w*)/", $text, $matches)) $text = ucfirst($matches[1]);
+		return $text;
+	}
+	
+	// Detect web browser language
+	function detectBrowserLanguage($languagesAllowed, $languageDefault)
+	{
+		$language = $languageDefault;
+		if(isset($_SERVER["HTTP_ACCEPT_LANGUAGE"]))
+		{
+			foreach(preg_split("/,\s*/", $_SERVER["HTTP_ACCEPT_LANGUAGE"]) as $string)
+			{
+				$tokens = split(";", $string, 2);
+				if(in_array($tokens[0], $languagesAllowed))
+				{
+					$language = $tokens[0];
+					break;
+				}
+			}
+		}
+		return $language;
+	}
 
 
-    // Detect PNG and JPG image dimensions
-    static function detectImageDimensions($fileName)
-    {
-        $width = $height = 0;
-        $fileHandle = @fopen($fileName, "rb");
-        if($fileHandle)
-        {
-            if(substr($fileName, -3) == "png")
-            {
-                $dataSignature = fread($fileHandle, 8);
-                $dataHeader = fread($fileHandle, 25);
-                if(!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n")
-                {
-                    $width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]);
-                    $height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]);
-                }
-            } else if(substr($fileName, -3) == "jpg") {
-                $dataSignature = fread($fileHandle, 11);
-                $dataHeader = fread($fileHandle, 147);
-                $dataHeader = fread($fileHandle, 16);
-                if(!feof($fileHandle) && $dataSignature=="\xff\xd8\xff\xe0\x00\x10JFIF\0")
-                {
-                    $width = (ord($dataHeader[7])<<8) + ord($dataHeader[8]);
-                    $height = (ord($dataHeader[5])<<8) + ord($dataHeader[6]);
-                }
-            }
-            fclose($fileHandle);
-        }
-        return array($width, $height);
-    }
+	// Detect PNG and JPG image dimensions
+	static function detectImageDimensions($fileName)
+	{
+		$width = $height = 0;
+		$fileHandle = @fopen($fileName, "rb");
+		if($fileHandle)
+		{
+			if(substr($fileName, -3) == "png")
+			{
+				$dataSignature = fread($fileHandle, 8);
+				$dataHeader = fread($fileHandle, 25);
+				if(!feof($fileHandle) && $dataSignature=="\x89PNG\r\n\x1a\n")
+				{
+					$width = (ord($dataHeader[10])<<8) + ord($dataHeader[11]);
+					$height = (ord($dataHeader[14])<<8) + ord($dataHeader[15]);
+				}
+			} else if(substr($fileName, -3) == "jpg") {
+				$dataSignature = fread($fileHandle, 11);
+				$dataHeader = fread($fileHandle, 147);
+				$dataHeader = fread($fileHandle, 16);
+				if(!feof($fileHandle) && $dataSignature=="\xff\xd8\xff\xe0\x00\x10JFIF\0")
+				{
+					$width = (ord($dataHeader[7])<<8) + ord($dataHeader[8]);
+					$height = (ord($dataHeader[5])<<8) + ord($dataHeader[6]);
+				}
+			}
+			fclose($fileHandle);
+		}
+		return array($width, $height);
+	}
 
 
-    // Start timer
-    static function timerStart(&$time)
-    {
-        $time = microtime(true);
-    }
-    
-    // Stop timer and calcuate elapsed time (milliseconds)
-    static function timerStop(&$time)
-    {
-        $time = intval((microtime(true)-$time) * 1000);
-    }
+	// Start timer
+	static function timerStart(&$time)
+	{
+		$time = microtime(true);
+	}
+	
+	// Stop timer and calcuate elapsed time (milliseconds)
+	static function timerStop(&$time)
+	{
+		$time = intval((microtime(true)-$time) * 1000);
+	}
 }
 }
 
 
 // Yellow configuration
 // Yellow configuration
 class Yellow_Config
 class Yellow_Config
 {
 {
-    var $config;            //configuration
-    var $configDefaults;    //configuration defaults
-    
-    function __construct()
-    {
-        $this->config = array();
-        $this->configDefaults = array();
-    }
-    
-    // Load configuration from file
-    function load($fileName)
-    {
-        $fileData = @file($fileName);
-        if($fileData)
-        {
-            foreach($fileData as $line)
-            {
-                if(preg_match("/^\//", $line)) continue;
-                preg_match("/^\s*(.*?)\s*=\s*(.*?)\s*$/", $line, $matches);
-                if($matches[1]!="" && $matches[2]!="")
-                {
-                    $this->set($matches[1], $matches[2]);
-                    if(defined("DEBUG") && DEBUG>=3) echo "Yellow_Config::load key:$matches[1] $matches[2]<br/>\n";
-                }
-            }
-        }
-    }
-    
-    // Set default configuration
-    function setDefault($key, $value)
-    {
-        $this->configDefaults[$key] = $value;
-    }
-    
-    // Set configuration
-    function set($key, $value)
-    {
-        $this->config[$key] = $value;
-    }
-    
-    // Return configuration
-    function get($key)
-    {
-        return is_null($this->config[$key]) ? $this->configDefaults[$key] : $this->config[$key];
-    }
-    
-    // Return configuration, HTML encoded
-    function getHtml($key)
-    {
-        return htmlspecialchars($this->get($key));
-    }
-    
-    // Check if configuration exists
-    function isExisting($key)
-    {
-        return !is_null($this->config[$key]);
-    }
+	var $config;			//configuration
+	var $configDefaults;	//configuration defaults
+	
+	function __construct()
+	{
+		$this->config = array();
+		$this->configDefaults = array();
+	}
+	
+	// Load configuration from file
+	function load($fileName)
+	{
+		$fileData = @file($fileName);
+		if($fileData)
+		{
+			if(defined("DEBUG") && DEBUG>=2) echo "Yellow_Config::load file:$fileName<br/>\n";
+			foreach($fileData as $line)
+			{
+				if(preg_match("/^\//", $line)) continue;
+				preg_match("/^\s*(.*?)\s*=\s*(.*?)\s*$/", $line, $matches);
+				if($matches[1]!="" && $matches[2]!="")
+				{
+					$this->set($matches[1], $matches[2]);
+					if(defined("DEBUG") && DEBUG>=3) echo "Yellow_Config::load key:$matches[1] $matches[2]<br/>\n";
+				}
+			}
+		}
+	}
+	
+	// Set default configuration
+	function setDefault($key, $value)
+	{
+		$this->configDefaults[$key] = $value;
+	}
+	
+	// Set configuration
+	function set($key, $value)
+	{
+		$this->config[$key] = $value;
+	}
+	
+	// Return configuration
+	function get($key)
+	{
+		return $this->isExisting($key) ? $this->config[$key] : $this->configDefaults[$key];
+	}
+	
+	// Return configuration, HTML encoded
+	function getHtml($key)
+	{
+		return htmlspecialchars($this->get($key));
+	}
+	
+	// Return configuration strings
+	function getData($filterEnd = "")
+	{
+		$config = array();
+		if($filterEnd == "")
+		{
+			$config = $this->config;
+		} else {
+			foreach($this->config as $key=>$value)
+			{
+				if(substr($key, -strlen($filterEnd)) == $filterEnd) $config[$key] = $value;
+			}
+		}
+		return $config;
+	}
+	
+	// Check if configuration exists
+	function isExisting($key)
+	{
+		return !is_null($this->config[$key]);
+	}
+}
+	
+// Yellow text strings
+class Yellow_Text
+{
+	var $text;			//text strings
+	var $language;		//current language
+
+	function __construct()
+	{
+		$this->text = array();
+	}
+	
+	// Load text strings from file
+	function load($fileName, $toolbox)
+	{
+		$path = dirname($fileName);
+		$regex = basename($fileName);
+		foreach($toolbox->getDirectoryEntries($path, "/$regex/", true, false) as $entry)
+		{
+			$fileData = @file("$path/$entry");
+			if($fileData)
+			{
+				if(defined("DEBUG") && DEBUG>=2) echo "Yellow_Text::load file:$path/$entry<br/>\n";
+				$language = "";
+				foreach($fileData as $line)
+				{
+					preg_match("/^\s*(.*?)\s*=\s*(.*?)\s*$/", $line, $matches);
+					if($matches[1]=="language" && $matches[2]!="") { $language = $matches[2]; break; }
+				}
+				foreach($fileData as $line)
+				{
+					if(preg_match("/^\//", $line)) continue;
+					preg_match("/^\s*(.*?)\s*=\s*(.*?)\s*$/", $line, $matches);
+					if($language!="" && $matches[1]!="" && $matches[2]!="")
+					{
+						$this->setLanguageText($language, $matches[1], $matches[2]);
+						if(defined("DEBUG") && DEBUG>=3) echo "Yellow_Text::load key:$matches[1] $matches[2]<br/>\n";
+					}
+				}
+			}
+		}
+	}
+	
+	// Set current language
+	function setLanguage($language)
+	{
+		$this->language = $language;
+	}
+	
+	// Set text string
+	function setLanguageText($language, $key, $value)
+	{
+		if(is_null($this->text[$language])) $this->text[$language] = array();
+		$this->text[$language][$key] = $value;
+	}
+	
+	// Return text string
+	function get($key)
+	{
+		return $this->isExisting($key) ? $this->text[$this->language][$key] : "[$key]";
+	}
+	
+	// Return text string, HTML encoded
+	function getHtml($key)
+	{
+		return htmlspecialchars($this->get($key)); 
+	}
+	
+	// Return text strings
+	function getData($language, $filterStart = "")
+	{
+		$text = array();
+		if(!is_null($this->text[$language]))
+		{
+			if($filterStart == "")
+			{
+				$text = $this->text[$language];
+			} else {
+				foreach($this->text[$language] as $key=>$value)
+				{
+					if(substr($key, 0, strlen("language")) == "language") $text[$key] = $value;
+					if(substr($key, 0, strlen($filterStart)) == $filterStart) $text[$key] = $value;
+				}
+			}
+		}
+		return $text;
+	}
+	
+	// Check if text string exists
+	function isExisting($key)
+	{
+		return !is_null($this->text[$this->language]) && !is_null($this->text[$this->language][$key]);
+	}
 }
 }
 
 
 // Yellow plugins
 // Yellow plugins
 class Yellow_Plugins
 class Yellow_Plugins
 {
 {
-    var $plugins;       //registered plugins
+	var $plugins;		//registered plugins
 
 
-    function __construct()
-    {
-        $this->plugins = array();
-    }
-    
-    // Load plugins
-    function load()
+	function __construct()
+	{
+		$this->plugins = array();
+	}
+	
+	// Load plugins
+	function load()
 	{
 	{
 		global $yellow;
 		global $yellow;
-        require_once("core_markdown.php");
-        require_once("core_rawhtml.php");
-        foreach($yellow->toolbox->getDirectoryEntries($yellow->config->get("pluginDir")) as $entry)
-        {
-            $fileName = "$entry/plugin_$entry.php";
-            if(is_file($fileName)) require_once($fileName);
-        }
- 		foreach($this->plugins as $key=>$value)
+		require_once("core_markdown.php");
+		require_once("core_rawhtml.php");
+		require_once("core_webinterface.php");
+		foreach($yellow->toolbox->getDirectoryEntries($yellow->config->get("pluginDir"), "/.*\.php/", true, false) as $entry)
 		{
 		{
-            $this->plugins[$key]["obj"] = new $value["class"];
-            if(defined("DEBUG") && DEBUG>=2) echo "Yellow_Plugins::load class:$value[class] $value[version]<br/>\n";
-            if(method_exists($this->plugins[$key]["obj"], "initPlugin"))
-            {
-                $this->plugins[$key]["obj"]->initPlugin($yellow);
-            }
+			$fileName = $yellow->config->get("pluginDir")."/$entry";
+			require_once($fileName);
+		}
+		foreach($this->plugins as $key=>$value)
+		{
+			$this->plugins[$key]["obj"] = new $value["class"];
+			if(defined("DEBUG") && DEBUG>=2) echo "Yellow_Plugins::load class:$value[class] $value[version]<br/>\n";
+			if(method_exists($this->plugins[$key]["obj"], "initPlugin"))
+			{
+				$this->plugins[$key]["obj"]->initPlugin($yellow);
+			}
 		}
 		}
 	}
 	}
-    
-    // Register plugin
+	
+	// Register plugin
 	function register($name, $class, $version)
 	function register($name, $class, $version)
 	{
 	{
-        if(is_null($this->plugins[$name]))
-        {
-            $this->plugins[$name] = array();
-            $this->plugins[$name]["class"] = $class;
-            $this->plugins[$name]["version"] = $version;
-        }
+		if(!$this->isExisting($name))
+		{
+			$this->plugins[$name] = array();
+			$this->plugins[$name]["class"] = $class;
+			$this->plugins[$name]["version"] = $version;
+		}
+	}
+	
+	// Check if plugin exists
+	function isExisting($name)
+	{
+		return !is_null($this->plugins[$name]);
 	}
 	}
 }
 }
 ?>
 ?>

+ 33 - 24
system/core/core_markdown.php

@@ -5,51 +5,60 @@
 // Markdown parser core plugin
 // Markdown parser core plugin
 class Yellow_Markdown
 class Yellow_Markdown
 {
 {
-	var $markdown;  //markdown parser
-	var $html;      //generated HTML
-    
-    // Initialise plugin
+	const Version = "0.1.1";
+	var $markdown;			//markdown parser
+	var $html;				//generated HTML
+	
+	// Initialise plugin
 	function initPlugin($yellow)
 	function initPlugin($yellow)
 	{
 	{
 		$this->markdown = new Yellow_MarkdownExtraParser($yellow);
 		$this->markdown = new Yellow_MarkdownExtraParser($yellow);
 	}
 	}
-
-    // Parse text
+	
+	// Parse text
 	function parse($text)
 	function parse($text)
 	{
 	{
 		return $this->html = $this->markdown->transform($text);
 		return $this->html = $this->markdown->transform($text);
 	}
 	}
 }
 }
-    
+
 require("markdown.php");
 require("markdown.php");
 class Yellow_MarkdownExtraParser extends MarkdownExtra_Parser
 class Yellow_MarkdownExtraParser extends MarkdownExtra_Parser
 {
 {
-    var $yellow;    //access to API
+	var $yellow;	//access to API
 
 
-    function __construct($yellow)
-    {
-        $this->yellow = $yellow;
-        parent::__construct();
-    }
-    
-    // Handle images
+	function __construct($yellow)
+	{
+		$this->yellow = $yellow;
+		parent::__construct();
+	}
+	
+	// Transform text
+	function transform($text)
+	{
+		$baseLocation = $this->yellow->config->get("baseLocation");
+		$text = preg_replace("/@baseLocation/i", $baseLocation, $text);
+		return parent::transform($text);
+	}
+	
+	// Handle images
 	function _doImages_inline_callback($matches)
 	function _doImages_inline_callback($matches)
-    {
+	{
 		$path = $matches[3]=="" ? $matches[4] : $matches[3];
 		$path = $matches[3]=="" ? $matches[4] : $matches[3];
-        $src = $this->yellow->config->get("baseLocation").$this->yellow->config->get("imagesLocation").$path;
-        list($width, $height) = $this->yellow->toolbox->detectImageDimensions(".".$this->yellow->config->get("imagesLocation").$path);
+		$src = $this->yellow->config->get("baseLocation").$this->yellow->config->get("imagesLocation").$path;
+		list($width, $height) = $this->yellow->toolbox->detectImageDimensions(".".$this->yellow->config->get("imagesLocation").$path);
 		$alt = $matches[2];
 		$alt = $matches[2];
-		$title =& $matches[7];
-        
+		$title = $matches[7];
+		
 		$result = "<img src=\"".$this->encodeAttribute($src)."\"";
 		$result = "<img src=\"".$this->encodeAttribute($src)."\"";
-        if($width && $height) $result .= " width=\"$width\" height=\"$height\"";
-        if(isset($alt)) $result .= " alt=\"".$this->encodeAttribute($alt)."\"";
+		if($width && $height) $result .= " width=\"$width\" height=\"$height\"";
+		if(isset($alt)) $result .= " alt=\"".$this->encodeAttribute($alt)."\"";
 		if(isset($title)) $result .=  " title=\"".$this->encodeAttribute($title)."\"";
 		if(isset($title)) $result .=  " title=\"".$this->encodeAttribute($title)."\"";
 		$result .= $this->empty_element_suffix;
 		$result .= $this->empty_element_suffix;
-        
+		
 		return $this->hashPart($result);
 		return $this->hashPart($result);
 	}
 	}
 }
 }
 
 
-$yellow->registerPlugin("markdown", "Yellow_Markdown", "0.1.0");
+$yellow->registerPlugin("markdown", "Yellow_Markdown", Yellow_Markdown::Version);
 ?>
 ?>

+ 4 - 3
system/core/core_rawhtml.php

@@ -5,14 +5,15 @@
 // Raw HTML parser core plugin
 // Raw HTML parser core plugin
 class Yellow_RawHtml
 class Yellow_RawHtml
 {
 {
-	var $html;      //generated HTML
+	const Version = "0.1.1";
+	var $html;				//generated HTML
 
 
-    // Parse text, dummy transformation
+	// Parse text, dummy transformation
 	function parse($text)
 	function parse($text)
 	{
 	{
 		return $this->html = $text;
 		return $this->html = $text;
 	}
 	}
 }
 }
 
 
-$yellow->registerPlugin("rawhtml", "Yellow_RawHtml", "0.1.0");
+$yellow->registerPlugin("rawhtml", "Yellow_RawHtml", Yellow_RawHtml::Version);
 ?>
 ?>

+ 25 - 0
system/core/core_webinterface.css

@@ -0,0 +1,25 @@
+.yellowlogin { width:600px; position:absolute; top:5px; padding:30px; border:1px solid #ccc; background:#fff; color:#000; }
+.yellowlogin h1 { margin:0px; padding:0px; }
+.yellowlogin p { margin:0.5em; text-align:right; }
+
+.yellowbar { width:600px; position:absolute; top:1px; background:#fff; color:#000; }
+.yellowbar img { vertical-align:top; }
+.yellowbar button { color:#05d; padding-left:0.5em; padding-right:0.5em; }
+.yellowbar button:hover { color:#f00; }
+.yellowbarleft { margin-left:0px; display:block; float:left; height:100%; }
+.yellowbarright { margin-right:0px; display:block; float:right; height:100%; }
+.yellowbarlink { cursor:pointer; font:inherit; background:none; border:none; margin:0px padding:0px; }
+.yellowbubble { -webkit-border-radius:4px; -moz-border-radius:4px; border-radius:4px; }
+
+.yellowpane { position:absolute; display:none; margin:0px; padding:5px; border:solid 1px #ccc; background:#fff; color:#000; z-index:10; }
+.yellowpane a { text-decoration:none; color:#000; }
+.yellowpane a:hover { text-decoration:none; color:#f00; }
+.yellowpane p { margin:0.5em; }
+.yellowpane ul { list-style:none; margin:0em 0.5em; padding:0px; }
+
+#yellowpaneedit { }
+#yellowpaneshow { min-width:250px; overflow:auto; }
+#yellowpaneuser { }
+#yellowedittext { margin:0px; margin-bottom:5px; padding:5px; border:solid 1px #ccc; resize:none; font-size:0.9em }
+#yelloweditbuttons { margin-bottom:5px; width:100%; }
+#yelloweditbuttons input { margin-left:5px; }

+ 243 - 0
system/core/core_webinterface.js

@@ -0,0 +1,243 @@
+// Copyright (c) 2013 Datenstrom, http://www.datenstrom.se
+// This file may be used and distributed under the terms of the public license.
+
+// Yellow main API
+var yellow =
+{
+	version: "0.0.0",	//Hello web interface!
+	onClick: function(e) { yellow.webinterface.hidePanesOnClick(e); },
+	onShow: function(id) { yellow.webinterface.showPane(id); },
+	onReset: function(id) { yellow.webinterface.resetPane(id); },
+	onResize: function() { yellow.webinterface.resizePanes(); },
+	webinterface:{}, page:{}, pages:{}, toolbox:{}, config:{}, text:{}
+}
+
+// Yellow web interface
+yellow.webinterface =
+{
+	created: false,		//interface created? (boolean)
+	timerId: 0,			//interface timer ID
+	heightOld: 0,		//height of big panes
+
+	// Initialise web interface
+	init: function()
+	{
+		this.intervalId = window.setInterval("yellow.webinterface.create()", 1);
+		window.onresize = yellow.onResize;
+		window.onclick = yellow.onClick;
+	},
+	
+	// Create action bar and panes
+	create: function()
+	{
+		var body = document.getElementsByTagName("body")[0];
+		if(!body || !body.firstChild || this.created) return;
+		if(yellow.debug) console.log("yellow.webinterface.create email:"+yellow.config.userEmail+" "+yellow.config.userName);		
+		if(yellow.config.userEmail)
+		{
+			var location = yellow.config.baseLocation+yellow.config.pluginsLocation;
+			var element = document.createElement("div");
+			element.className = "yellowbar";
+			element.setAttribute("id", "yellowbar");
+			element.innerHTML =
+				"<div class=\"yellowbarleft\">"+
+				"<img src=\""+location+"core_webinterface.png\" width=\"16\" height=\"16\"> Yellow"+
+				"<button class=\"yellowbarlink\" onclick=\"yellow.onShow('yellowpaneedit');\">"+this.getText("Edit")+"</button>"+
+				"<button class=\"yellowbarlink\" onclick=\"yellow.onShow('yellowpaneshow');\">"+this.getText("Show")+"</button>"+
+				"</div>"+
+				"<div class=\"yellowbarright\">"+
+				"<button class=\"yellowbarlink\" onclick=\"yellow.onShow('yellowpaneuser');\" id=\"yellowusername\">"+this.getText("User")+"</button>"+
+				"</div>";
+			body.insertBefore(element, body.firstChild);
+			yellow.toolbox.insertAfter(this.createPane("yellowpaneedit"), body.firstChild);
+			yellow.toolbox.insertAfter(this.createPane("yellowpaneshow", yellow.pages), body.firstChild);
+			yellow.toolbox.insertAfter(this.createPane("yellowpaneuser"), body.firstChild);
+			yellow.toolbox.setText(document.getElementById("yellowusername"), yellow.config.userName+" ↓");
+			yellow.toolbox.setText(document.getElementById("yellowedittext"), yellow.page.rawData);
+		} else {
+			var element = document.createElement("div");
+			element.className = "yellowlogin yellowbubble";
+			element.setAttribute("id", "yellowlogin");
+			element.innerHTML =
+				"<form method=\"post\" name=\"formlogin\">"+
+				"<input type=\"hidden\" name=\"action\" value=\"login\"/>"+
+				"<h1>"+this.getText("LoginText")+"</h1>"+
+				"<p>"+this.getText("LoginEmail")+" <input name=\"email\" maxlength=\"64\" /></p>"+
+				"<p>"+this.getText("LoginPassword")+" <input type=\"password\" name=\"password\" maxlength=\"64\" /></p>"+
+				"<p><input type=\"submit\" value=\""+this.getText("LoginButton")+"\"/></p>"+
+				"</form>";
+			body.insertBefore(element, body.firstChild);
+		}
+		window.clearInterval(this.intervalId);
+		this.created = true;
+		this.resizePanes(true);
+	},
+	
+	// Create pane
+	createPane: function (id, data)
+	{
+		if(yellow.debug) console.log("yellow.webinterface.createPane id:"+id);
+		var outDiv = document.createElement("div");
+		if(id == "yellowpaneedit")
+		{
+			outDiv.innerHTML =
+				"<p>Editing page...</p>"+
+				"<form method=\"post\" name=\"formeditor\">"+
+				"<input type=\"hidden\" name=\"action\" value=\"edit\"/>"+
+				"<textarea id=\"yellowedittext\" name=\"rawdata\"></textarea>"+
+				"<div id=\"yelloweditbuttons\">"+
+				"<input type=\"submit\" value=\""+this.getText("SaveButton")+"\"/>"+
+				"<input type=\"button\" value=\""+this.getText("CancelButton")+"\" onclick=\"yellow.onReset('yellowpaneedit');\"/>"+
+				"</div>"+
+				"</form>";
+		} else if(id == "yellowpaneshow") {
+			outDiv.innerHTML = "<p>Showing files...</p>";
+			for(var n in data)
+			{
+				var outUl = document.createElement("ul");
+				var outLi = document.createElement("li");
+				var outA = document.createElement("a");
+				outA.setAttribute("href", data[n]["location"]);
+				yellow.toolbox.setText(outA, data[n]["title"]);
+				outLi.appendChild(outA);
+				outUl.appendChild(outLi);
+				outDiv.appendChild(outUl);
+			}
+		} else if(id == "yellowpaneuser") {
+			outDiv.innerHTML =
+				"<p>"+yellow.config.userEmail+"</p>"+
+				"<form method=\"post\" name=\"formlogout\">"+
+				"<input type=\"hidden\" name=\"action\" value=\"logout\"/>"+
+				"<p><a href=\"javascript:document.formlogout.submit();\">"+this.getText("UserLogout")+"</a></p> "+
+				"</form>";
+		}
+		var element = document.createElement("div");
+		element.className = "yellowpane yellowbubble";
+		element.setAttribute("id", id);
+		element.appendChild(outDiv);
+		return element;
+	},
+	
+	// Reset pane
+	resetPane: function(id)
+	{
+		if(id == "yellowpaneedit")
+		{
+			document.formeditor.reset();
+			yellow.toolbox.setText(document.getElementById("yellowedittext"), yellow.page.rawData);
+			this.hidePane(id);
+		}
+	},
+	
+	// Show pane
+	showPane: function(id)
+	{
+		if(document.getElementById(id).style.display == "block")
+		{
+			this.hidePanes();
+		} else {
+			this.hidePanes();
+			if(yellow.debug) console.log("yellow.webinterface.showPane id:"+id);
+			document.getElementById(id).style.display = "block";
+			this.resizePanes(true);
+		}
+	},
+
+	// Hide pane
+	hidePane: function(id)
+	{
+		if(document.getElementById(id)) document.getElementById(id).style.display = "none";
+	},
+
+	// Hide all panes
+	hidePanes: function ()
+	{
+		for(var element=document.getElementById("yellowbar"); element; element=element.nextSibling)
+		{
+			if(element.className && element.className.indexOf("yellowpane")>=0)
+			{
+				this.hidePane(element.getAttribute("id"));
+			}
+		}
+	},
+
+	// Hide all panes on mouse click
+	hidePanesOnClick: function(e)
+	{
+		var element = e.target || e.srcElement;
+		while(element = element.parentNode)
+		{
+			if(element.className)
+			{
+				if(element.className.indexOf("yellowpane")>=0 || element.className.indexOf("yellowbar")>=0) return;
+			}
+		}
+		this.hidePanes();
+	},
+
+	// Resize panes, recalculate height and width where needed
+	resizePanes: function(force)
+	{
+		var interfaceHeight = Number(window.innerHeight);
+		if((interfaceHeight!=this.heightOld || force) && document.getElementById("yellowbar"))
+		{
+			this.heightOld = interfaceHeight;
+			var elementBar = document.getElementById("yellowbar");
+			var borderRadius = 6;
+			var panePadding = 5;
+			var editPadding = 5;
+			var interfaceTop = elementBar.offsetHeight + 1;
+			interfaceHeight -= interfaceTop + borderRadius*2;
+			if(yellow.debug) console.log("yellow.webinterface.resizePanes windowY:"+interfaceHeight+" actionbarY:"+document.getElementById("yellowbar").offsetHeight+" buttonsY:"+document.getElementById("yelloweditbuttons").offsetHeight+" editorX:"+document.getElementById("yellowpaneedit").offsetWidth);
+
+			this.setPaneHeight(document.getElementById("yellowpaneedit"), interfaceHeight, null, interfaceTop);
+			this.setPaneHeight(document.getElementById("yellowpaneshow"), null, interfaceHeight, interfaceTop);
+			this.setPaneHeight(document.getElementById("yellowpaneuser"), null, null, interfaceTop);
+			
+			var editTextHeight = interfaceHeight - panePadding*2 - editPadding*2 - 10
+								- (document.getElementById("yellowedittext").offsetTop-document.getElementById("yellowpaneedit").getElementsByTagName("p")[0].offsetTop)
+								- document.getElementById("yelloweditbuttons").offsetHeight;
+			document.getElementById("yellowpaneedit").style.width = (elementBar.offsetWidth - panePadding*2).toString()+"px";
+			document.getElementById("yellowedittext").style.height = editTextHeight.toString()+"px";
+			document.getElementById("yellowedittext").style.width = (document.getElementById("yellowpaneedit").offsetWidth - 2 - panePadding*2 - editPadding*2).toString()+"px";
+			document.getElementById("yellowpaneuser").style.marginLeft = (elementBar.offsetWidth - document.getElementById("yellowpaneuser").offsetWidth).toString()+"px";
+		}
+	},
+
+	// Set pane height
+	setPaneHeight: function(element, height, maxHeight, top)
+	{
+		if(maxHeight)
+		{
+			element.style.maxHeight = maxHeight.toString()+"px"; 
+		} else if(height) {
+			element.style.height = height.toString()+"px";
+		}
+		element.style.top = top+"px";	
+	},
+	
+	// Return text string
+	getText: function(key)
+	{
+		return ("webinterface"+key in yellow.text) ? yellow.text["webinterface"+key] : "[webinterface"+key+"]";
+	}
+}
+
+// Yellow toolbox with helpers
+yellow.toolbox =
+{
+	// Set element text
+	setText: function(element, text)
+	{
+		while(element.firstChild!==null) element.removeChild(element.firstChild);
+		element.appendChild(document.createTextNode(text));
+	},
+
+	// Insert element after element
+	insertAfter: function(newElement, referenceElement)
+	{
+		referenceElement.parentNode.insertBefore(newElement, referenceElement.nextSibling);
+	}
+}
+
+yellow.webinterface.init();

+ 317 - 0
system/core/core_webinterface.php

@@ -0,0 +1,317 @@
+<?php
+// Copyright (c) 2013 Datenstrom, http://www.datenstrom.se
+// This file may be used and distributed under the terms of the public license.
+
+// Web interface core plugin
+class Yellow_Webinterface
+{
+	const Version = "0.0.0";	//Hello web interface!
+	var $yellow;			//access to API
+	var $users;				//web interface users
+	var $activeLocation;	//web interface location? (boolean)
+	var $activeUserFail;	//web interface login failed (boolean)
+	var $activeUserEmail;	//web interface user currently logged in
+
+	// Initialise plugin
+	function initPlugin($yellow)
+	{
+		$this->yellow = $yellow;
+		$this->yellow->config->setDefault("webinterfaceLocation", "/wiki/");
+		$this->yellow->config->setDefault("webinterfaceUserFile", "user.ini");
+		$this->users = new Yellow_WebinterfaceUsers();
+		$this->users->load($this->yellow->config->get("configDir").$this->yellow->config->get("webinterfaceUserFile"));
+	}
+
+	// Handle web interface location
+	function onRequest($baseLocation, $location, $fileName)
+	{
+		$statusCode = 0;
+		if($this->checkWebinterfaceLocation($location))
+		{
+			$baseLocation .= rtrim($this->yellow->config->get("webinterfaceLocation"), '/');
+			$location = $this->yellow->getRelativeLocation($baseLocation);
+			$fileName = $this->yellow->getContentFileName($location);
+			if($this->checkUser()) $statusCode = $this->processRequestAction($baseLocation, $location, $fileName);
+			if($statusCode == 0) $statusCode = $this->yellow->processRequestFile($baseLocation, $location, $fileName,
+													$this->activeUserFail ? 401 : 0, false);
+		} else {
+			if($this->yellow->config->get("webinterfaceLocation") == "$location/")
+			{
+				$statusCode = 301;
+				$this->yellow->sendStatus($statusCode, "Location: http://$_SERVER[SERVER_NAME]$baseLocation$location/");
+			}
+		}
+		return $statusCode;
+	}
+	
+	// Handle extra HTML header lines
+	function onHeaderExtra()
+	{
+		$header = "";
+		if($this->isWebinterfaceLocation())
+		{
+			$location = $this->yellow->config->getHtml("baseLocation").$this->yellow->config->getHtml("pluginsLocation");
+			$language = $this->isUser() ? $this->users->getLanguage($this->activeUserEmail) : $this->yellow->page->get("language");
+			$header .= "<link href=\"{$location}core_webinterface.css\" rel=\"styleSheet\" media=\"all\" type=\"text/css\" />\n";
+			$header .= "<script type=\"text/javascript\" src=\"{$location}core_webinterface.js\"></script>\n";
+			$header .= "<script type=\"text/javascript\">\n";
+			$header .= "// <![CDATA[\n";
+			if($this->isUser())
+			{
+				$header .= "yellow.page.rawData = ".json_encode($this->yellow->page->rawData).";\n";
+				$header .= "yellow.pages = ".json_encode($this->getPagesData()).";\n";
+				$header .= "yellow.config = ".json_encode($this->getConfigData($this->activeUserEmail)).";\n";
+			}
+			$header .= "yellow.text = ".json_encode($this->yellow->text->getData($language, "webinterface")).";\n";
+			if(defined("DEBUG")) $header .= "yellow.debug = ".json_encode(DEBUG).";\n";
+			$header .= "// ]]>\n";
+			$header .= "</script>\n";
+		}
+		return $header;
+	}
+	
+	// Handle page before parser
+	function onParseBefore($text, $statusCode)
+	{
+		if($this->isWebinterfaceLocation() && $this->isUser())
+		{
+			if($statusCode == 424)
+			{
+				$this->yellow->page->rawData = "---\r\n";
+				$this->yellow->page->rawData .= "Title: ".$this->yellow->text->get("webinterface424Title")."\r\n";
+				$this->yellow->page->rawData .= "Author: ".$this->users->getName($this->activeUserEmail)."\r\n";
+				$this->yellow->page->rawData .= "---\r\n";
+				$this->yellow->page->rawData .= $this->yellow->text->get("webinterface424Text");
+			}
+		}
+		return $text;
+	}
+	
+	// Handle page after parser
+	function onParseAfter($text, $statusCode)
+	{
+		if($this->isWebinterfaceLocation() && $this->isUser())
+		{
+			$this->yellow->toolbox->timerStart($time);
+			$baseLocation = $this->yellow->config->get("baseLocation");
+			$webinterfaceLocation = rtrim($this->yellow->config->get("webinterfaceLocation"), '/');
+			$text = preg_replace("#<a(.*?)href=\"$baseLocation(?!$webinterfaceLocation)(.*?)\"(.*?)>#",
+								 "<a$1href=\"$baseLocation$webinterfaceLocation$2\"$3>", $text);
+		}
+		return $text;
+	}
+	
+	// Process request for an action
+	function processRequestAction($baseLocation, $location, $fileName)
+	{
+		$statusCode = 0;
+		if($_POST["action"] == "edit")
+		{
+			if(strlen($_POST["rawdata"]))
+			{
+				$fileHandle = @fopen($fileName, "w");
+				if($fileHandle)
+				{
+					fwrite($fileHandle, $_POST["rawdata"]);
+					fclose($fileHandle);
+				} else {
+					die("Configuration problem: Can't write page '$fileName'!");
+				}
+			}
+		} else if($_POST["action"]== "logout") {
+			$this->users->destroyCookie("login");
+			$this->activeUserEmail = "";
+			$statusCode = 302;
+			$newLocation = $this->yellow->config->getHtml("baseLocation").$location;
+			$this->yellow->sendStatus($statusCode, "Location: http://$_SERVER[SERVER_NAME]$newLocation");
+		} else {
+			if(!is_readable($fileName))
+			{
+				if($this->yellow->toolbox->isFileLocation($location) && is_dir($this->yellow->getContentDirectory("$location/")))
+				{
+					$statusCode = 301;
+					$this->yellow->sendStatus($statusCode, "Location: http://$_SERVER[SERVER_NAME]$baseLocation$location/");
+				} else {
+					$statusCode = $this->checkUserPermissions($location, $fileName) ? 424 : 404;
+					$this->yellow->processRequestFile($baseLocation, $location, $fileName, $statusCode, false);
+				}
+			}
+		}
+		return $statusCode;
+	}
+	
+	// Check web interface location
+	function checkWebinterfaceLocation($location)
+	{
+		$locationLength = strlen($this->yellow->config->get("webinterfaceLocation"));
+		$this->activeLocation = substr($location, 0, $locationLength) == $this->yellow->config->get("webinterfaceLocation");
+		return $this->isWebinterfaceLocation();
+	}
+	
+	// Check user login
+	function checkUser()
+	{
+		if($_POST["action"] == "login")
+		{
+			$email = $_POST["email"];
+			$password = $_POST["password"];
+			if($this->users->checkUser($email, $password))
+			{
+				$this->users->createCookie("login", $email);
+				$this->activeUserEmail = $email;
+			} else {
+				$this->activeUserFail = true;
+			}
+		} else if(isset($_COOKIE["login"])) {
+			$cookie = $_COOKIE["login"];
+			if($this->users->checkCookie($cookie))
+			{
+				$this->activeUserEmail = $this->users->getCookieEmail($cookie);
+			} else {
+				$this->activeUserFail = true;
+			}
+		}
+		return $this->isUser();
+	}
+	
+	// Check users permissions for creating new page
+	function checkUserPermissions($location, $fileName)
+	{
+		$path = dirname($fileName);
+		return is_dir($path);
+	}
+	
+	// Check if web interface location
+	function isWebinterfaceLocation()
+	{
+		return $this->activeLocation;
+	}
+	
+	// Check if user is logged in
+	function isUser()
+	{
+		return !empty($this->activeUserEmail);
+	}
+
+	// Return page tree with content/media information
+	function getPagesData()
+	{
+		$data = array();
+		foreach($this->yellow->pages->root(true) as $page)
+		{
+			$data[$page->fileName] = array();
+			$data[$page->fileName]["location"] = $page->getLocation();
+			$data[$page->fileName]["title"] = $page->getTitle();
+		}
+		return $data;
+	}
+	
+	// Return configuration data including user information
+	function getConfigData($email)
+	{
+		$data = array("userEmail" => $email,
+					  "userName" => $this->users->getName($email),
+					  "userLanguage" => $this->users->getLanguage($email),
+					  "baseLocation" => $this->yellow->config->get("baseLocation"));
+		return array_merge($data, $this->yellow->config->getData("Location"));
+	}
+}
+
+// Yellow web interface users
+class Yellow_WebinterfaceUsers
+{
+	var $users;		//registered users
+	
+	function __construct()
+	{
+		$this->users = array();
+	}
+
+	// Load users from file
+	function load($fileName) 
+	{
+		$fileData = @file($fileName);
+		if($fileData)
+		{
+			foreach($fileData as $line)
+			{
+				if(preg_match("/^\//", $line)) continue;
+				preg_match("/^(.*?)\s*,(.*?),\s*(.*?),\s*(.*?)\s*$/", $line, $matches);
+				if($matches[1]!="" && $matches[2]!="" && $matches[3]!="" && $matches[4]!="")
+				{
+					$this->setUser($matches[1], $matches[2], $matches[3], $matches[4]);
+					if(defined("DEBUG") && DEBUG>=3) echo "Yellow_WebinterfaceUsers::load email:$matches[1] $matches[3]<br/>\n";
+				}
+			}
+		}
+	}
+
+	// Set user data
+	function setUser($email, $password, $name, $language)
+	{
+		$this->users[$email] = array();
+		$this->users[$email]["email"] = $email;
+		$this->users[$email]["password"] = $password;
+		$this->users[$email]["name"] = $name;
+		$this->users[$email]["language"] = $language;
+		$this->users[$email]["session"] = hash("sha256", $email.$password.strrev($email.$password));
+	}
+	
+	// Check user login
+	function checkUser($email, $password)
+	{
+		return $this->isExisting($email) && hash("sha256", $email.$password)==$this->users[$email]["password"];
+	}
+
+	// Create browser cookie
+	function createCookie($cookieName, $email)
+	{
+		if($this->isExisting($email))
+		{
+			$salt = hash("sha256", uniqid(mt_rand(), true));
+			$text = $email.";".$salt.";".hash("sha256", $salt.$this->users[$email]["session"]);
+			setcookie($cookieName, $text, time()+60*60*24*30*365*10, "/") || die("Server problem: Can't create '$cookieName' cookie!");
+		}
+	}
+	
+	// Destroy browser cookie
+	function destroyCookie($cookieName)
+	{
+		setcookie($cookieName, "", time()-3600, "/");
+	}
+	
+	// Check user login from browser cookie
+	function checkCookie($cookie)
+	{
+		list($email, $salt, $session) = explode(";", $cookie);
+		return $this->isExisting($email) && hash("sha256", $salt.$this->users[$email]["session"])==$session;
+	}
+	
+	// Return user email from browser cookie
+	function getCookieEmail($cookie)
+	{
+		list($email, $salt, $session) = explode(";", $cookie);
+		return $email;
+	}
+	
+	// Return user name
+	function getName($email)
+	{
+		return $this->isExisting($email) ? $this->users[$email]["name"] : "";
+	}
+
+	// Return user language
+	function getLanguage($email)
+	{
+		return $this->isExisting($email) ? $this->users[$email]["language"] : "";
+	}	
+	
+	// Check if user exists
+	function isExisting($email)
+	{
+		return !is_null($this->users[$email]);
+	}
+}
+
+$yellow->registerPlugin("webinterface", "Yellow_Webinterface", Yellow_Webinterface::Version);
+?>

BIN=BIN
system/core/core_webinterface.png


+ 14 - 0
system/plugins/example.php

@@ -0,0 +1,14 @@
+<?php
+// Copyright (c) 2013 Datenstrom, http://www.datenstrom.se
+// This file may be used and distributed under the terms of the public license.
+
+// Example plugin
+class Yellow_ExamplePlugin
+{
+	//You can download plugins and extensions from Github.
+	//See https://github.com/markseu/yellowcms-extensions
+	const Version = "0.0.0";
+}
+
+$yellow->registerPlugin("example", "Yellow_ExamplePlugin", Yellow_ExamplePlugin::Version);
+?>