Robert Marshall 10 роки тому
коміт
9cb54c60b9

+ 7 - 0
.htaccess

@@ -0,0 +1,7 @@
+Options -Indexes
+
+RewriteEngine On
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-l
+RewriteRule ^(.*)$ index.php?__url=$1 [QSA,L,NC]

+ 6 - 0
Controller/E404.php

@@ -0,0 +1,6 @@
+<?php
+class E404{
+	public function Index($params) {
+		return new View("E404/index.view");
+	}
+}

+ 7 - 0
Controller/Home.php

@@ -0,0 +1,7 @@
+<?php
+class Home{
+	public function Index($params){
+		Breadcrumbs::Add("Test", "/home");
+		return new View("Home/index.view");
+	}
+}

+ 15 - 0
Controller/Navigation/BlogNav.php

@@ -0,0 +1,15 @@
+<?php
+class BlogNav implements INavigationController{
+	private $_uri;
+	
+	public function __construct() {
+		$this->_uri=new URI("Blog","/blog","images/blog.svg");
+	}
+	
+	public function GetItems() {
+	}
+
+	public function GetURI() {
+		return $this->_uri;
+	}
+}

+ 18 - 0
Controller/Navigation/HomeNav.php

@@ -0,0 +1,18 @@
+<?php
+class HomeNav implements INavigationController{
+	private $_uri;
+	
+	public function __construct() {
+		$this->_uri=new URI("Home","/","images/home.svg");
+	}
+	
+	public function GetItems() {
+		return array(
+			new URI("Test","test")
+		);
+	}
+
+	public function GetURI() {
+		return $this->_uri;
+	}
+}

+ 15 - 0
Controller/Navigation/ProjectsNav.php

@@ -0,0 +1,15 @@
+<?php
+class ProjectsNav implements INavigationController{
+	private $_uri;
+	
+	public function __construct() {
+		$this->_uri=new URI("Projects","/projects","images/projects.svg");
+	}
+	
+	public function GetItems() {
+	}
+
+	public function GetURI() {
+		return $this->_uri;
+	}
+}

+ 5 - 0
Controller/Navigation/base/INavigationController.php

@@ -0,0 +1,5 @@
+<?php
+interface INavigationController {
+	public function GetURI();
+	public function GetItems();
+}

+ 4 - 0
Interfaces/IConvertible.php

@@ -0,0 +1,4 @@
+<?php
+interface IConvertible{
+	public function Convert($item);
+}

+ 7 - 0
Interfaces/ISavableObject.php

@@ -0,0 +1,7 @@
+<?php
+interface ISavableObject {	
+	public function __get($name);
+	public function __set($name, $value);
+	public function Load();
+	public function Save();
+}

+ 15 - 0
Model/Breadcrumbs.php

@@ -0,0 +1,15 @@
+<?php
+class Breadcrumbs{
+	private static $_crumbs=array();
+	
+	public static function Add($text,$link){
+		self::$_crumbs[]=array(
+			"text"=>$text,
+			"link"=>$link
+		);
+	}
+	
+	public static function GetAll(){
+		return self::$_crumbs;
+	}
+}

+ 131 - 0
Model/DBObject.php

