Commits

kiri  committed a0e693a

initial commit

  • Participants
  • Parent commits 1f5e496

Comments (0)

Files changed (48)

File components/agent.php

+<?php
+/**
+ * Helper for detecting user agents.
+ *
+ * @version 		1.0.1
+ * @author 			Kiripolszky Károly <karcsi@ekezet.com>
+ * @package 		page
+ * @subpackage 	components
+ */
+
+/**
+ * User agent detection class.
+ *
+ * @package 		page
+ * @subpackage 	components
+ */
+class AgentComponent extends Loadable
+{
+	/**
+	 * The inspected user agent string.
+	 *
+	 * @var string
+	 */
+	private $userAgent = "";
+	function getUserAgent() { return $this->userAgent; }
+
+	/**
+	 * True if client is a mobile browser.
+	 *
+	 * @var boolean
+	 */
+	private $isMobile = false;
+	/**
+	 * @return boolean
+	 */
+	function isMobile() { return $this->isMobile; }
+
+	/**
+	 * Contains information about the browser.
+	 *
+	 * @var array
+	 * @see get_browser()
+	 */
+	private $browser = array();
+	/**
+	 * @return array
+	 */
+	function getBrowser() { return $this->browser; }
+
+	/**
+	 * @see Loadable::initialize()
+	 */
+	function initialize()
+	{
+		$this->update($_SERVER["HTTP_USER_AGENT"]);
+	}
+
+	/**
+	 * Updates client information.
+	 *
+	 * @param string $userAgent Optional user agent string.
+	 */
+	function update($userAgent=null)
+	{
+		$userAgent = empty($userAgent) ? $this->userAgent : $userAgent;
+		$this->userAgent = $userAgent;
+		$this->browser = @get_browser($userAgent, true);
+		$this->isMobile = stripos($userAgent, "Android") !== false;
+		$this->isMobile = $this->isMobile || stripos($userAgent, "iPhone") !== false;
+		$this->isMobile = $this->isMobile || stripos($userAgent, "Palm") !== false;
+		$this->isMobile = $this->isMobile || stripos($userAgent, "Symbian") !== false;
+		$this->isMobile = $this->isMobile || stripos($userAgent, "Mobile") !== false;
+		$this->isMobile = $this->isMobile || stripos($userAgent, "MIDP") !== false;
+		$this->isMobile = $this->isMobile || stripos($userAgent, "CLDC") !== false;
+		$this->isMobile = $this->isMobile || stripos($userAgent, "Kindle") !== false;
+		$this->isMobile = $this->isMobile || stripos($userAgent, "Windows CE") !== false;
+		$this->isMobile = $this->isMobile || stripos($userAgent, "PPC") !== false;
+		$this->isMobile = $this->isMobile || stripos($userAgent, "Windows Phone") !== false;
+	}
+
+	/**
+	 * Magic properties for browser name testing.
+	 *
+	 * examples: $isIE, $isChrome, $isFirefox, etc.
+	 *
+	 * @param string $name
+	 * @return int
+	 * @see http://uk3.php.net/manual/en/misc.configuration.php#ini.browscap
+	 */
+	function __get($name)
+	{
+		if (strlen($name) > 2 && substr($name, 0, 2) === "is")
+		{
+			$what = substr($name, 2);
+			return intval($this->browser['browser'] === $what);
+		}
+		return 0;
+	}
+}
+?>

File components/html.php

+<?php
+/**
+ * Standard helper for creating HTML.
+ *
+ * @author 			Kiripolszky Károly <karcsi@ekezet.com>
+ * @package 		page
+ * @subpackage 	components
+ */
+
+/**
+ * Helper class for generating HTML quickly.
+ *
+ * @package 		page
+ * @subpackage 	components
+ */
+class HtmlComponent extends Loadable
+{
+	protected $scripts = array();
+	protected $stylesheets = array();
+
+	/**
+	 * Returns a string of the proper tag closing characters.
+	 *
+	 * @return string
+	 * @uses page::contentType()
+	 */
+	private function endTag()
+	{
+		$isXHTML = true;
+		if (is_a($this->parent, "PageController"))
+		{
+			$isXHTML = in_array(page::contentType(), array("xhtml", "html5"));
+		}
+		return $isXHTML ? " />" : ">";
+	}
+
+	/**
+	 * Creates proper XML attribute list from an associative array having its
+	 * attribute names and their values.
+	 *
+	 * @param array $a
+	 * @return string
+	 */
+	public function toXmlAttrs($attrs)
+	{
+		if (empty($attrs) || !is_array($attrs))
+			return "";
+		$tmp = array();
+		foreach ($attrs as $k => $v)
+		{
+			$tmp[] = "$k=\"$v\"";
+		}
+		return " " . implode(" ", $tmp);
+	}
+
+	/**
+	 * Creates a proper content-type metatag.
+	 *
+	 * @param boolean $return
+	 * @return mixed Depends on return.
+	 * @uses page::charset()
+	 * @uses page::mimeType()
+	 * @uses e()
+	 */
+	function contentType($return = false)
+	{
+		if (page::contentType() == "html5")
+			$s = sprintf('<meta charset="%s" />', page::charset());
+		else
+			$s = sprintf('<meta http-equiv="Content-Type" content="%s; charset=%s"' . $this->endTag(), page::mimeType(), page::charset());
+		return e("$s\n", $return);
+	}
+
+	/**
+	 * Creates link tags for including the styleheets.
+	 *
+	 * @param boolean $return
+	 * @return mixed Depends on return.
+	 * @uses $stylesheets
+	 * @uses e()
+	 */
+	function stylesheets($return = false)
+	{
+		if (empty($this->stylesheets))
+			return false;
+		$s = "";
+		foreach ($this->stylesheets as $id => $stylesheet)
+		{
+			$attrs = $stylesheet['attrs'];
+			if (!isset($attrs['rel']))
+				$attrs['rel'] = "stylesheet";
+			if (!isset($attrs['type']))
+				$attrs['type'] = "text/css";
+			$s .= sprintf('<link%s href="%s"' . $this->endTag(), $this->toXmlAttrs($attrs), uri($stylesheet['uri']));
+			$s .= "\n";
+		}
+		return e($s, $return);
+	}
+
+	/**
+	 * Add a stylesheet to the page.
+	 *
+	 * @param string $uri The Unique Resource Identifier.
+	 * @param array $attrs Additional attributes.
+	 * @uses $stylesheets
+	 */
+	function addStylesheet($uri, $attrs = array())
+	{
+		$this->stylesheets[md5($uri)] = array('uri' => $uri, 'attrs' => $attrs);
+	}
+
+	/**
+	 * @return array
+	 * @uses $stylesheets
+	 */
+	function getStylesheets()
+	{
+		return $this->stylesheets;
+	}
+
+	/**
+	 * @param array $stylesheets
+	 * @uses $stylesheets
+	 */
+	function setStylesheets($stylesheets)
+	{
+		$this->stylesheets = $stylesheets;
+	}
+
+	/**
+	 * Creates link tags for including the scripts.
+	 *
+	 * @param boolean $return
+	 * @return mixed Depends on return.
+	 * @uses $scripts
+	 * @uses e()
+	 */
+	function scripts($return = false)
+	{
+		if (empty($this->scripts))
+			return false;
+		$s = "";
+		foreach ($this->scripts as $id => $script)
+		{
+			$attrs = $script['attrs'];
+			if ($this->contentType === "html5" && isset($attrs['type']))
+				unset($attrs['type']);
+			$s .= sprintf("<script%s></script>", $this->toXmlAttrs($attrs));
+			$s .= "\n";
+		}
+		return e($s, $return);
+	}
+
+	/**
+	 * Add a script to the page.
+	 *
+	 * @param string $uri The Unique Resource Identifier.
+	 * @param array $attrs Additional attributes.
+	 * @uses $scripts
+	 */
+	function addScript($uri, $attrs = array())
+	{
+		$this->scripts[md5($uri)] = array('uri' => $uri, 'attrs' => $attrs);
+	}
+
+	/**
+	 * @return array
+	 * @uses $scripts
+	 */
+	function getScripts()
+	{
+		return $this->scripts;
+	}
+
+	/**
+	 * @param array $scripts
+	 * @uses $cripts
+	 */
+	function setScripts($scripts)
+	{
+		$this->scripts = $scripts;
+	}
+
+	/**
+	 * Renders a reusable element (a piece of template).
+	 *
+	 * @param string $name Element name.
+	 * @param array $vars Template variables.
+	 * @param boolean $return
+	 * @return mixed False on error.
+	 * @uses $parent
+	 * @uses Controller::getHelpers()
+	 * @uses Geg::render()
+	 */
+	function element($name, $vars = array(), $return = false)
+	{
+		if (!is_a($this->parent, 'Controller'))
+		{
+			return e("<!-- HtmlComponent.element() FAIL [$name] -->", $return);
+		}
+		$vars["me"] = $this->parent;
+		$ecs = $this->parent->getHelpers();
+		if (!empty($ecs))
+			foreach ($ecs as $ec_name => $ec_obj)
+				$vars[$ec_name] = $ec_obj;
+		$doc = Geg::render('elements' . DS . $name, $vars, $return);
+		if (empty($doc))
+			return e("<!-- HtmlComponent.element() RENDER ERROR [$name] -->", $return);
+		return $doc;
+	}
+
+	/**
+	 * Creates an anchor tag.
+	 *
+	 * @param string $uri A relative or absolute URI.
+	 * @param string $name Becomes the URI when omitted.
+	 * @param array $attrs Additional attributes.
+	 * @param bool $return
+	 * @return mixed Depends on return.
+	 * @uses e()
+	 */
+	function link($uri = 'javascript:;', $name = false, $attrs = array(), $return = false)
+	{
+		$name = empty($name) ? $uri : $name;
+		if ($name == $uri && strlen($uri) >= 60)
+			$name = substr($name, 0, 59) . "&#x2026;";
+		$s = sprintf('<a href="%s"%s>%s</a>', uri($uri), $this->toXmlAttrs($attrs), $name);
+		return e($s, $return);
+	}
+
+	/**
+	 * Creates and anchor tag with a static URL.
+	 *
+	 * @param string $uri A relative or absolute URI.
+	 * @param string $name Becomes the URI when omitted.
+	 * @param array $attrs Additional attributes.
+	 * @param bool $return
+	 * @return mixed Depends on return.
+	 * @uses e()
+	 */
+	function links($uri, $name = false, $attrs = array(), $return = false)
+	{
+		$prefix = Config::get('app_url');
+		if (empty($prefix))
+			return $this->link($uri, $name, $attrs, $return);
+		$name = empty($name) ? $uri : $name;
+		if ($name == $uri && strlen($uri) >= 60)
+			$name = substr($name, 0, 59) . "&#x2026;";
+		$s = sprintf('<a href="%s"%s>%s</a>', $prefix . $uri, $this->toXmlAttrs($attrs), $name);
+		return e($s, $return);
+	}
+
+	/**
+	 * Creates a markup tag.
+	 *
+	 * @param string $name Name of tag.
+	 * @param array $attrs Array of attributes.
+	 * @param string $inner_html Set to an empty value for standalone tags like br
+	 * and hr.
+	 * @return string
+	 * @uses e()
+	 */
+	function tag($name, $inner_html = "", $attrs = array(), $return = true)
+	{
+		$s = "";
+		if ($inner_html === "")
+			$s = sprintf("<%s%s%s", $name, $this->toXmlAttrs($attrs), $this->endTag());
+		else
+			$s = sprintf("<%s%s>%s</%s>", $name, $this->toXmlAttrs($attrs), $inner_html, $name);
+		return e($s, $return);
+	}
+
+	/**
+	 * Creates an image tag.
+	 *
+	 * @param string $src URI of the image.
+	 * @param array $attrs Additional attributes.
+	 * @param bool $return
+	 * @return mixed Depends on return.
+	 * @uses e()
+	 */
+	function img($src, $attrs = array(), $return = false)
+	{
+		if (!isset($attrs['alt']))
+			$attrs['alt'] = basename($src);
+		$uri = uri($src);
+		if (function_exists("getimagesize") && strpos($src, "://") === false)
+		{
+			$imginfo = getimagesize(getcwd().$uri);
+			if ($imginfo !== false)
+			{
+				list($width, $height, $type, $attr) = $imginfo;
+				$attrs['width'] = $width;
+				$attrs['height'] = $height;
+			}
+		}
+		$s = sprintf('<img src="%s"%s' . $this->endTag(), $uri, $this->toXmlAttrs($attrs));
+		return e($s, $return);
+	}
+
+	/**
+	 * Creates a link tag to a favicon.
+	 *
+	 * @param string $src URI of the icon.
+	 * @param array $attrs Additional attributes.
+	 * @param bool $return
+	 * @return mixed Depends on return.
+	 * @uses e()
+	 */
+	function shortcutIcon($src, $type, $return = false)
+	{
+		$s = sprintf('<link rel="shortcut icon" type="%s" href="%s"' . $this->endTag(), $type, uri($src));
+		return e($s, $return);
+	}
+
+}
+?>

File components/session.php

+<?php
+/**
+ * Session data helper.
+ *
+ * @author 			Kiripolszky Károly <karcsi@ekezet.com>
+ * @package 		page
+ * @subpackage 	components
+ */
+
+/**
+ * Session helper class.
+ *
+ * @package 		page
+ * @subpackage 	components
+ */
+class SessionComponent extends Loadable
+{
+	private $data = null;
+
+	/**
+	 * @see Loadable::initialize()
+	 */
+	function initialize()
+	{
+		session_name(Config::get("session_cookie"));
+		session_start();
+		$this->data = $_SESSION;
+	}
+
+	function reset()
+	{
+		$tmp = $this->data;
+		$this->destroy();
+		$this->initialize();
+		$this->data = $tmp;
+		$_SESSION = $tmp;
+	}
+
+	function regenerate()
+	{
+		$this->data = $_SESSION;
+		session_regenerate_id(true);
+		session_id(sha1($_SERVER['REMOTE_ADDR'].$_SERVER['HTTP_USER_AGENT'].microtime()));
+		$_SESSION = $this->data;
+	}
+
+	function finalize()
+	{
+		session_write_close();
+	}
+
+	function destroy()
+	{
+		$_SESSION = null;
+		$this->data = null;
+		session_destroy();
+	}
+
+	function write($key, $value)
+	{
+		$ret = mawrite($key, $value, $this->data);
+		$_SESSION = $this->data;
+		return $ret;
+	}
+
+	function read($key, $default=false)
+	{
+		return maread($key, $this->data, $default);
+	}
+
+	function del($key)
+	{
+		$ret = madel($key, $this->data);
+		$_SESSION = $this->data;
+		return $ret;
+	}
+}
+?>