@@ -0,0 +1,131 @@
+<?php
+ApplicationSettings::RegisterDefaultSetting("database", "host", "localhost");
+ApplicationSettings::RegisterDefaultSetting("database", "database", "php-mvc");
+ApplicationSettings::RegisterDefaultSetting("database", "username", "root");
+ApplicationSettings::RegisterDefaultSetting("database", "password", "");
+
+
+class DBObject implements ISavableObject{
+	protected static $PDO=null;
+	protected static $PREPARED_STATEMENTS=array();
+	
+	private static $_classFields=array();
+	
+	protected $_fields=array();
+	protected $_changedFields=array();
+	
+	private $_table,$_key,$_id;
+
+	protected static function SetupPDO(){
+		if (self::$PDO!=null)
+			return;
+		$host=ApplicationSettings::GetSetting("database", "host");
+		$db=ApplicationSettings::GetSetting("database", "database");
+		$username=ApplicationSettings::GetSetting("database", "username");
+		$password=ApplicationSettings::GetSetting("database", "password");
+		self::$PDO=new PDO("mysql:host=$host;dbname=$db",$username,$password);
+		self::$PDO->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE,PDO::FETCH_ASSOC);
+	}
+	
+	public static function GetPDO(){
+		self::SetupPDO();
+		return self::$PDO;
+	}
+	
+	public static function VariableToDBField($variableName) {
+		$parts=preg_split('/(?=[A-Z])/', $variableName);
+		for ($i=0;$i<count($parts);$i++)
+			$parts[$i]=strtolower($parts[$i]);
+		return trim(implode("_", $parts),"_"); // If the variable name start with upper case then we get an extra blank entry in the array causing an extra _
+	}
+	
+	public static function DBFieldToVariable($fieldName) {
+		$parts=explode("_",$fieldName);
+		for ($i=0;$i<count($parts);$i++)
+			$parts[$i]=ucfirst($parts[$i]);
+		return implode("", $parts);
+	}
+	
+	function __construct($table, $key, $id) {
+		$this->_table=$table;
+		$this->_key=$key;
+		$this->_id=$id;
+		
+		self::SetupPDO();
+		
+		$this->Load();
+	}
+	
+	public function __get($name) {
+		if (array_key_exists($name,$this->_fields))
+			return $this->_fields[$name];
+		return null;
+	}
+	
+	public function __set($name, $value) {
+		if (array_key_exists($name,$this->_fields) && $this->_fields[$name]!=$value){
+			$this->_changedFields[$name]=$name;
+			$this->_fields[$name]=$value;
+		}
+	}
+	
+	public function Load(){
+		$class=get_class($this);
+		
+		if (!isset(self::$_classFields[$class])){
+			self::$_classFields[$class]=self::$PDO->query("DESCRIBE `{$this->_table}`")->fetchAll(PDO::FETCH_COLUMN);
+		}
+		
+		$statementKey=$class.'_construct';
+		if (!isset(self::$PREPARED_STATEMENTS[$statementKey])){
+			$fields=implode(", ", self::$_classFields[$class]);
+			$sql="SELECT $fields FROM `{$this->_table}` WHERE `{$this->_key}`=?";
+			self::$PREPARED_STATEMENTS[$statementKey]=self::$PDO->prepare($sql);
+		}
+		$prep=self::$PREPARED_STATEMENTS[$statementKey];
+		$prep->execute(array($this->_id));
+		$record=$prep->fetch();
+		if ($record!==false)
+			foreach ($record as $key=>$value)
+				$this->_fields[self::DBFieldToVariable($key)]=$value;
+		else {
+			foreach (self::$_classFields[$class] as $field)
+				$this->_fields[self::DBFieldToVariable($field)]=null;
+			$this->_id=0;
+		}
+	}
+	
+	public function Save() {
+		if (count($this->_changedFields)==0)
+			return;
+		
+		$fields=array();
+		$execData=array();
+		foreach ($this->_changedFields as $field){
+			$fields[]=self::VariableToDBField($field).'=:'.$field;
+			$execData[':'.$field]=$this->_fields[$field];
+		}
+		
+		if ($this->_id!==0){
+			$sql="UPDATE `{$this->_table}` SET ".implode(", ", $fields)." WHERE `{$this->_key}`=:soi5yh58y";
+			$execData[':soi5yh58y']=$this->_id;
+		}else{
+			$sql="INSERT INTO `{$this->_table}` SET ".implode(", ", $fields);
+		}
+		
+		$prep=self::$PDO->prepare($sql);
+		$prep->execute($execData);
+		
+		/*$errorInfo=$prep->errorInfo();
+		if ($errorInfo[0]!='00000')
+			trigger_error($errorInfo[2]);*/
+		
+		if ($this->_id===0){ // If this is a new object we want to reload fromt he DB to make sure all fields are correct.
+			// In order to do so we need to find the value for the key we're using
+			$id=self::$PDO->lastInsertId();
+			$key=self::$PDO->query("SHOW INDEX FROM `{$this->_table}` WHERE Key_name='PRIMARY'")->fetch()['Column_name'];
+			$this->_id=self::$PDO->query("SELECT `{$this->_key}` FROM `{$this->_table}` WHERE `$key`=$id")->fetchColumn();
+			$this->Load();
+		}
+	}
+}

+ 47 - 0
Model/Navigation.php

@@ -0,0 +1,47 @@
+<?php
+ApplicationSettings::RegisterDefaultSetting("navigation", "mode", "exclude");
+ApplicationSettings::RegisterDefaultSetting("navigation", "pages", "");
+
+require_once 'Controller/Navigation/base/INavigationController.php';
+
+Navigation::Load();
+
+class Navigation {
+	private static $_controllers=array();
+	
+	public static function LoadNavigationController($name){
+		include_once "Controller/Navigation/$name.php";
+		$controller=new $name;
+		self::RegisterPage($name,$controller);
+	}
+
+	public static function Load(){
+		$mode=ApplicationSettings::GetSetting("navigation", "mode");
+		$pages=ApplicationSettings::GetSetting("navigation", "pages");
+		$pageList=explode(",", $pages);
+		switch (strtolower($mode)){
+			case "include":
+				foreach ($pageList as $page)
+					self::LoadNavigationController($page);
+				break;
+			case "exclude":
+				$files=glob("Controller/Navigation/*.php");
+				foreach ($files as $file){
+					$page=basename($file,".php");
+					if (!in_array($page, $pageList))
+						self::LoadNavigationController($page);
+				}
+				break;
+			default:
+				throw new Exception("Invalid navigiation mode: $mode. Please set to include or exclude.");
+		}
+	}
+	
+	public static function RegisterPage($name, INavigationController $contoller){
+		self::$_controllers[$name]=$contoller;
+	}
+	
+	public static function Get(){
+		return self::$_controllers;
+	}
+}

+ 26 - 0
Model/URI.php

@@ -0,0 +1,26 @@
+<?php
+class URI {
+	private $_text,$_link,$_image;
+	
+	public function __construct($text, $link, $image="") {
+		$this->_text=$text;
+		$this->_link=$link;
+		$this->_image=$image;
+	}
+	
+	public function GetText(){
+		return $this->_text;
+	}
+	
+	public function GetLinkLocation(){
+		return $this->_link;
+	}
+	
+	public function GetAbsoluteLocation(){
+		throw new BadFunctionCallException("GetAbsoluteLocation is yet to be implemented");
+	}
+	
+	public function GetImage() {
+		return $this->_image;
+	}
+}

+ 40 - 0
Tests/Controller/E404Test.php

@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * Generated by PHPUnit_SkeletonGenerator on 2014-12-04 at 12:32:32.
+ */
+class E404Test extends PHPUnit_Framework_TestCase {
+
+	/**
+	 * @var E404
+	 */
+	protected $object;
+
+	/**
+	 * Sets up the fixture, for example, opens a network connection.
+	 * This method is called before a test is executed.
+	 */
+	protected function setUp() {
+		$this->object=new E404;
+	}
+
+	/**
+	 * Tears down the fixture, for example, closes a network connection.
+	 * This method is called after a test is executed.
+	 */
+	protected function tearDown() {
+		
+	}
+
+	/**
+	 * @covers E404::Index
+	 * @todo   Implement testIndex().
+	 */
+	public function testIndex() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+}