File components/widgets.php

+<?php
+/**
+ * Widget support for page.
+ *
+ * @author Károly Kiripolszky <karcsi@ekezet.com>
+ * @package page
+ * @subpackage components
+ */
+
+/**
+ * Widget helper component.
+ *
+ * @package page
+ * @subpackage components
+ */
+class WidgetsComponent extends Loadable
+{
+	function render($name, $options=array(), $return=false)
+	{
+		$options['action'] = maread("action", $options, "index");
+		$options['path'] = maread("path", $options, APP_ROOT."widgets".DS.$name.DS.$name.".php");
+		$options['controller'] = maread("controller", $options, $name);
+		$options['className'] = maread("className", $options, cc($name)."Controller");
+
+		/**
+		 * @var PageController
+		 */
+		$ctrl = Lib::load("widgets", $name, $options);
+
+		$ctrl->viewPath = APP_ROOT."widgets".DS.$name.DS."views".DS;
+		$ctrl->doAction();
+		$s = $ctrl->render();
+		return e($s, $return);
+	}
+}
+?>
+<?php
+/**
+ * A static class to hold configuration data.
+ *
+ * @author Kiripolszky Károly <karcsi@ekezet.com>
+ * @package page
+ * @subpackage core
+ */
+
+/**
+ * Configuration class.
+ *
+ * @package page
+ */
+final class Config
+{
+	static private $values = array();
+	static private $loaded = false;
+
+	/**
+	 * Reset all to factory defaults.
+	 *
+	 * {@source 3 26}
+	 */
+	final static public function loadDefaults()
+	{
+		self::$values = array(
+			'debug'   => 0,
+			'charset' => "utf-8",
+			'content_types' => array(
+				"html"   => "text/html",
+				"html5"   => "text/html",
+				"xhtml"  => "application/xhtml+xml",
+				"xml"    => "text/xml",
+				"json"   => "application/json",
+				"css"    => "text/css",
+				"js"     => "text/javascript",
+				"txt"    => "text/plain",
+				"stream" => "application/octet-stream"
+			),
+			'default_title'					=> 'Page',
+			'default_content_type'	=> 'html5',
+			'default_layout'				=> 'default',
+			'database_connection'		=> 'default'
+		);
+	}
+
+	/**
+	 * Includes the files from the config folder.
+	 *
+	 * @param boolean $force Force reloading.
+	 * @return boolean Returns true if config files are found and loaded.
+	 * @uses CONFIG_DIR
+	 */
+	final static public function loadFromDisk($force=false)
+	{
+		if (self::$loaded && !$force) return;
+		self::loadDefaults();
+		$cfg_files = array(
+			CONFIG_DIR."includes.php",
+			CONFIG_DIR."options.php",
+			CONFIG_DIR."urls.php"
+		);
+		foreach ($cfg_files as $cfg)
+		{
+			if (!file_exists($cfg))
+				return false;
+			else
+			 include_once($cfg);
+		}
+		self::$loaded = true;
+		return true;
+	}
+
+	/**
+	 * Returns true if config was loaded from disk.
+	 *
+	 * @return boolean
+	 */
+	final static public function isLoaded()
+	{
+		return self::$loaded;
+	}
+
+	/**
+	 * Gets the value of a configuration option.
+	 *
+	 * @param string $key
+	 * @param mixed $default
+	 * @return mixed
+	 * @see maread()
+	 */
+	final static public function get($key, $default=false)
+	{
+		return maread($key, self::$values, $default);
+	}
+
+	/**
+	 * Sets the value of a configuration option.
+	 *
+	 * @param string $key
+	 * @param mixed $value
+	 * @return boolean
+	 * @see mawrite()
+	 */
+	final static public function set($key, $value)
+	{
+		return mawrite($key, $value, self::$values);
+	}
+
+	/**
+	 * Shortcut for getting debug settings.
+	 *
+	 * @return integer
+	 */
+	final static public function debug()
+	{
+		return (int) self::$values['debug'];
+	}
+
+	/**
+	 * Adds a new content type to be recognized by the system.
+	 *
+	 * @final
+	 * @static
+	 * @param string $alias Short name for type, like "mpg".
+	 * @param string $mime_type Mime type for content, like "video/mpeg".
+	 */
+	final static public function addContentType($alias, $mime_type)
+	{
+		$types = self::get('content_types');
+		$types[$alias] = $mime_type;
+		self::set("content_types", $types);
+	}
+
+	/**
+	 * Tries to guess the mime type of a file.
+	 *
+	 * @param string $path Path to a file.
+	 */
+	final static public function guessMimeType($path)
+	{
+		$info = pathinfo($path, PATHINFO_EXTENSION);
+		return maread($info, self::$values["content_types"], self::$values["stream"]);
+	}
+
+	final private function __construct() { }
+
+	final private function __clone() { }
+}
+?>

File controller.php