+ 95 - 0
Tests/Model/DBObjectTest.php

@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * Generated by PHPUnit_SkeletonGenerator on 2014-12-04 at 12:32:38.
+ */
+class DBObjectTest extends PHPUnit_Framework_TestCase {
+
+	/**
+	 * @var DBObject
+	 */
+	protected $object;
+
+	/**
+	 * Sets up the fixture, for example, opens a network connection.
+	 * This method is called before a test is executed.
+	 */
+	protected function setUp() {
+		$this->object=new DBObject;
+	}
+
+	/**
+	 * Tears down the fixture, for example, closes a network connection.
+	 * This method is called after a test is executed.
+	 */
+	protected function tearDown() {
+		
+	}
+
+	/**
+	 * @covers DBObject::VariableToDBField
+	 * @todo   Implement testVariableToDBField().
+	 */
+	public function testVariableToDBField() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+	/**
+	 * @covers DBObject::DBFieldToVariable
+	 * @todo   Implement testDBFieldToVariable().
+	 */
+	public function testDBFieldToVariable() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+	/**
+	 * @covers DBObject::__get
+	 * @todo   Implement test__get().
+	 */
+	public function test__get() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+	/**
+	 * @covers DBObject::__set
+	 * @todo   Implement test__set().
+	 */
+	public function test__set() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+	/**
+	 * @covers DBObject::Load
+	 * @todo   Implement testLoad().
+	 */
+	public function testLoad() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+	/**
+	 * @covers DBObject::Save
+	 * @todo   Implement testSave().
+	 */
+	public function testSave() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+}

+ 56 - 0
Tests/base/ApplicationSettingsTest.php

@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * Generated by PHPUnit_SkeletonGenerator on 2014-12-05 at 12:28:24.
+ */
+class ApplicationSettingsTest extends PHPUnit_Framework_TestCase {
+
+	/**
+	 * Sets up the fixture, for example, opens a network connection.
+	 * This method is called before a test is executed.
+	 */
+	protected function setUp() {
+	}
+
+	/**
+	 * Tears down the fixture, for example, closes a network connection.
+	 * This method is called after a test is executed.
+	 */
+	protected function tearDown() {
+		
+	}
+
+	/**
+	 * @covers ApplicationSettings::ReloadSettings
+	 * @todo   Implement testReloadSettings().
+	 */
+	public function testReloadSettings() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+	/**
+	 * @covers ApplicationSettings::GetSetting
+	 * @todo   Implement testGetSetting().
+	 */
+	public function testGetSetting() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+	/**
+	 * @covers ApplicationSettings::RegisterDefaultSetting
+	 * @todo   Implement testRegisterDefaultSetting().
+	 */
+	public function testRegisterDefaultSetting() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+}

+ 51 - 0
Tests/base/ApplicationTest.php

@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * Generated by PHPUnit_SkeletonGenerator on 2014-12-04 at 12:32:37.
+ */
+class ApplicationTest extends PHPUnit_Framework_TestCase {
+
+	/**
+	 * @var Application
+	 */
+	protected $object;
+
+	/**
+	 * Sets up the fixture, for example, opens a network connection.
+	 * This method is called before a test is executed.
+	 */
+	protected function setUp() {
+		$this->object=new Application("");
+	}
+
+	/**
+	 * Tears down the fixture, for example, closes a network connection.
+	 * This method is called after a test is executed.
+	 */
+	protected function tearDown() {
+		
+	}
+
+	/**
+	 * @covers Application::GetOutput
+	 * @todo   Implement testGetOutput().
+	 */
+	public function testGetOutput() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+	/**
+	 * @covers Application::FindControllerPath
+	 * @todo   Implement testFindControllerPath().
+	 */
+	public function testFindControllerPath() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+}

+ 67 - 0
Tests/base/HelperTest.php

@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * Generated by PHPUnit_SkeletonGenerator on 2014-12-04 at 12:32:35.
+ */
+class HelperTest extends PHPUnit_Framework_TestCase {
+	/**
+	 * Sets up the fixture, for example, opens a network connection.
+	 * This method is called before a test is executed.
+	 */
+	protected function setUp() {
+	}
+
+	/**
+	 * Tears down the fixture, for example, closes a network connection.
+	 * This method is called after a test is executed.
+	 */
+	protected function tearDown() {
+		
+	}
+
+	/**
+	 * @covers Helper::TableMaker
+	 * @todo   Implement testTableMaker().
+	 */
+	public function testTableMaker() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+	/**
+	 * @covers Helper::IsValidEmail
+	 * @todo   Implement testIsValidEmail().
+	 */
+	public function testIsValidEmail() {
+		$validEmail="rob@robware.uk";
+		$invalidEmail="rob@*";
+		
+		$this->assertEquals(true,Helper::IsValidEmail($validEmail));
+		$this->assertEquals(false,Helper::IsValidEmail($invalidEmail));
+	}
+
+	/**
+	 * @covers Helper::DeleteCookie
+	 * @todo   Implement testDeleteCookie().
+	 */
+	public function testDeleteCookie() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+	/**
+	 * @covers Helper::GetMaxUploadString
+	 * @todo   Implement testGetMaxUploadString().
+	 */
+	public function testGetMaxUploadString() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+}

+ 40 - 0
Tests/base/ViewTest.php