+<?php
+/**
+ * The Controller class responds to requests.
+ *
+ * Event callbacks:
+ *
+ *   beforeAction()
+ *   afterAction()
+ *   beforeRender()
+ *   afterRender()
+ *
+ * @author Kiripolszky Károly <karcsi@ekezet.com>
+ * @package page
+ * @subpackage core
+ * @uses Geg
+ * @uses Lib
+ * @uses Config
+ */
+
+/**
+ * Base Controller class.
+ *
+ * @package page
+ * @subpackage core
+ */
+class PageController extends Component
+{
+	/**
+	 * Name of the layout template (without extension).
+	 *
+	 * @var string
+	 */
+	var $layout = "";
+
+	/**
+	 * Name of the controller source (w/o ".php").
+	 *
+	 * @see Controller
+	 * @var string
+	 */
+	var $controller = "";
+
+	/**
+	 * Name of the current action.
+	 *
+	 * @var string
+	 */
+	var $action = "";
+
+	/**
+	 * The view (template) to render. It's {@link $action} by default.
+	 *
+	 * You can set this to render a view other than the action name.
+	 *
+	 * @var string
+	 */
+	var $view = "";
+	/**
+	 * Returns the name of the current view.
+	 *
+	 * @return string
+	 */
+	final public function getView() { return empty($this->view) ? $this->action : $this->view; }
+
+	/**
+	 * Overwrite the template lookup path for this controller.
+	 *
+	 * @var string
+	 */
+	var $viewPath = "";
+
+	/**
+	 * Rendered content.
+	 *
+	 * @var string
+	 */
+	var $content = "";
+
+	/**
+	 * Arguments derived from the request URI.
+	 *
+	 * @see Router
+	 * @var array
+	 */
+	var $args = array();
+
+	/**
+	 * @var string Name of the layout template (without extension).
+	 */
+	var $pageTitle = "";
+
+	/**
+	 * Response charset, eg. "utf-8", etc.
+	 *
+	 * @var string
+	 */
+	var $charset = "";
+	/**
+	 * Returns current response charset.
+	 *
+	 * @return string
+	 */
+	final public function charset() { return $this->charset; }
+
+	/**
+	 * Components exposed to templates.
+	 *
+	 * @var array
+	 */
+	var $helpers = array();
+
+	/**
+	 * Models used by this controller.
+	 *
+	 * @var array
+	 */
+	var $models = array();
+
+	/**
+	 * Template variables.
+	 *
+	 * @var array
+	 */
+	protected $vars = array();
+	/**
+	 * Returns the template variables.
+	 *
+	 * @return array
+	 */
+	final public function getVars() { return $this->vars; }
+	/**
+	 * Sets one or more template variable.
+	 *
+	 * @param string|array $key Variable name or array of variables.
+	 * @param mixed $value Value for the variable.
+	 * @return bool
+	 * @see mawrite()
+	 */
+	final public function set($key, $value=true)
+	{
+		if (is_array($key))
+		{
+			foreach($key as $k => $v)
+				mawrite($k, $v, $this->vars);
+			return true;
+		}
+		return mawrite($key, $value, $this->vars);
+	}
+	/**
+	 * Gets the value of a template variable.
+	 *
+	 * @param string $key
+	 * @return mixed
+	 * @see maread()
+	 */
+	final public function get($key) { return maread($key, $this->vars); }
+
+	/**
+	 * @var array Page HTTP headers.
+	 * @since 0.2
+	 */
+	protected $headers = array();
+
+	/**
+	 * Response content type, like "html", "json", etc.
+	 *
+	 * @var string
+	 * @see Config::loadDefaults()
+	 */
+	protected $contentType = "";
+	/**
+	 * Sets the response content type for the current request.
+	 *
+	 * The content type must already be added to the configuration!
+	 *
+	 * @param string $type Internal content type alias, like "html", "json", etc.
+	 * @param string $charset Set the character set as well. Can be false to omit.
+	 * @return bool True if content type is successfully set.
+	 * @see Config::addContentType()
+	 */
+	final public function setContentType($type, $charset=null)
+	{
+		$types = array_keys(Config::get("content_types"));
+		if (!in_array($type, $types) || empty($type))
+			return false;
+		$this->contentType = $type;
+		$this->charset = empty($charset) ? $this->charset : $charset;
+		return true;
+	}
+	/**
+	 * Returns current response content type.
+	 *
+	 * @return string
+	 */
+	final public function contentType() { return $this->contentType; }
+	/**
+	 * Returns a MIME type for the current request, eg. "text/html".
+	 *
+	 * @return string
+	 * @uses $contentType
+	 */
+	final public function mimeType($type=null)
+	{
+		$types = Config::get("content_types");
+		$type = empty($type) ? $this->contentType : $type;
+		return $types[$type];
+	}
+
+	/**
+	 * Enable/disable HTTP caching for this controller.
+	 *
+	 * @var bool
+	 * @since 0.2
+	 */
+	protected $useCache = true;
+
+
+	/**
+	 * Perform a controller action.
+	 *
+	 * @param string $action Action name or null.
+	 * @uses beforeAction()
+	 * @uses afterAction()
+	 */
+	final public function doAction($action=null)
+	{
+		$action = empty($action) ? $this->action : $action;
+		$this->action = $action;
+		$this->beforeAction();
+		foreach ($this->helpers as $helper_id => $helper_opts)
+			if (is_numeric($helper_id))
+				$this->components []= $helper_opts;
+			else
+				$this->components[$helper_id] = $this->helpers[$helper_id];
+		$this->loadComponents();
+		$this->loadModels();
+		$code = sprintf('$this->%s(%s);', $action, implode(", ", quotes($this->args)));
+		eval($code);
+		$this->afterAction();
+	}
+
+	/**
+	 * Tells the app to setup headers and render the page into {@link $content}.
+	 *
+	 * @param string $view Overrides {@link $action} if not null.
+	 * @param string $layout Overrides {@link $layout} if not null.
+	 * @uses Config
+	 * @uses Geg::render()
+	 */
+	final public function render($view=null, $layout=null)
+	{
+		// setup variables
+		$start_time = microtime(true);
+		$this->beforeRender();
+		$view = empty($view) ? $this->getView() : $view;
+		$layout = empty($layout) ? (empty($this->layout) ? Config::get("default_layout") : $this->layout) : $layout;
+		$this->charset = empty($this->charset) ? Config::get("charset") : $this->charset;
+		$this->setContentType(empty($this->contentType) ? Config::get("default_content_type") : $this->contentType);
+
+		// check if client can accept the response content type
+		if ($this->accepts() == 0)
+		{
+			// look for an appropriate content type
+			$types = Config::get("content_types");
+			$fallback = false;
+
+			foreach ($types as $alias => $mime)
+			{
+				if ($this->accepts($alias))
+				{
+					$this->setContentType($alias);
+					$fallback = true;
+					break;
+				}
+			}
+
+			if (!$fallback)
+				htthrow(406);
+		}
+
+		// check for charset support
+		if ($this->acceptsCharset() == 0)
+			// charset not supported by client
+			htthrow(406);
+
+		$content_dir = $this->contentType == Config::get("default_content_type") ? DS : DS.$this->contentType.DS;
+		$this->pageTitle = empty($this->pageTitle) ? Config::get("default_title") : $this->pageTitle;
+		$this->set("me", $this);
+
+		// add helper components as template variables
+		$helpers = $this->getHelpers();
+		foreach ($helpers as $name => $obj)
+			$this->vars[$name] = $obj;
+
+		/*
+		 * Rendering below:
+		 */
+
+		$view_path = $this->viewPath.$this->controller.$content_dir.$view;
+		$content_for_layout = Geg::render($view_path, $this->vars, true);
+
+		// this might be the first failed attempt to render the layout content
+		if ($content_for_layout === false)
+		{
+			// ...on error look also in default view path
+			$view_path = $this->viewPath.$this->controller.DS.$view;
+			$content_for_layout = Geg::render($view_path, $this->vars, true);
+		}
+		// did the rendering fail again?
+		if ($content_for_layout === false && get_class($this) !== "ErrorController")
+		{
+			// if debug mode is on throw view_not_found error
+			if (Config::debug() > 0)
+				page::launchErrorController("view_not_found");
+			// ...otherwise it's the ultimate error
+			else
+				page::launchErrorController("not_found");
+		}
+
+		$this->set("content_for_layout", $content_for_layout);
+
+		// rendering layout...
+		$layout_path = $this->viewPath."layout".$content_dir.$layout;
+		$this->content = Geg::render($layout_path, $this->vars, true);
+		if ($this->content === false)
+		{
+			// ...on error look also in default layout path
+			$layout_path = $this->viewPath."layout".DS.$layout;
+			$this->content = Geg::render($layout_path, $this->vars, true);
+		}
+
+		// afterRender callback may modify the content
+		$this->afterRender();
+
+		// controller content self-check
+		if ($this->content === false)
+		{
+			// layout_not_found error
+			if (Config::debug() > 0)
+				page::launchErrorController("layout_not_found", array("layout"=>$layout));
+			// ultimate render error
+			else
+				page::launchErrorController("not_found");
+		}
+
+		// check cache and generate output
+		$this->checkCache(Geg::getPath($view_path), $this->content);
+		$this->addHeader("Content-Type: ".$this->mimeType()."; charset=".$this->charset);
+		if (Config::debug() == 0)
+			$this->addHeader("Content-Length: ".strlen($this->content), true, 200);
+		$time = microtime(true) - $start_time;
+		if (Config::debug() > 0 && in_array($this->contentType, array("html5", "html", "xhtml", "xml")))
+			$this->content .= "<!-- $time s -->";
+	}
+
+	/**
+	 * Returns true if HTTP_X_REQUESTED_WITH is set.
+	 *
+	 * @return bool
+	 */
+	final public function isAjax()
+	{
+		return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
+			(strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest');
+	}
+
+	/**
+	 * Returns a positive float larger than 0.0 if client accepts a certain content type.
+	 *
+	 * @param string $type Internal content type alias. Overrides {@link $contentType} if not null.
+	 * @return float Quality value (q parameter).
+	 * @see Config::addContentType()
+	 * @uses $contentType
+	 */
+	final public function accepts($type=null)
+	{
+		if (!isset($_SERVER['HTTP_ACCEPT']))
+			return 1.0;
+		$type = empty($type) ? $this->contentType : $type;
+		$types = explode(',', $_SERVER['HTTP_ACCEPT']);
+		foreach ($types as $mime)
+		{
+			$q = 1.0;
+			$duel = explode(";", $mime);
+			if (isset($duel[1]))
+				$q = floatval(substr(trim($duel[1]), 2));
+			if (fnmatch(trim($duel[0]), $this->mimeType($type)) && $q > 0)
+				return $q;
+		}
+		return 0.0;
+	}
+
+	/**
+	 * Checks if a charset is accepted by the client.
+	 *
+	 * @param string $charset Set to {@link $charset) if null.
+	 * @return float Quality value (q parameter).
+	 * @uses $charset
+	 */
+	final public function acceptsCharset($charset=null)
+	{
+		if (!isset($_SERVER['HTTP_ACCEPT_CHARSET']))
+			return true;
+		$charset = empty($charset) ? strtoupper($this->charset) : $charset;
+		$charsets = explode(',', $_SERVER['HTTP_ACCEPT_CHARSET']);
+		foreach ($charsets as $encoding)
+		{
+			$q = 1.0;
+			$duel = explode(";", $encoding);
+			if (isset($duel[1]))
+				$q = floatval(substr(trim($duel[1]), 2));
+			if (fnmatch(trim($duel[0]), $charset) && $q > 0)
+				return $q;
+		}
+		return 0.0;
+	}
+
+	/**
+	 * Returns an associative array of the components used as helpers.
+	 *
+	 * @return array Array of {@link Component} objects.
+	 * @uses $helpers
+	 * @uses cc()
+	 * @uses Lib::load()
+	 */
+	final public function getHelpers()
+	{
+		$ret = array();
+		if (!empty($this->helpers))
+			foreach ($this->helpers as $helper)
+			{
+				if (!is_string($helper))
+					continue;
+				if (isset($this->components[$helper])
+					&& is_array($this->components[$helper])
+					&& isset($this->components[$helper]['attachName']))
+					$member = $this->components[$helper]['attachName'];
+				elseif (is_string($helper) && in_array($helper, $this->components))
+					$member = cc($helper);
+				$ret[$helper] = $this->{$member};
+			}
+		return $ret;
+	}
+
+	/**
+	 * Loads the models for this controller.
+	 *
+	 * @since 0.3
+	 * @uses $models
+	 * @uses Component::setParent()
+	 * @uses cc()
+	 * @uses Lib::load()
+	 */
+	final public function loadModels()
+	{
+		if (!is_array($this->models) || empty($this->models))
+			return;
+		foreach ($this->models as $model_id => $model_opts)
+		{
+			$options = array();
+			$model_name = $model_id;
+			if (is_numeric($model_id))
+			{
+				$model_name = $model_opts;
+				$options['attachName'] = cc($model_opts);
+				$options['className'] = $options['attachName'] . 'Model';
+			} elseif (is_string($model_id))
+			{
+				$options = $model_opts;
+				$options['attachName'] = empty($options['attachName']) ? cc($model_id) : $options['attachName'];
+				$options['className'] = empty($options['className']) ? $options['attachName'] . 'Model' : $options['className'];
+			}
+			if (!property_exists($this, $options['attachName']))
+			{
+				$model = Lib::load("models", $model_name, $options);
+				$model->setParent($this);
+				$this->{$options['attachName']} = $model;
+			}
+		}
+	}
+
+	/**
+	 * Sends prepared HTTP headers to client.
+	 *
+	 * @since 0.2
+	 * @uses $headers
+	 */
+	final public function sendHeaders()
+	{
+		foreach ($this->headers as $header)
+			header($header['string'], $header['replace'], $header['code']);
+	}
+
+	/**
+	 * Adds a HTTP header to queue.
+	 *
+	 * @param string $string
+	 * @param bool $replace
+	 * @param int $code
+	 * @since 0.2
+	 * @uses $headers
+	 */
+	final public function addHeader($string, $replace=true, $code=null)
+	{
+		$this->headers []= array(
+			'string'   => $string,
+			'replace'	 => $replace,
+			'code'		 => $code
+		);
+	}
+
+	/**
+	 * Updates HTTP cache control headers.
+	 *
+	 * @param string $path Path to file.
+	 * @param string $content File contents.
+	 * @since 0.2
+	 * @uses addHeader()
+	 */
+	final protected function checkCache($path, $content)
+	{
+		if (Config::debug() == 0 && $this->useCache)
+		{
+			$view_cache_time = filemtime($path);
+			$http_view_cache_time = gmdate("D, d M Y H:i:s", $view_cache_time)." GMT";
+			$etag = md5($content);
+			if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $etag === $_SERVER['HTTP_IF_NONE_MATCH'])
+				htthrow(304);
+			if (isset($_SERVER['HTTP_IF_MATCH']) && $etag !== $_SERVER['HTTP_IF_MATCH'])
+				htthrow(304);
+			if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $http_view_cache_time == $_SERVER['HTTP_IF_MODIFIED_SINCE'])
+				htthrow(304);
+			$this->addHeader("Last-Modified: ".$http_view_cache_time);
+			$this->addHeader("Etag: ".$etag);
+		}
+	}
+
+	/**
+	 * Common presentation action has to be defined.
+	 */
+	public function index()
+	{
+		// override this
+	}
+
+	/**
+	 * This method is called before performing an action.
+	 */
+	public function beforeAction() { }
+
+	/**
+	 * This method is called after an action has been performed.
+	 */
+	public function afterAction() { }
+
+	/**
+	 * This method is called before rendering a page.
+	 */
+	public function beforeRender() { }
+
+	/**
+	 * This method is called after a page has been rendered.
+	 */
+	public function afterRender() { }
+}
+?>