@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * Generated by PHPUnit_SkeletonGenerator on 2014-12-04 at 12:32:34.
+ */
+class ViewTest extends PHPUnit_Framework_TestCase {
+
+	/**
+	 * @var View
+	 */
+	protected $object;
+
+	/**
+	 * Sets up the fixture, for example, opens a network connection.
+	 * This method is called before a test is executed.
+	 */
+	protected function setUp() {
+		$this->object=new View;
+	}
+
+	/**
+	 * Tears down the fixture, for example, closes a network connection.
+	 * This method is called after a test is executed.
+	 */
+	protected function tearDown() {
+		
+	}
+
+	/**
+	 * @covers View::GetHTMLFromTemplate
+	 * @todo   Implement testGetHTMLFromTemplate().
+	 */
+	public function testGetHTMLFromTemplate() {
+		// Remove the following lines when you implement this test.
+		$this->markTestIncomplete(
+			'This test has not been implemented yet.'
+		);
+	}
+
+}

+ 25 - 0
Tests/bootstrap.php

@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+/**
+ * @author robert.marshall
+ */
+// TODO: check include path
+//ini_set('include_path', ini_get('include_path'));
+chdir("..");
+$a=getcwd();
+$files=glob("base/*.php");
+foreach ($files as $file)
+		require_once $file;
+
+spl_autoload_register(function($class){
+	if (file_exists("../Model/$class.php"))
+		require_once("../Model/$class.php");
+});
+
+?>

+ 5 - 0
View/E404/index.view

@@ -0,0 +1,5 @@
+@Title{404 Not Found}@
+@Header{404 Not Found - page unavailable}@
+@Body{
+	This page cannot be found.
+}@

+ 4 - 0
View/Home/index.view

@@ -0,0 +1,4 @@
+@Title{Home}@
+@Body{
+	Home page
+}@

+ 84 - 0
base/Application.php

@@ -0,0 +1,84 @@
+<?php
+
+spl_autoload_register(function($class){
+	if (file_exists("Interfaces/$class.php")){
+		include("Interfaces/$class.php");
+		return;
+	}
+	if (file_exists("Model/$class.php"))
+		include("Model/$class.php");
+});
+
+/*include_once 'ApplicationSettings.php';
+include_once 'View.php';
+include_once 'Helper.php';
+include_once 'IConvertible.php';*/
+
+$files=glob("base/*.php");
+foreach ($files as $file)
+		require_once $file;
+
+class Application{
+	private $_view, $_controller;
+	
+	function __construct($url) {
+//		session_start();
+		
+		$page=ApplicationSettings::GetSetting("general", "default_page");
+		$action="Index";
+		$params=array();
+		
+		$parts=explode("/", $url, 3);
+		switch (count($parts)){
+			case 3:
+				if ($parts[2]!='')
+					$params=explode("/",$parts[2]);
+			case 2:
+				//$action=ucfirst($parts[1]);
+				$action=$parts[1];
+			case 1:
+				if ($parts[0]!="")
+					$page=$parts[0];
+				$controller=self::FindControllerPath($page);
+				if ($controller===false){
+					$controller="Controller/E404.php";
+					$params=array($url);
+				}
+				break;
+		}
+		
+		foreach ($_GET as $key=>$_)
+			$params[$key]=filter_input(INPUT_GET, $key);
+		foreach ($_POST as $key=>$_)
+			$params[$key]=filter_input(INPUT_POST, $key);
+		//$params['cookies']=array();;
+		//foreach ($_COOKIE as $key=>$_)
+		//	$params['cookies'][$key]=filter_input(INPUT_COOKIE, $key);
+		
+		include_once $controller;
+		$this->_controller=new $page();
+		if (!method_exists($this->_controller, $action))
+			$action="Index";
+		$this->_view=$this->_controller->$action($params);
+	}
+	
+	public function GetOutput(){
+		if (gettype($this->_view)=="object")
+			return $this->_view->GetHTMLFromTemplate(ApplicationSettings::GetSetting("general", "template"));
+		switch (gettype($this->_view)){
+			case "string":
+			case "integer":
+				return $this->_view;
+		}
+		return "";
+	}
+	
+	public static function FindControllerPath($controller){
+		$controllerLower=strtolower($controller);
+		$files=glob("Controller/*.php");
+		foreach ($files as $file)
+			if (strtolower(basename($file,".php"))==$controllerLower)
+				return $file;
+		return false;
+	}
+}

+ 68 - 0
base/ApplicationSettings.php

@@ -0,0 +1,68 @@
+<?php
+
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+/**
+ * Description of ApplicationSettings
+ *
+ * @author robert.marshall
+ */
+class ApplicationSettings {
+	const SETTINGS_PATH="settings.ini";
+	
+	private static $DEFAULT_SETTINGS=array(
+		"general"=>array(
+			"default_page"=>"E404",
+			"template"=>"template.php"
+		)
+	);
+	private static $_settings;
+	
+	private static function PopulateSettingsWithDefaults(){
+		if (self::$_settings==null)
+			self::$_settings=array();
+		
+		foreach (self::$DEFAULT_SETTINGS as $category=>$settings){
+			if (!isset(self::$_settings[$category]))
+				self::$_settings[$category]=self::$DEFAULT_SETTINGS[$category];
+			else
+				foreach ($settings as $key=>$value)
+					if (!isset(self::$_settings[$category][$key]))
+						self::$_settings[$category][$key]=$value;
+		}
+	}
+	
+	public static function ReloadSettings(){
+		if (file_exists(self::SETTINGS_PATH)){
+			try {
+				self::$_settings=parse_ini_file(self::SETTINGS_PATH,true);
+			} catch(Exception $e){
+				throw new Exception("Unable to load settings.ini", 0, $e);
+			}
+		}
+		self::PopulateSettingsWithDefaults();	
+	}
+
+	public static function GetSetting($category,$setting){
+		if (self::$_settings==null)
+			self::ReloadSettings();
+		
+		if (!isset(self::$_settings[$category]))
+			throw new Exception("Settings category '$category' has not been defined.");
+		if (!isset(self::$_settings[$category][$setting]))
+			throw new Exception("Setting '$setting' has not been defined.");
+		
+		return self::$_settings[$category][$setting];
+	}
+	
+	public static function RegisterDefaultSetting($category,$setting,$value){
+		if(!isset(self::$DEFAULT_SETTINGS[$category]))
+			self::$DEFAULT_SETTINGS[$category]=array();
+		self::$DEFAULT_SETTINGS[$category][$setting]=$value;
+		self::ReloadSettings();
+	}
+}