File controllers/error.php

+<?php
+/**
+ * Controller for displaying error messages.
+ *
+ * @author Kiripolszky Károly <karcsi@ekezet.com>
+ * @package page
+ * @subpackage controllers
+ */
+
+/**
+ * Base error controller.
+ *
+ * @package page
+ * @subpackage controllers
+ */
+class ErrorController extends Controller
+{
+	var $code = 0;
+	var $layout = "error";
+	var $components = array("html");
+	var $exposedComponents = array("html");
+
+	function beforeRender()
+	{
+		if (empty($this->pageTitle))
+			$this->pageTitle = "error!";
+		$this->Html->addStylesheet("/css/error.css");
+	}
+
+	function index()
+	{
+	}
+
+	function not_found()
+	{
+		$this->pageTitle = "page not found!";
+	}
+
+	function controller_not_found()
+	{
+		$this->pageTitle = "controller not found!";
+	}
+
+	function action_not_found()
+	{
+		$this->pageTitle = "action not found!";
+	}
+
+	function view_not_found()
+	{
+		$this->pageTitle = "view not found!";
+	}
+
+	function layout_not_found()
+	{
+		$this->pageTitle = "layout not found!";
+	}
+}
+?>

File controllers/pages.php

+<?php
+/**
+ * A basic controller for serving pages.
+ *
+ * @author Kiripolszky Károly <karcsi@ekezet.com>
+ * @package page
+ * @subpackage controllers
+ */
+
+/**
+ * Pages controller.
+ *
+ * @package page
+ * @subpackage controllers
+ */
+class PagesController extends Controller
+{
+	function index()
+	{
+	}
+}
+?>

File controllers/widgets.php

+<?php
+/**
+ * WidgetsController is an attempt to implement plug-n-play site functionality.
+ *
+ * A widget is basically a Controller and some views bundled together.
+ *
+ * @author Károly Kiripolszky <karcsi@ekezet.com>
+ * @package page
+ * @subpackage controllers
+ * @since 0.3
+ */
+
+/**
+ * Widgets controller.
+ *
+ * @package page
+ * @subpackage controllers
+ */
+class WidgetsController extends PageController
+{
+	var $layout = "widget";
+
+	/**
+	 * Assets management.
+	 *
+	 * @uses addHeader()
+	 * @uses sendHeaders()
+	 * @uses checkCache()
+	 * @uses Config::guessMimeType()
+	 */
+	function index()
+	{
+		$widget = array_shift($this->args);
+		$path = APP_ROOT."widgets".DS.$widget.DS."webroot".DS.implode(DS, $this->args);
+		$mime = Config::guessMimeType($path);
+		$s = file_get_contents($path);
+		$this->addHeader("content-type: $mime; charset=".page::charset());
+		$this->addHeader("content-length: ".strlen($s), true, 200);
+		$this->checkCache($path, $s);
+		$this->sendHeaders();
+		die($s);
+	}
+}
+?>

File database.php

+<?php
+/**
+ * Database connection handling.
+ *
+ * @author Károly Kiripolszky <karcsi@ekezet.com>
+ * @package page
+ * @subpackage core
+ * @since 0.3
+ */
+
+/**
+ * Adapter factory class.
+ *
+ * @package page
+ * @see Adapter
+ * @uses Adapter
+ */
+final class Database
+{
+	/**
+	 * Connection handle.
+	 *
+	 * @var array
+	 */
+	private static $connections = null;
+
+	/**
+	 * Returns a database adapter object.
+	 *
+	 * @param mixed $dckey Selects database connection settings. Set it to null to use configured value.
+	 * @param string $type Set it to null to use configured value.
+	 * @param string $host Set it to null to use configured value.
+	 * @param string $user Set it to null to use configured value.
+	 * @param string $password Set it to null to use configured value.
+	 * @param string $dbname Set it to null to use configured value.
+	 * @param string $charset Set it to null to use configured value.
+	 * @return DatabaseAdapter
+	 * @uses Config
+	 * @uses $connection
+	 */
+	public static function &get($dckey=null, $type=null, $host=null, $user=null, $password=null, $name=null, $charset=null)
+	{
+		// get database settings from config
+		$dckey = empty($dckey) ? Config::get("database_connection") : empty($dckey);
+
+		// create new adapter class
+		if (!is_a(self::$connections[$dckey], "DatabaseAdapter"))
+		{
+			$args = func_get_args();
+			self::$connections[$dckey] =& self::createConnection($dckey, $args);
+		// check and update connection
+		} elseif (!self::$connections[$dckey]->isConnected())
+			self::$connections[$dckey]->connect();
+
+		return self::$connections[$dckey];
+	}
+
+	private static function createConnection($dckey, $args)
+	{
+		$dbcfg = Config::get("Database.$dckey");
+
+		// prepare arguments for creating an adapter object
+		$dbparams = array('type', 'host', 'user', 'password', 'name', 'charset');
+		if (1 < count($args))
+			// strip out $dckey
+			$args = array_slice($args, 1);
+		$vars = array();
+		foreach ($dbparams as $i => $param)
+			$vars[$param] = isset($args[$i]) ? $args[$i] :
+				(isset($dbcfg[$param]) ? $dbcfg[$param] : '');
+		// creating variables in the symbol table
+		extract($vars);
+
+		$cls = $type."Adapter";
+		if (!class_exists($cls))
+		{
+			$module_path = LIB_DIR."database".DS.strtolower($type).".php";
+			@require_once($module_path);
+		}
+
+		return new $cls($host, $user, $password, $name, $charset);
+	}
+
+	final private function __construct() { }
+
+	final private function __clone() { }
+}
+
+
+/**
+ * Abstract connection class for dealing with different database engines.
+ *
+ * @package page
+ */
+abstract class DatabaseAdapter
+{
+	/**
+	 * @var resource Connection handle.
+	 */
+	protected $connection = false;
+
+	/**
+	 * @var string
+	 */
+	public $dbname = "";
+
+	/**
+	 * @var string
+	 */
+	public $host = "";
+
+	/**
+	 * @var string
+	 */
+	public $user = "";
+
+	/**
+	 * @var string
+	 */
+	public $password = "";
+
+	/**
+	 * @var string Connection charset.
+	 */
+	public $charset = "";
+
+	/**
+	 * Last error thrown by the server.
+	 *
+	 * @var string
+	 */
+	public $lastError = "";
+
+	/**
+	 * The last executed query.
+	 *
+	 * @var string
+	 */
+	public $lastQuery = "";
+
+	final public function __construct($host, $user, $password, $dbname, $charset)
+	{
+		$this->host = $host;
+		$this->user = $user;
+		$this->password = $password;
+		$this->dbname = $dbname;
+		$this->charset = $charset;
+	}
+
+	/**
+	 * Connect to server.
+	 *
+	 * @return bool True on success.
+	 */
+	abstract public function connect();
+
+	/**
+	 * Disconnect from server.
+	 */
+	abstract public function disconnect();
+
+	/**
+	 * Returns true if connected.
+	 *
+	 * @return bool
+	 */
+	final public function isConnected() { return !empty($this->connection); }
+
+	/**
+	 * Executes a database query and sets both {@link $lastQuery} and {@link $lastError}.
+	 *
+	 * @param string $s Query string.
+	 * @param bool $plain Return a numeric array instead of an associative.
+	 * @return array
+	 * @uses $lastQuery
+	 * @uses $lastError
+	 */
+	abstract public function query($s, $numeric=false);
+
+	/**
+	 * Returns the id of the last inserted record.
+	 *
+	 * @return mixed
+	 */
+	abstract public function lastInsertID();
+
+	/**
+	 * Returns the last executed query.
+	 *
+	 * @return string
+	 * @uses $lastQuery
+	 */
+	final public function lastQuery() { return $this->lastQuery; }
+
+	/**
+	 * Returns the last database error or empty.
+	 *
+	 * @return string
+	 * @uses $lastError
+	 */
+	final public function lastError() { return $this->lastError; }
+}
+?>

File database/mysql.php