+ 24 - 0
base/DBScriptRunner.php

@@ -0,0 +1,24 @@
+<?php
+//Let's see if we've got a database set up in settings.ini
+try{
+	ApplicationSettings::GetSetting("database", "database");
+} catch(Exception $e){
+	return;
+}
+
+include_once 'Model/DBObject.php';
+
+class DBScriptRunner{
+	public static function RunAllScripts(){
+		$schemas=glob("DB Scripts/*.sql");
+
+		$PDO=DBObject::GetPDO();
+		foreach ($schemas as $schema)
+			$PDO->query(file_get_contents($schema));
+	}
+	
+	public static function RunScript($file) {
+		$PDO=DBObject::GetPDO();
+		$PDO->query(file_get_contents("DB Scripts/".$file));
+	}
+}

+ 33 - 0
base/ErrorHandling.php

@@ -0,0 +1,33 @@
+<?php
+//Let's see if we've got a database set up in settings.ini
+try{
+	ApplicationSettings::GetSetting("database", "database");
+} catch(Exception $e){
+	return;
+}
+
+ApplicationSettings::RegisterDefaultSetting("error_handling", "enabled", FALSE);
+if (ApplicationSettings::GetSetting("error_handling", "enabled")!=true)
+	return;
+
+$GLOBALS['php_errors']=array();
+
+set_error_handler(function($errNo, $errStr, $errFile, $errLine, $errContext){
+	LogError($errNo,$errStr,$errFile,$errLine,print_r($errContext,true));
+	//return false;
+});
+
+set_exception_handler(function(Exception $ex){
+	LogError($ex->getCode(), $ex->getMessage(), $ex->getFile(), $ex->getLine(), $ex->getTraceAsString());
+});
+
+function LogError($code, $message, $file, $line, $extraData){
+	$obj=new DBObject("error_log", "error_id", 0);
+	$obj->Code=$code;
+	$obj->Message=$message;
+	$obj->File=$file;
+	$obj->Line=$line;
+	$obj->ExtraData=$extraData;
+	$obj->Save();
+	$GLOBALS['php_errors'][]=$obj;
+}

+ 73 - 0
base/Helper.php

@@ -0,0 +1,73 @@
+<?php
+class Helper{
+	const METRES_TO_FEET=3.2808399;
+	const KILOMETRES_TO_MILES=0.621371192;
+	
+	public static function TableMaker($data,$buttonText="Submit",$action="",$method="post") {
+		ob_start();
+		echo '<form action="',$action,'" method="',$method,'"><table>';
+		foreach ($data as $datum){
+			if (!isset($datum['value']))
+				$datum['value']="";
+			if (!isset($datum['type']))
+				$datum['type']="text";
+			
+			$html="";
+			switch ($datum['type']) {
+				case "select":
+					$html="<select id=\"$datum[name]\" name=\"$datum[name]\">";
+					foreach ($datum['options'] as $value=>$option){
+						$html.="<option value=\"$value\"";
+						if ($value==$datum['value'])
+							$html.="selected=\"selected\"";
+						$html.=">$option</option>";
+					}
+					$html.="</select>";
+					break;
+				case "checkbox":
+					$checked="";
+					if ($datum['checked'])
+						$checked='checked="checked"';
+					$html="<input type=\"checkbox\" id=\"$datum[name]\" name=\"$datum[name]\" $checked />";
+					break;
+				default:
+					$html="<input id=\"$datum[name]\" name=\"$datum[name]\" type=\"$datum[type]\" value=\"$datum[value]\" />";
+					break;
+			}
+			if ($datum['type']=="hidden"){
+				echo $html;
+				continue;
+			}
+				
+			echo '<tr><td><label for="',$datum['name'],'">',$datum['display'],': </label></td>',
+				'<td>',$html,'</td></tr>';
+		}
+		echo '<tr><td></td><td><input name="submit_form" type="submit" value="',$buttonText,'" /></td></tr>',
+			'</table></form>';
+		return ob_get_clean();
+	}
+	
+	public static function IsValidEmail($email){
+		if( !preg_match("/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/",$email))
+			return false;
+		return true;
+		list($userName, $mailDomain)=explode("@", $email);
+		if (checkdnsrr($mailDomain,"MX"))
+			return true;
+		else
+			return false;
+	}
+	
+	public static function DeleteCookie($cookie){
+		unset($_COOKIE[$cookie]);
+		setcookie($cookie,null,-1,'/');
+	}
+	
+	public static function GetMaxUploadString(){
+		$maxUpload=(int)ini_get('upload_max_filesize');
+		$maxPost=(int)ini_get('post_max_size');
+		$memoryLimit=(int)ini_get('memory_limit');
+		$size=min($maxUpload, $maxPost, $memoryLimit);
+		return $size.'MB';
+	}
+}

+ 57 - 0
base/View.php

@@ -0,0 +1,57 @@
+<?php
+$files=glob('Converters/*');
+//var_dump($files);
+foreach ($files as $file)
+	include_once $file;
+
+class View{
+	private $_view,$_variables;
+	
+	public function __construct($view, $variables=array()) {
+		$this->_view=$view;
+		$this->_variables=$variables;
+	}
+	
+	// This is here to force the IConvertible interface use
+	private static function DoConversion(IConvertible $converter, $item){
+		return $converter->Convert($item);
+	}
+	
+	public function GetHTMLFromTemplate($template) {
+		$viewContent=file_get_contents('View/'.$this->_view);
+		$htmlParts=array();
+		
+		// Make the sent variables available to the view
+		foreach ($this->_variables as $var=>$value)
+			eval('$'.$var.'=$this->_variables[\''.$var.'\'];'.PHP_EOL);
+		
+		// Break the view up to be applied to the template
+		$matches=array();
+		preg_match_all('/@(.*)\{(.*)\}@/sU', $viewContent,$matches);
+		for ($i=0;$i<count($matches[0]);$i++)
+			$htmlParts[$matches[1][$i]]=$matches[2][$i];
+		
+		// Populate the template
+		$templateHTML=file_get_contents($template);
+		$code=preg_replace_callback("/\{@(.*)\}/", function($match) use ($htmlParts){
+			if (isset($htmlParts[$match[1]]))
+				return $htmlParts[$match[1]];
+			return "";
+		}, $templateHTML);
+		
+		// Process the new PHP
+		file_put_contents("output.php", $code);
+		ob_start();
+		eval('?>'.$code.'<?php ');
+		$output=ob_get_clean();
+		
+		// Do the convertible tags
+		$output=preg_replace_callback('/<convertible type="(.*)">(.*)<\/convertible>/sU', function($match) {
+			if (class_exists($match[1],false))
+				return self::DoConversion(new $match[1](), $match[2]);
+			return $match[2];
+		}, $output);
+		
+		return $output;
+	}
+}

+ 9 - 0
images/blog.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<path fill="#CFD8DC" d="M20,2H4C2.9,2,2.01,2.9,2.01,4L2,22l4-4h14c1.1,0,2-0.9,2-2V4C22,2.9,21.1,2,20,2z"/>
+<path fill="#607D8B" d="M6,9h12v2H6V9z M14,14H6v-2h8V14z M18,8H6V6h12V8z"/>
+<path fill="none" d="M0,0h24v24H0V0z"/>
+</svg>

BIN
images/chevron.png


+ 10 - 0
images/home.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<path fill="none" d="M0,0h24v24H0V0z"/>
+<rect x="9.375" y="13.292" fill="#795548" width="5.333" height="6.708"/>
+<polygon fill="#D7CCC8" points="5,12 5,20 10,20 10,14 14,14 14,20 19,20 19,12 19.021,12 12.01,5.691 "/>
+<polygon fill="#F44336" points="12,3 2,12 5,12 12.01,5.691 19.021,12 22,12 "/>
+</svg>

BIN
images/logo.png


+ 13 - 0
images/photo.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<path fill="none" d="M0,0h24v24H0V0z"/>
+<path fill="#BBDEFB" d="M21,19V5c0-1.1-0.9-2-2-2H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14C20.1,21,21,20.1,21,19z M8.5,13.5
+	l2.5,3.01L14.5,12l4.5,6H5L8.5,13.5z"/>
+<g>
+	<polygon fill="#43A047" points="19,17.979 5,17.979 8.5,13.479 11,16.489 14.5,11.979 	"/>
+	<polygon fill="#66BB6A" points="5,17.979 8.5,13.479 12.234,17.979 	"/>
+</g>
+</svg>

+ 19 - 0
images/projects.svg

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<g>
+	<path fill="#4E342E" d="M19.001,3.016H14.82c-0.42-1.16-1.521-2-2.819-2s-2.4,0.84-2.82,2h-4.18c-1.1,0-2,0.9-2,2v14
+		c0,1.101,0.9,2,2,2h14c1.101,0,2-0.899,2-2v-14C21.001,3.916,20.102,3.016,19.001,3.016z"/>
+	<path fill="#D7CCC8" d="M19.71,6.553c0-1.354-1.103-2.454-2.455-2.454H6.79c-1.356,0-2.455,1.099-2.455,2.454V17.23
+		c0,1.354,1.099,2.453,2.455,2.453h10.465c1.354,0,2.455-1.101,2.455-2.453V6.553z"/>
+	<circle fill="#F1F8E9" cx="12.001" cy="4.016" r="1"/>
+</g>
+<path fill="none" d="M0,0h24v24H0V0z"/>
+<rect x="7" y="16" fill="#455A64" width="7" height="2"/>
+<rect x="7" y="12" fill="#455A64" width="10" height="2"/>
+<rect x="7" y="8" fill="#455A64" width="10" height="2"/>
+<path fill="#FFA000" d="M7.011,6.213l9.984,0.031l0.002-1.236v-2h-2.18c-0.42-1.16-1.521-2-2.82-2s-2.4,0.84-2.82,2h-2.18v2
+	L7.011,6.213z M11.997,3.008c0.55,0,1,0.45,1,1s-0.45,1-1,1s-1-0.45-1-1S11.447,3.008,11.997,3.008z"/>
+</svg>

+ 117 - 0
index.html