+<?php
+/**
+ * Database connector class for MySQL.
+ *
+ * @author Károly Kiripolszky <karcsi@ekezet.com>
+ * @package page
+ * @subpackage database
+ * @since 0.3
+ */
+
+/**
+ * MySQL connector class.
+ *
+ * @package page
+ */
+class MySQLAdapter extends DatabaseAdapter
+{
+	function connect()
+	{
+		try  {
+			$this->link = mysql_connect($this->host, $this->user, $this->password);
+		} catch (Exception $e)
+		{
+			return false;
+		}
+		mysql_select_db($this->dbname, $this->link);
+		mysql_query("SET NAMES '{$this->charset}'", $this->link);
+		return true;
+	}
+
+	function disconnect()
+	{
+		mysql_close($this->link);
+		$this->link = false;
+	}
+
+	function query($s, $index_field=false)
+	{
+		$this->lastError = "";
+		$this->lastQuery = $s;
+		if (!$this->isConnected())
+			$this->connect();
+
+		$tmp = mysql_query($s, $this->link);
+		if (empty($tmp))
+		{
+			$this->lastError = mysql_error($this->link);
+			return false;
+		}
+		$ret = array();
+		if (empty($index_field))
+			while ($row = @mysql_fetch_array($tmp, MYSQL_ASSOC))
+				$ret []= $row;
+		else
+			while ($row = @mysql_fetch_array($tmp, MYSQL_ASSOC))
+				$ret[$row[$index_field]] = $row;
+		@mysql_free_result($tmp);
+		return $ret;
+	}
+
+	function lastInsertID()
+	{
+		if (!$this->isConnected()) return false;
+		return mysql_insert_id($this->link);
+	}
+}
+?>

File database/null.php

+<?php
+/**
+ * Dummy database connector class for disabling database connection.
+ *
+ * @author Károly Kiripolszky <karcsi@ekezet.com>
+ * @package page
+ * @subpackage database
+ * @since 0.3
+ */
+
+/**
+ * Null connector class.
+ *
+ * @package page
+ */
+class NullAdapter extends DatabaseAdapter
+{
+	function connect()
+	{
+		return true;
+	}
+
+	function disconnect()
+	{
+	}
+
+	function query($s, $index_field=false)
+	{
+		$this->lastQuery = $s;
+		return array();
+	}
+
+	function lastInsertID()
+	{
+		return false;
+	}
+}
+?>

File etc/create.cmd

+@echo off
+echo Creating application skeleton...
+mkdir ..\..\config
+mkdir ..\..\controllers
+mkdir ..\..\database
+mkdir ..\..\webroot
+mkdir ..\..\webroot\css
+mkdir ..\..\webroot\img
+mkdir ..\..\webroot\js
+mkdir ..\..\webroot\files
+mkdir ..\..\views
+mkdir ..\..\views\error
+mkdir ..\..\views\layout
+mkdir ..\..\views\pages
+copy /Y skeleton\*.* ..\..
+copy /Y skeleton\webroot\*.* ..\..\webroot
+copy /Y skeleton\webroot\css\*.* ..\..\webroot\css
+copy /Y skeleton\webroot\img\*.* ..\..\webroot\img
+copy /Y skeleton\webroot\img\page-icon-16x16.png ..\..\webroot\favicon.png
+copy /-Y skeleton\config\urls.default.php ..\..\config\urls.php
+copy /-Y skeleton\config\options.default.php ..\..\config\options.php
+copy /-Y skeleton\config\includes.default.php ..\..\config\includes.php
+copy /Y ..\controllers\pages.php ..\..\controllers\pages.php
+copy /Y ..\database\*.* ..\..\database
+copy /Y ..\views\error\*.* ..\..\views\error
+copy /Y ..\views\layout\*.* ..\..\views\layout
+copy /Y ..\views\pages\*.* ..\..\views\pages
+echo done.
+echo NOTE: Please redefine LIB_DIR in "webroot/index.php"!
+pause

File etc/setup.php

+<?php
+/**
+ * A very basic install script. You may have to modify the .htaccess file in /etc.
+ *
+ * @author Károly Kiripolszky <karcsi@ekezet.com>
+ * @package page
+ * @subpackage skeleton
+ */
+
+header("content-type: text/html; charset=utf-8");
+define("DS", DIRECTORY_SEPARATOR);
+define("DEFAULT_PATH", "");
+$project = isset($_POST['proj']) && !empty($_POST['proj']) ? $_POST['proj'] : "Untitled";
+$path = isset($_POST['path']) && !empty($_POST['path']) ? $_POST['path'] : DEFAULT_PATH;
+if (strlen($path)>1 && substr($path, -1, 1) != DS)
+    $path .= DS;
+
+function slog($s)
+{
+  echo sprintf("[%s] %s\n", date("H:i:s"), $s);
+}
+
+function replaceInFile($path, $vars)
+{
+  $subject = @file_get_contents($path);
+  if ($subject === false)
+    return false;
+  $data = str_replace(array_keys($vars), array_values($vars), $subject);
+  return @file_put_contents($path, $data);
+}
+
+function updateFile($path, $vars)
+{
+  if (replaceInFile($path, $vars))
+    slog("ok: ".$path);
+  else
+    slog("<strong>fail:</strong> ".$path);
+}
+
+function createFolders($path)
+{
+  $folders = array(
+    "config",
+    "controllers",
+    "models",
+    "webroot",
+    "webroot".DS."css",
+    "webroot".DS."img",
+    "webroot".DS."js",
+    "webroot".DS."files",
+    "views",
+    "views".DS."layout",
+    "views".DS."pages"
+  );
+  foreach ($folders as $folder)
+  {
+    $dir = $path.$folder;
+    slog("Creating folder: " . $dir);
+    if (@mkdir($dir))
+      slog("ok");
+    else
+      slog("<strong>fail</strong>");
+  }
+}
+
+function copyFile($src, $dst, $overwrite=false)
+{
+  slog("Copying: $src\n\t\t-> $dst");
+  if (file_exists($dst) && !$overwrite)
+  {
+    slog("<strong>fail</strong>: Destination exists: " . $dst);
+    return;
+  }
+  if (@copy($src, $dst))
+    slog("ok");
+  else
+    slog("<strong>fail</strong>");
+}
+
+function copyFiles($path)
+{
+  $skeleton = "skeleton";
+  $skeleton_folders = array(
+    "",
+    "webroot".DS,
+    "webroot".DS."css".DS,
+    "webroot".DS."img".DS
+  );
+  $source_folders = array(
+    "controllers".DS,
+    "views".DS."layout".DS,
+    "views".DS."pages".DS
+  );
+  $root = $skeleton.DS;
+  foreach ($skeleton_folders as $folder)
+  {
+    $files = glob($root.$folder."*");
+    foreach ($files as $file)
+    {
+      if (is_dir($file))
+        continue;
+      $dest = $path . str_replace($root, "", $file);
+      copyFile($file, $dest);
+    }
+  }
+  copyFile($root.".htaccess", $path.".htaccess");
+  copyFile($root."webroot".DS.".htaccess", $path."webroot".DS.".htaccess");
+  copyFile($root."webroot".DS."img".DS."page-icon-16x16.png", $path."webroot".DS."favicon.png");
+  copyFile($root."config".DS."options.default.php", $path."config".DS."options.php");
+  copyFile($root."config".DS."includes.default.php", $path."config".DS."includes.php");
+  copyFile($root."config".DS."urls.default.php", $path."config".DS."urls.php");
+  $root = "..".DS;
+  foreach ($source_folders as $folder)
+  {
+    $files = glob($root.$folder."*");
+    foreach ($files as $file)
+    {
+      if (is_dir($file))
+        continue;
+      $dest = $path . str_replace($root, "", $file);
+      copyFile($file, $dest);
+    }
+  }
+}
+?>
+<html>
+  <head>
+    <title>Page Setup</title>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+  </head>
+  <body>
+  	<h1>Page Setup</h1>
+    <form method="POST" action="setup.php">
+    	<label for="proj">Project:&nbsp;</label>
+    	<input id="proj" name="proj" type="text" value="<?php echo $project ?>" style="width: 15ex" />
+    	<br />
+    	<label for="path">Path:&nbsp;</label>
+    	<input id="path" name="path" type="text" value="<?php echo $path ?>" style="width: 60ex" />
+    	<br />
+    	<input type="submit" name="submit" value="Okay" />
+    </form>
+    <hr />
+    <pre><?php
+      if (isset($_POST['submit']))
+      {
+        echo "<h2>Setup log</h2>";
+
+        if (empty($project))
+        {
+          echo "<p><strong>ERROR:</strong> Please enter a name for the project!</p>";
+          return;
+        }
+        if (!file_exists($path) || !is_dir($path))
+        {
+          echo "<p><strong>ERROR:</strong> Setup path does not exist or is not a directory!</p>";
+          return;
+        }
+
+        $setup_root = dirname(getcwd()).DS;
+        slog("Setup root: " . $setup_root);
+
+        echo "<h3>Folder structure</h3>";
+        createFolders($path);
+
+        echo "<h3>Source and web files</h3>";
+        copyFiles($path);
+
+        echo "<h3>Finalizing config</h3>";
+        slog("Setting LIB_DIR...");
+        $f = $path."webroot".DS."index.php";
+        updateFile($f, array("/*LIB_DIR*/" => addslashes($setup_root)));
+        slog("Setting project name...");
+        $f = $path."views".DS."layout".DS."default.tpl";
+        updateFile($f, array("(default)" => "($project)"));
+        slog("Setting page title...");
+        $f = $path."config".DS."options.php";
+        updateFile($f, array('"Untitled"' => '"'.$project.'"'));
+        slog("Updating PHPDOC...");
+        $vars = array("@package page" => "@package $project");
+        updateFile($path."controllers".DS."pages.php", $vars);
+        updateFile($path."controllers".DS."error.php", $vars);
+        $vars["@subpackage skeleton"] = "@subpackage core";
+        updateFile($path."controller.php", $vars);
+        $vars["@subpackage skeleton"] = "@subpackage config";
+        updateFile($path."config".DS."options.php", $vars);
+        updateFile($path."config".DS."urls.php", $vars);
+        updateFile($path."config".DS."includes.php", $vars);
+        $vars["@subpackage skeleton"] = "@subpackage views";
+        updateFile($path."views".DS."layout".DS."default.tpl", $vars);
+        updateFile($path."views".DS."layout".DS."error.tpl", $vars);
+        updateFile($path."views".DS."pages".DS."index.tpl", $vars);
+
+        echo "<h3>FIN.</h3>";
+      }
+    ?></pre>
+  </body>
+</html>