@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Robware</title>
+		<meta charset="UTF-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<style type="text/css">
+			@import url(http://fonts.googleapis.com/css?family=Roboto);
+			
+			html,body,h1,h2 {
+				padding:0;
+				margin:0;
+			}
+			
+			html{
+				font-family: 'Roboto', sans-serif;
+			}
+			
+			body{
+				width:100%;
+				background:#f9f9f9;
+			}
+						
+			nav {
+				padding:10px;
+			}
+			
+			h1, h2{
+			}
+			
+			#menu{
+				background:#f9f9f9;
+				width:240px;
+				left:0;
+				position:fixed;
+				height:100%;
+				box-shadow: 7px 0px 4px 0px rgba(0,0,0,0.15);
+			}
+			
+			#main{
+				float:left;
+				padding-left:240px;
+				width:100%;
+			}
+			
+			#content{
+				padding:10px;
+			}
+			
+			#footer{
+				padding:20px;
+			}
+			
+			.header {
+				height:64px;
+				background:#F44336;
+				color:white;
+				width:100%;
+				white-space:nowrap;
+			}
+			
+			.header>*{
+				line-height:64px;
+				font-size:24px;
+				font-weight:400;
+				float:left;
+				padding-left:10px;
+			}
+			
+			.header>img{
+				height:40px;
+				margin-top:12px;
+			}
+			
+			.header span{
+				float:left;
+			}
+			
+			.header span:not(:first-child):before{
+				margin:2px 10px 0 10px;
+				width:6px;
+				float:left;
+				height:62px;
+				background: url("chevron.png") no-repeat scroll right center / contain  rgba(0, 0, 0, 0);
+				content:"";
+			}
+		</style>
+	</head>
+	<body>
+		<div id="menu">
+			<div style="position: relative; overflow:auto; height:100%">
+				<div class="header" style="postition:fixed">
+					<img src="logo.png" />
+					<h1>Robware</h1>
+				</div>
+				<nav>
+					<dl>
+						<dt>Page</dt>
+						<div class="sub-pages">
+							<dd><a href="">Sub Page</a></dd>
+						</div>
+					</dl>
+				</nav>
+			</div>
+		</div>
+		<div id="main">
+			<div class="header">
+				<h2>
+					<span>Style</span>
+					<span>Colour</span>
+				</h2>
+			</div>
+			<div id="content">{@Body}</div>
+			<div id="footer">{@Footer}</div>
+		</div>
+	</body>
+</html>

+ 12 - 0
index.php

@@ -0,0 +1,12 @@
+<?php
+include_once "base/Application.php";
+$url="";
+if (!isset($_GET['__url'])){
+	if (!isset($_SERVER['PATH_INFO']))
+		$_SERVER['PATH_INFO']="/";
+} else {
+	$url=filter_input(INPUT_GET, $_GET['__url']);
+}
+$url=ltrim($_SERVER['PATH_INFO'],'/');
+$app=new Application($url);
+echo $app->GetOutput();

+ 0 - 0
nbproject/licenseheader.txt


+ 22 - 0
nbproject/project.properties

@@ -0,0 +1,22 @@
+auxiliary.org-netbeans-modules-php-phpunit.bootstrap_2e_create_2e_tests=false
+auxiliary.org-netbeans-modules-php-phpunit.bootstrap_2e_enabled=true
+auxiliary.org-netbeans-modules-php-phpunit.bootstrap_2e_path=Tests/bootstrap.php
+auxiliary.org-netbeans-modules-php-phpunit.configuration_2e_enabled=false
+auxiliary.org-netbeans-modules-php-phpunit.configuration_2e_path=
+auxiliary.org-netbeans-modules-php-phpunit.customSuite_2e_enabled=false
+auxiliary.org-netbeans-modules-php-phpunit.customSuite_2e_path=
+auxiliary.org-netbeans-modules-php-phpunit.phpUnit_2e_enabled=false
+auxiliary.org-netbeans-modules-php-phpunit.phpUnit_2e_path=
+auxiliary.org-netbeans-modules-php-phpunit.test_2e_groups_2e_ask=false
+auxiliary.org-netbeans-modules-php-phpunit.test_2e_run_2e_all=false
+include.path=${php.global.include.path}
+php.version=PHP_54
+project.license=default
+project.licensePath=./nbproject/licenseheader.txt
+source.encoding=UTF-8
+src.dir=.
+tags.asp=false
+tags.short=false
+test.src.dir=Tests
+testing.providers=PhpUnit
+web.root=.

+ 9 - 0
nbproject/project.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://www.netbeans.org/ns/project/1">
+    <type>org.netbeans.modules.php.project</type>
+    <configuration>
+        <data xmlns="http://www.netbeans.org/ns/php-project/1">
+            <name>Robware Material</name>
+        </data>
+    </configuration>
+</project>

+ 72 - 0
output.php

@@ -0,0 +1,72 @@
+<?php
+function FormatURI(URI $uri){
+	$image=$uri->GetImage();
+	$imageHTML="";
+	if ($image!="")
+		$imageHTML='<img src="'.$uri->GetImage().'" />';
+	return '<a href="'.$uri->GetLinkLocation().'">'.$imageHTML.$uri->GetText().'</a>';
+}
+?>
+
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Home | Robware</title>
+		<meta charset="UTF-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<link href="/style.css" rel="stylesheet" type="text/css" />
+		<style type="text/css">
+			
+		</style>
+		<script type="text/javascript">
+			
+		</script>
+	</head>
+	<body>
+		<div id="main-header">
+			<div class="header">
+				<h2>
+					<span>Home</span>
+					<?php
+						$__crumbs=Breadcrumbs::GetAll();
+						foreach ($__crumbs as $crumb)
+							echo '<span>',$crumb['text'],'</span>';
+					?>
+				</h2>
+			</div>
+		</div>
+		<div id="menu">
+			<div>
+				<div class="header">
+					<img src="/images/logo.png" />
+					<h1>Robware</h1>
+				</div>
+				<div id="nav-container">
+					<nav>
+						<dl>
+							<?php					
+							$__controllers=Navigation::Get();
+							foreach ($__controllers as $controller){
+								echo '<dt>',FormatURI($controller->GetURI()),'</dt>';
+								$items=$controller->GetItems();
+								if (count($items)>0){
+									echo '<div class="sub-pages">';
+									foreach ($items as $uri)
+										echo '<dd>',FormatURI($uri),'</dd>';
+									echo '</div>';
+								}
+							}
+							?>
+						</dl>
+					</nav>
+				</div>
+			</div>
+		</div>
+		<div id="main">
+			<div id="content">
+	Home page
+</div>
+			<div id="footer"></div>
+		</div>
+	</body>
+</html>

+ 14 - 0
settings.ini

@@ -0,0 +1,14 @@
+[general]
+error_handling=true
+default_page=Home
+template=template.php
+
+[database]
+host=localhost
+username=root
+password=""
+database=php-mvc
+
+[navigation]
+mode=include
+pages=HomeNav,BlogNav,ProjectsNav

+ 114 - 0
style.css

@@ -0,0 +1,114 @@
+@import url(http://fonts.googleapis.com/css?family=Roboto);
+
+html,body,h1,h2 {
+	padding:0;
+	margin:0;
+}
+
+html{
+	font-family: 'Roboto', sans-serif;
+}
+
+body{
+	width:100%;
+	background:#f9f9f9;
+}
+
+nav img{
+	vertical-align: middle;
+	margin-right: 5px;
+}
+
+nav dt:not(:first-child){
+	margin-top:3px;
+}
+
+nav a{
+	color: black;
+	text-decoration: none;
+}
+
+#menu{
+	background:#f9f9f9;
+	width:240px;
+	left:0;
+	position:fixed;
+	height:100%;
+	box-shadow: 7px 0px 4px 0px rgba(0,0,0,0.15);
+}
+
+#menu>div{
+	position: relative;
+	height:100%
+}
+
+#menu>div>*{
+	padding-left:10px;
+}
+
+#nav-container{
+	position:fixed;
+	top:64px;
+	bottom:0;
+	width:230px;
+	overflow: auto;
+}
+
+#main{
+	padding-left:240px;
+	padding-top:64px;
+}
+
+#main>*{
+	padding-left:20px !important;
+}
+
+#main-header{
+	position: fixed;
+	padding-left: 240px;
+	left:0;
+	right:0;
+	box-shadow: 0 5px 10px rgba(249,249,249, 0.9);
+}
+
+#main-header .header>*{
+	padding-left: 20px !important;
+}
+
+#content{
+	padding:20px;
+}
+
+.header {
+	height:64px;
+	background:#F44336;
+	color:white;
+	white-space:nowrap;
+	padding:0 !important;
+}
+
+.header>*{
+	line-height:64px;
+	font-size:24px;
+	font-weight:400;
+	float:left;
+	padding-left: 10px;
+}
+
+.header>img{
+	height:40px;
+	margin-top:12px;
+}
+
+.header span{
+	float:left;
+}
+
+.header span:not(:first-child):before{
+	margin:2px 10px 0 10px;
+	width:6px;
+	float:left;
+	height:62px;
+	background: url("/images/chevron.png") no-repeat scroll right center / contain  rgba(0, 0, 0, 0);
+	content:"";
+}

+ 70 - 0
template.php

@@ -0,0 +1,70 @@
+<?php
+function FormatURI(URI $uri){
+	$image=$uri->GetImage();
+	$imageHTML="";
+	if ($image!="")
+		$imageHTML='<img src="'.$uri->GetImage().'" />';
+	return '<a href="'.$uri->GetLinkLocation().'">'.$imageHTML.$uri->GetText().'</a>';
+}
+?>
+
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>{@Title} | Robware</title>
+		<meta charset="UTF-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<link href="/style.css" rel="stylesheet" type="text/css" />
+		<style type="text/css">
+			{@CSS}
+		</style>
+		<script type="text/javascript">
+			{@JavaScript}
+		</script>
+	</head>
+	<body>
+		<div id="main-header">
+			<div class="header">
+				<h2>
+					<span>{@Title}</span>
+					<?php
+						$__crumbs=Breadcrumbs::GetAll();
+						foreach ($__crumbs as $crumb)
+							echo '<span>',$crumb['text'],'</span>';
+					?>
+				</h2>
+			</div>
+		</div>
+		<div id="menu">
+			<div>
+				<div class="header">
+					<img src="/images/logo.png" />
+					<h1>Robware</h1>
+				</div>
+				<div id="nav-container">
+					<nav>
+						<dl>
+							<?php					
+							$__controllers=Navigation::Get();
+							foreach ($__controllers as $controller){
+								echo '<dt>',FormatURI($controller->GetURI()),'</dt>';
+								$items=$controller->GetItems();
+								if (count($items)>0){
+									echo '<div class="sub-pages">';
+									foreach ($items as $uri)
+										echo '<dd>',FormatURI($uri),'</dd>';
+									echo '</div>';
+								}
+							}
+							?>
+						</dl>
+					</nav>
+				</div>
+			</div>
+		</div>
+		<div id="main">
+			<div id="content">{@Body}</div>
+			<div id="footer">{@Footer}</div>
+		</div>
+	</body>
+</html>