File etc/skeleton/.htaccess

+<IfModule mod_rewrite.c>
+  RewriteEngine On
+  RewriteRule ^$   webroot/   [L]
+  RewriteRule (.*) webroot/$1 [L]
+</IfModule>

File etc/skeleton/config/includes.default.php

+<?php
+/**
+ * Third-party includes.
+ *
+ * @author Kiripolszky Károly <karcsi@ekezet.com>
+ * @package page
+ * @subpackage skeleton
+ */
+
+// place your custom includes here!
+?>

File etc/skeleton/config/options.default.php

+<?php
+/**
+ * Default settings.
+ *
+ * @author Kiripolszky Károly <karcsi@ekezet.com>
+ * @package page
+ * @subpackage skeleton
+ */
+
+// debug level
+Config::set('debug', 1);
+// name of session cookie
+Config::set('session_cookie', "PSID");
+
+// default layout file
+Config::set('default_layout', "default");
+// default page title
+Config::set('default_title', "Untitled");
+// default URL of the application for static linking
+Config::set('app_url', "CHANGE_ME");
+
+/*
+ * Internal content types:
+ *
+ *   "html"   == "text/html"
+ *   "html5"  == "text/html"
+ *   "xhtml"  == "application/xhtml+xml"
+ *   "xml"    == "text/xml"
+ *   "json"   == "application/json"
+ *   "txt"    == "text/plain"
+ *   "stream" == "application/octet-stream"
+ *
+ * You can add more types like this:
+ *
+ *   Config::addContentType("mpg", "video/mpeg");
+ */
+// type of content to serve
+Config::set('default_content_type', "html5");
+// content character set
+Config::set('charset', "utf-8");
+
+// database connection settings
+$database = array(
+  'default' => array(
+    'type'	    => "MySQL",
+    'host'	    => "",
+    'user'	    => "",
+    'password'	=> "",
+    'name'	    => "",
+    'charset'		=> "utf8"
+  ),
+  'test'    => array(
+    'type'	    => "MySQL",
+    'host'	    => "127.0.0.1",
+    'user'	    => "root",
+    'password'	=> "",
+    'name'	    => "",
+    'charset'		=> "utf8"
+  )
+);
+Config::set('Database', $database);
+// select a connection
+Config::set('database_connection', "default");
+
+// extension of template files
+Geg::$templateExt = ".tpl";
+// search paths for templates
+Geg::setSearchPaths(array(
+  "views"
+));
+?>

File etc/skeleton/config/urls.default.php

+<?php
+/**
+ * Default URI routing.
+ *
+ * @author Kiripolszky Károly <karcsi@ekezet.com>
+ * @package page
+ * @subpackage skeleton
+ */
+
+Router::plug("/", array("controller" => "pages", "action" => "index"));
+//Router::plug("widgets/*", array("controller" => "widgets", "action" => "index"));
+//Router::plug("*", array("controller" => "pages", "action" => "index"));
+?>

File etc/skeleton/controller.php

+<?php
+/**
+ * Common Controller other Controllers may extend.
+ *
+ * @author Kiripolszky Károly <karcsi@ekezet.com>
+ * @package page
+ * @subpackage skeleton
+ */
+
+/**
+ * Application parent controller.
+ *
+ * @package page
+ * @subpackage skeleton
+ */
+abstract class Controller extends PageController
+{
+  var $components = array("html");
+  var $helpers = array("html", "agent");
+
+  function beforeRender()
+  {
+    $this->Html->addStylesheet("/css/base.css");
+  }
+}
+?>

File etc/skeleton/webroot/.htaccess

+<IfModule mod_rewrite.c>
+  RewriteEngine On
+  RewriteCond %{REQUEST_FILENAME} !-d
+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteRule ^(.*)$ index.php?path=$1 [QSA,L]
+</IfModule>

File etc/skeleton/webroot/css/base.css

+@CHARSET "UTF-8";
+
+*,html {
+  font-family: "Helvetica Neue", Helvetica, "Trebuchet MS", Arial, Verdana,
+    sans-serif;
+  margin: 0;
+  padding: 0;
+}
+
+#container {
+  width: 765px;
+}
+
+body {
+  color: #131313;
+  background-color: #FAFAFA;
+  font-size: 13px;
+  margin: 2em;
+}
+
+p,ul,ol {
+  margin-bottom: .5em;
+}
+
+h1 {
+  color: #0099ff;
+  margin-bottom: .5em;
+}
+
+h2,h3,h4,h5,h6 {
+  margin-bottom: .333em;
+}
+
+a {
+  color: #0080FF;
+  text-decoration: none;
+  background-color: transparent;
+}
+
+a:hover {
+  background-color: #E1F0FF;
+}
+
+ul, ol {
+  margin-left: 2em;
+  line-height: 1.5em;
+}
+
+hr {
+  margin: 1em 0 1em 0;
+}
+
+.pvd-var_dump {
+	font-family: monospace;
+	color: #014992;