<?php
/*
 * File: ratatoeskr/sys/models.php
 * Data models to make database accesses more comfortable.
 * 
 * License:
 * This file is part of Ratatöskr.
 * Ratatöskr is licensed unter the MIT / X11 License.
 * See "ratatoeskr/licenses/ratatoeskr" for more information.
 */

require_once(dirname(__FILE__) . "/db.php");
require_once(dirname(__FILE__) . "/utils.php");
require_once(dirname(__FILE__) . "/../libs/kses.php");
require_once(dirname(__FILE__) . "/textprocessors.php");
require_once(dirname(__FILE__) . "/pluginpackage.php");

db_connect();

/*
 * Array: $imagetype_file_extensions
 * Array of default file extensions for most IMAGETYPE_* constants
 */
$imagetype_file_extensions = array(
	IMAGETYPE_GIF     => "gif",
	IMAGETYPE_JPEG    => "jpg",
	IMAGETYPE_PNG     => "png",
	IMAGETYPE_BMP     => "bmp",
	IMAGETYPE_TIFF_II => "tif",
	IMAGETYPE_TIFF_MM => "tif",
);

/*
 * Variable: $ratatoeskr_settings
 * The global <Settings> object. Can be accessed like an array.
 * Has these fields:
 * 
 * "default_language"        - The Language code of the default language.
 * "comment_visible_default" - True, if comments should be visible by default.
 * "allow_comments_default"  - True, if comments should be allowed by default.
 * "default_section"         - The id of the default <Section>.
 * "comment_textprocessor"   - The textprocessor to be used for comments.
 * "languages"               - Array of activated languages.
 * "last_db_cleanup"         - Timestamp of the last database cleanup.
 */
$ratatoeskr_settings = NULL;

/*
 * Constants: ARTICLE_STATUS_
 * Possible <Article>::$status values.
 * 
 * ARTICLE_STATUS_HIDDEN - Article is hidden (Numeric: 0)
 * ARTICLE_STATUS_LIVE   - Article is visible / live (Numeric: 1)
 * ARTICLE_STATUS_STICKY - Article is sticky (Numeric: 2)
 */
define("ARTICLE_STATUS_HIDDEN", 0);
define("ARTICLE_STATUS_LIVE",   1);
define("ARTICLE_STATUS_STICKY", 2);

/*
 * Class: DoesNotExistError
 * This Exception is thrown by an ::by_*-constructor or any array-like object if the desired object is not present in the database.
 */
class DoesNotExistError extends Exception { }

/*
 * Class: AlreadyExistsError
 * This Exception is thrown by an ::create-constructor or a save-method, if the creation/modification of the object would result in duplicates.
 */
class AlreadyExistsError extends Exception { }

/*
 * Class: NotAllowedError
 */
class NotAllowedError extends Exception { }

/*
 * Class: InvalidDataError
 * Exception that will be thrown, if a object with invalid data (e.g. urlname in this form not allowed) should have been saved / created.
 * Unless something else is said at a function, the exception message is a translation key.
 */
class InvalidDataError extends Exception { }

abstract class BySQLRowEnabled
{
	protected function __construct() {  }
	
	abstract protected function populate_by_sqlrow($sqlrow);
	
	protected static function by_sqlrow($sqlrow)
	{
		$obj = new static();
		$obj->populate_by_sqlrow($sqlrow);
		return $obj;
	}
}

/*
 * Class: KVStorage
 * An abstract class for a KVStorage.
 *
 * See also:
 * 	<PluginKVStorage>, <ArticleExtradata>
 */
abstract class KVStorage implements Countable, ArrayAccess, Iterator
{
	private $keybuffer;
	private $counter;
	private $silent_mode;
	
	private $common_vals;
	
	private $stmt_get;
	private $stmt_unset;
	private $stmt_update;
	private $stmt_create;
	
	final protected function init($sqltable, $common)
	{
		$this->silent_mode = False;
		$this->keybuffer = array();
		
		$selector = "WHERE ";
		$fields = "";
		foreach($common as $field => $val)
		{
			$selector .= "`$field` = ? AND ";
			$fields .= ", `$field`";
			$this->common_vals[] = $val;
		}
		
		$this->stmt_get    = prep_stmt("SELECT `value` FROM `$sqltable` $selector `key` = ?");
		$this->stmt_unset  = prep_stmt("DELETE FROM `$sqltable` $selector `key` = ?");
		$this->stmt_update = prep_stmt("UPDATE `$sqltable` SET `value` = ? $selector `key` = ?");
		$this->stmt_create = prep_stmt("INSERT INTO `$sqltable` (`key`, `value` $fields) VALUES (?,?" . str_repeat(",?", count($common)) . ")");
		
		$get_keys = prep_stmt("SELECT `key` FROM `$sqltable` $selector");
		$get_keys->execute($this->common_vals);
		while($sqlrow = $get_keys->fetch())
			$this->keybuffer[] = $sqlrow["key"];
		
		$this->counter = 0;
	}
	
	/*
	 * Functions: Silent mode
	 * If the silent mode is enabled, the KVStorage behaves even more like a PHP array, i.e. it just returns NULL,
	 * if a unknown key was requested and does not throw an DoesNotExistError Exception.
	 * 
	 * enable_silent_mode  - Enable the silent mode.
	 * disable_silent_mode - Disable the silent mode (default).
	 */
	final public function enable_silent_mode()  { $this->silent_mode = True;  }
	final public function disable_silent_mode() { $this->silent_mode = False; }
	
	/* Countable interface implementation */
	final public function count() { return count($this->keybuffer); }
	
	/* ArrayAccess interface implementation */
	final public function offsetExists($offset) { return in_array($offset, $this->keybuffer); }
	final public function offsetGet($offset)
	{
		if($this->offsetExists($offset))
		{
			$this->stmt_get->execute(array_merge($this->common_vals, array($offset)));
			$sqlrow = $this->stmt_get->fetch();
			$this->stmt_get->closeCursor();
			return unserialize(base64_decode($sqlrow["value"]));
		}
		elseif($this->silent_mode)
			return NULL;
		else
			throw new DoesNotExistError();
	}
	final public function offsetUnset($offset)
	{
		if($this->offsetExists($offset))
		{
			unset($this->keybuffer[array_search($offset, $this->keybuffer)]);
			$this->keybuffer = array_merge($this->keybuffer);
			$this->stmt_unset->execute(array_merge($this->common_vals, array($offset)));
			$this->stmt_unset->closeCursor();
		}
	}
	final public function offsetSet($offset, $value)
	{
		if($this->offsetExists($offset))
		{
			$this->stmt_update->execute(array_merge(array(base64_encode(serialize($value))), $this->common_vals, array($offset)));
			$this->stmt_update->closeCursor();
		}
		else
		{
			$this->stmt_create->execute(array_merge(array($offset, base64_encode(serialize($value))), $this->common_vals));
			$this->stmt_create->closeCursor();
			$this->keybuffer[] = $offset;
		}
	}
	
	/* Iterator interface implementation */
	final public function rewind()  { return $this->counter = 0; }
	final public function current() { return $this->offsetGet($this->keybuffer[$this->counter]); }
	final public function key()     { return $this->keybuffer[$this->counter]; }
	final public function next()    { ++$this->counter; }
	final public function valid()   { return isset($this->keybuffer[$this->counter]); }
}

/*
 * Class: User
 * Data model for Users
 */
class User extends BySQLRowEnabled
{
	private $id;
	
	/*
	 * Variables: Public class properties
	 * 
	 * $username - The username.
	 * $pwhash   - <PasswordHash> of the password.
	 * $mail     - E-Mail-address.
	 * $fullname - The full name of the user.
	 * $language - Users language
	 */
	public $username;
	public $pwhash;
	public $mail;
	public $fullname;
	public $language;
	
	/*
	 * Constructor: create
	 * Creates a new user.
	 * 
	 * Parameters:
	 * 	$username - The username
	 * 	$pwhash   - <PasswordHash> of the password
	 * 
	 * Returns:
	 * 	An User object
	 * 
	 * Throws:
	 * 	<AlreadyExistsError>
	 */
	public static function create($username, $pwhash)
	{
		global $ratatoeskr_settings;
		global $db_con;
		try
		{
			$obj = self::by_name($name);
		}
		catch(DoesNotExistError $e)
		{
			global $ratatoeskr_settings;
			qdb("INSERT INTO `PREFIX_users` (`username`, `pwhash`, `mail`, `fullname`, `language`) VALUES (?, ?, '', '', ?)",
				$username, $pwhash, $ratatoeskr_settings["default_language"]);
			$obj = new self();
			
			$obj->id       = $db_con->lastInsertId();
			$obj->username = $username;
			$obj->pwhash   = $pwhash;
			$obj->mail     = "";
			$obj->fullname = "";
			$obj->language = $ratatoeskr_settings["default_language"];
			
			return $obj;
		}
		throw new AlreadyExistsError("\"$name\" is already in database.");
	}
	
	protected function populate_by_sqlrow($sqlrow)
	{
		$this->id       = $sqlrow["id"];
		$this->username = $sqlrow["username"];
		$this->pwhash   = $sqlrow["pwhash"];
		$this->mail     = $sqlrow["mail"];
		$this->fullname = $sqlrow["fullname"];
		$this->language = $sqlrow["language"];
	}
	
	/*
	 * Constructor: by_id
	 * Get a User object by ID
	 * 
	 * Parameters:
	 * 	$id - The ID.
	 * 
	 * Returns:
	 * 	An User object.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$stmt = qdb("SELECT `id`, `username`, `pwhash`, `mail`, `fullname`, `language` FROM `PREFIX_users` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if(!$sqlrow)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: by_name
	 * Get a User object by username
	 * 
	 * Parameters:
	 * 	$username - The username.
	 * 
	 * Returns:
	 * 	An User object.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_name($username)
	{
		$stmt = qdb("SELECT `id`, `username`, `pwhash`, `mail`, `fullname`, `language` FROM `PREFIX_users` WHERE `username` = ?", $username);
		$sqlrow = $stmt->fetch();
		if(!$sqlrow)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Function: all
	 * Returns array of all available users.
	 */
	public static function all()
	{
		$rv = array();
		
		$stmt = qdb("SELECT `id`, `username`, `pwhash`, `mail`, `fullname`, `language` FROM `PREFIX_users` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$rv[] = self::by_sqlrow($sqlrow);
		
		return $rv;
	}
	
	/*
	 * Function: get_id
	 * Returns:
	 * 	The user ID.
	 */
	public function get_id()
	{
		return $this->id;
	}
	
	/*
	 * Function: save
	 * Saves the object to database
	 * 
	 * Throws:
	 * 	AlreadyExistsError
	 */
	public function save()
	{
		transaction(function()
		{
			$stmt = qdb("SELECT COUNT(*) AS `n` FROM `PREFIX_users` WHERE `username` = ? AND `id` != ?", $this->username, $this->id);
			$sqlrow = $stmt->fetch();
			if($sqlrow["n"] > 0)
				throw new AlreadyExistsError();
			
			qdb("UPDATE `PREFIX_users` SET `username` = ?, `pwhash` = ?, `mail` = ?, `fullname` = ?, `language` = ? WHERE `id` = ?",
				$this->username, $this->pwhash, $this->mail, $this->fullname, $this->language, $this->id);
		});
	}
	
	/*
	 * Function: delete
	 * Deletes the user from the database.
	 * WARNING: Do NOT use this object any longer after you called this function!
	 */
	public function delete()
	{
		transaction(function()
		{
			qdb("DELETE FROM `PREFIX_group_members` WHERE `user` = ?", $this->id);
			qdb("DELETE FROM `PREFIX_users` WHERE `id` = ?", $this->id);
		});
	}
	
	/*
	 * Function: get_groups
	 * Returns:
	 * 	List of all groups where this user is a member (array of <Group> objects).
	 */
	public function get_groups()
	{
		$rv = array();
		$stmt = qdb("SELECT `a`.`id` AS `id`, `a`.`name` AS `name` FROM `PREFIX_groups` `a` INNER JOIN `PREFIX_group_members` `b` ON `a`.`id` = `b`.`group` WHERE `b`.`user` = ?", $this->id);
		while($sqlrow = $stmt->fetch())
			$rv[] = Group::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: member_of
	 * Checks, if the user is a member of a group.
	 * 
	 * Parameters:
	 * 	$group - A Group object
	 * 
	 * Returns:
	 * 	True, if the user is a member of $group. False, if not.
	 */
	public function member_of($group)
	{
		$stmt = qdb("SELECT COUNT(*) AS `num` FROM `PREFIX_group_members` WHERE `user` = ? AND `group` = ?", $this->id, $group->get_id());
		$sqlrow = $stmt->fetch();
		return ($sqlrow["num"] > 0);
	}
}

/*
 * Class: Group
 * Data model for groups
 */
class Group extends BySQLRowEnabled
{
	private $id;
	
	/*
	 * Variables: Public class properties
	 * 
	 * $name - Name of the group.
	 */
	public $name;
	
	/*
	 * Constructor: create
	 * Creates a new group.
	 * 
	 * Parameters:
	 * 	$name - The name of the group.
	 * 
	 * Returns:
	 * 	An Group object
	 * 
	 * Throws:
	 * 	<AlreadyExistsError>
	 */
	public static function create($name)
	{
		global $db_con;
		try
		{
			$obj = self::by_name($name);
		}
		catch(DoesNotExistError $e)
		{
			qdb("INSERT INTO `PREFIX_groups` (`name`) VALUES (?)", $name);
			$obj = new self();
			
			$obj->id   = $db_con->lastInsertId();
			$obj->name = $name;
			
			return $obj;
		}
		throw new AlreadyExistsError("\"$name\" is already in database.");
	}
	
	protected function populate_by_sqlrow($sqlrow)
	{
		$this->id   = $sqlrow["id"];
		$this->name = $sqlrow["name"];
	}
	
	/*
	 * Constructor: by_id
	 * Get a Group object by ID
	 * 
	 * Parameters:
	 * 	$id - The ID.
	 * 
	 * Returns:
	 * 	A Group object.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$stmt =  qdb("SELECT `id`, `name` FROM `PREFIX_groups` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if(!$sqlrow)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: by_name
	 * Get a Group object by name
	 * 
	 * Parameters:
	 * 	$name - The group name.
	 * 
	 * Returns:
	 * 	A Group object.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_name($name)
	{
		$stmt = qdb("SELECT `id`, `name` FROM `PREFIX_groups` WHERE `name` = ?", $name);
		$sqlrow = $stmt->fetch();
		if(!$sqlrow)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Function: all
	 * Returns array of all groups
	 */
	public static function all()
	{
		$rv = array();
		
		$stmt = qdb("SELECT `id`, `name` FROM `PREFIX_groups` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$rv[] = self::by_sqlrow($sqlrow);
		
		return $rv;
	}
	
	/*
	 * Function: get_id
	 * Returns:
	 * 	The group ID.
	 */
	public function get_id()
	{
		return $this->id;
	}
	
	/*
	 * Function: delete
	 * Deletes the group from the database.
	 */
	public function delete()
	{
		transaction(function()
		{
			qdb("DELETE FROM `PREFIX_group_members` WHERE `group` = ?", $this->id);
			qdb("DELETE FROM `PREFIX_groups` WHERE `id` = ?", $this->id);
		});
	}
	
	/*
	 * Function: get_members
	 * Get all members of the group.
	 *
	 * Returns:
	 * 	Array of <User> objects.
	 */
	public function get_members()
	{
		$rv = array();
		$stmt = qdb("SELECT `a`.`id` AS `id`, `a`.`username` AS `username`, `a`.`pwhash` AS `pwhash`, `a`.`mail` AS `mail`, `a`.`fullname` AS `fullname`, `a`.`language` AS `language`
FROM `PREFIX_users` `a` INNER JOIN `PREFIX_group_members` `b` ON `a`.`id` = `b`.`user`
WHERE `b`.`group` = ?", $this->id);
		while($sqlrow = $stmt->fetch())
			$rv[] = User::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: exclude_user
	 * Excludes user from group.
	 * 
	 * Parameters:
	 * 	$user - <User> object.
	 */
	public function exclude_user($user)
	{
		qdb("DELETE FROM `PREFIX_group_members` WHERE `user` = ? AND `group` = ?", $user->get_id(), $this->id);
	}
	
	/*
	 * Function: include_user
	 * Includes user to group.
	 * 
	 * Parameters:
	 * 	$user - <User> object.
	 */
	public function include_user($user)
	{
		if(!$user->member_of($this))
			qdb("INSERT INTO `PREFIX_group_members` (`user`, `group`) VALUES (?, ?)", $user->get_id(), $this->id);
	}
}

/*
 * Class: Translation
 * A translation. Can only be stored using an <Multilingual> object.
 */
class Translation
{
	/*
	 * Variables: Public class variables.
	 * 
	 * $text - The translated text.
	 * $texttype - The type of the text. Has only a meaning in a context.
	 */
	public $text;
	public $texttype;
	
	/*
	 * Constructor: __construct
	 * Creates a new Translation object.
	 * IT WILL NOT BE STORED TO DATABASE!
	 *
	 * Parameters:
	 * 	$text - The translated text.
	 * 	$texttype - The type of the text. Has only a meaning in a context.
	 * 
	 * See also:
	 * 	<Multilingual>
	 */
	public function __construct($text, $texttype)
	{
		$this->text     = $text;
		$this->texttype = $texttype;
	}
}

/*
 * Class: Multilingual
 * Container for <Translation> objects.
 * Translations can be accessed array-like. So, if you want the german translation: $translation = $my_multilingual["de"];
 * 
 * See also:
 * 	<languages.php>
 */
class Multilingual implements Countable, ArrayAccess, IteratorAggregate
{
	private $translations;
	private $id;
	private $to_be_deleted;
	private $to_be_created;
	
	private function __construct()
	{
		$this->translations  = array();
		$this->to_be_deleted = array();
		$this->to_be_created = array();
	}
	
	/*
	 * Function: get_id
	 * Retuurns the ID of the object.
	 */
	public function get_id()
	{
		return $this->id;
	}
	
	/*
	 * Constructor: create
	 * Creates a new Multilingual object
	 * 
	 * Returns:
	 * 	An Multilingual object.
	 */
	public static function create()
	{
		global $db_con;
		
		$obj = new self();
		qdb("INSERT INTO `PREFIX_multilingual` () VALUES ()");
		$obj->id = $db_con->lastInsertId();
		return $obj;
	}
	
	/*
	 * Constructor: by_id
	 * Gets an Multilingual object by ID.
	 * 
	 * Parameters:
	 * 	$id - The ID.
	 * 
	 * Returns:
	 * 	An Multilingual object.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$obj = new self();
		$stmt = qdb("SELECT `id` FROM `PREFIX_multilingual` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if($sqlrow == False)
			throw new DoesNotExistError();
		$obj->id = $id;
		
		$stmt = qdb("SELECT `language`, `text`, `texttype` FROM `PREFIX_translations` WHERE `multilingual` = ?", $id);
		while($sqlrow = $stmt->fetch())
			$obj->translations[$sqlrow["language"]] = new Translation($sqlrow["text"], $sqlrow["texttype"]);
		
		return $obj;
	}
	
	/*
	 * Function: save
	 * Saves the translations to database.
	 */
	public function save()
	{
		transaction(function()
		{
			foreach($this->to_be_deleted as $deletelang)
				qdb("DELETE FROM `PREFIX_translations` WHERE `multilingual` = ? AND `language` = ?", $this->id, $deletelang);
			
			foreach($this->to_be_created as $lang)
				qdb("INSERT INTO `PREFIX_translations` (`multilingual`, `language`, `text`, `texttype`) VALUES (?, ?, ?, ?)",
					$this->id, $lang, $this->translations[$lang]->text, $this->translations[$lang]->texttype);
			
			foreach($this->translations as $lang => $translation)
			{
				if(!in_array($lang, $this->to_be_created))
					qdb("UPDATE `PREFIX_translations` SET `text` = ?, `texttype` = ? WHERE `multilingual` = ? AND `language` = ?",
						$translation->text, $translation->texttype, $this->id, $lang);
			}
			
			$this->to_be_deleted = array();
			$this->to_be_created = array();
		});
	}
	
	/*
	 * Function: delete
	 * Deletes the data from database.
	 */
	public function delete()
	{
		transaction(function()
		{
			qdb("DELETE FROM `PREFIX_translations` WHERE `multilingual` = ?", $this->id);
			qdb("DELETE FROM `PREFIX_multilingual` WHERE `id` = ?", $this->id);
		});
	}
	
	/* Countable interface implementation */
	public function count() { return count($this->languages); }
	
	/* ArrayAccess interface implementation */
	public function offsetExists($offset) { return isset($this->translations[$offset]); }
	public function offsetGet($offset)
	{
		if(isset($this->translations[$offset]))
			return $this->translations[$offset];
		else
			throw new DoesNotExistError();
	}
	public function offsetUnset($offset)
	{
		unset($this->translations[$offset]);
		if(in_array($offset, $this->to_be_created))
			unset($this->to_be_created[array_search($offset, $this->to_be_created)]);
		else
			$this->to_be_deleted[] = $offset;
	}
	public function offsetSet($offset, $value)
	{
		if(!isset($this->translations[$offset]))
		{
			if(in_array($offset, $this->to_be_deleted))
				unset($this->to_be_deleted[array_search($offset, $this->to_be_deleted)]);
			else
				$this->to_be_created[] = $offset;
		}
		$this->translations[$offset] = $value;
	}
	
	/* IteratorAggregate interface implementation */
	public function getIterator() { return new ArrayIterator($this->translations); }
}

class SettingsIterator implements Iterator
{
	private $index;
	private $keys;
	private $settings_obj;
	
	public function __construct($settings_obj, $keys)
	{
		$this->index = 0;
		$this->settings_obj = $settings_obj;
		$this->keys = $keys;
	}
	
	/* Iterator implementation */
	public function current() { return $this->settings_obj[$this->keys[$this->index]]; }
	public function key()     { return $this->keys[$this->index]; }
	public function next()    { ++$this->index; }
	public function rewind()  { $this->index = 0; }
	public function valid()   { return $this->index < count($this->keys); }
}

/*
 * Class: Settings
 * A class that holds the Settings of Ratatöskr.
 * You can access settings like an array.
 */
class Settings implements ArrayAccess, IteratorAggregate, Countable
{
	/* Singleton implementation */
	private function __copy() {}
	private static $instance = NULL;
	/*
	 * Constructor: get_instance
	 * Get an instance of this class.
	 * All instances are equal (ie. this is a singleton), so you can also use
	 * the global <$ratatoeskr_settings> instance.
	 */
	public static function get_instance()
	{
		if(self::$instance === NULL)
			self::$instance = new self;
		return self::$instance;
	}
	
	private $buffer;
	private $to_be_deleted;
	private $to_be_created;
	private $to_be_updated;
	
	private function __construct()
	{
		$this->buffer = array();
		$stmt = qdb("SELECT `key`, `value` FROM `PREFIX_settings_kvstorage` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$this->buffer[$sqlrow["key"]] = unserialize(base64_decode($sqlrow["value"]));
		
		$this->to_be_created = array();
		$this->to_be_deleted = array();
		$this->to_be_updated = array();
	}
	
	public function save()
	{
		transaction(function(){
			foreach($this->to_be_deleted as $k)
				qdb("DELETE FROM `PREFIX_settings_kvstorage` WHERE `key` = ?", $k);
			foreach($this->to_be_updated as $k)
				qdb("UPDATE `PREFIX_settings_kvstorage` SET `value` = ? WHERE `key` = ?", base64_encode(serialize($this->buffer[$k])), $k);
			foreach($this->to_be_created as $k)
				qdb("INSERT INTO `PREFIX_settings_kvstorage` (`key`, `value`) VALUES (?, ?)", $k, base64_encode(serialize($this->buffer[$k])));
		
			$this->to_be_created = array();
			$this->to_be_deleted = array();
			$this->to_be_updated = array();
		});
	}
	
	/* ArrayAccess implementation */
	public function offsetExists($offset)
	{
		return isset($this->buffer[$offset]);
	}
	public function offsetGet($offset)
	{
		return $this->buffer[$offset];
	}
	public function offsetSet ($offset, $value)
	{
		if(!$this->offsetExists($offset))
		{
			if(in_array($offset, $this->to_be_deleted))
			{
				$this->to_be_updated[] = $offset;
				unset($this->to_be_deleted[array_search($offset, $this->to_be_deleted)]);
			}
			else
				$this->to_be_created[] = $offset;
		}
		elseif((!in_array($offset, $this->to_be_created)) and (!in_array($offset, $this->to_be_updated)))
			$this->to_be_updated[] = $offset;
		$this->buffer[$offset] = $value;
	}
	public function offsetUnset($offset)
	{
		if(in_array($offset, $this->to_be_created))
			unset($this->to_be_created[array_search($offset, $this->to_be_created)]);
		else
			$this->to_be_deleted[] = $offset;
		unset($this->buffer[$offset]);
	}
	
	/* IteratorAggregate implementation */
	public function getIterator() { return new SettingsIterator($this, array_keys($this->buffer)); }
	
	/* Countable implementation */
	public function count() { return count($this->buffer); }
}

$ratatoeskr_settings = Settings::get_instance();

/*
 * Class: PluginKVStorage
 * A Key-Value-Storage for Plugins
 * Can be accessed like an array.
 * Keys are strings and Values can be everything serialize() can process.
 *
 * Extends the abstract <KVStorage> class.
 */
class PluginKVStorage extends KVStorage
{
	/*
	 * Constructor: __construct
	 * 
	 * Parameters:
	 * 	$plugin_id - The ID of the Plugin.
	 */
	public function __construct($plugin_id)
	{
		$this->init("PREFIX_plugin_kvstorage", array("plugin" => $plugin_id));
	}
}

/*
 * Class: Comment
 * Representing a user comment
 */
class Comment extends BySQLRowEnabled
{
	private $id;
	private $article_id;
	private $language;
	private $timestamp;
	
	/*
	 * Variables: Public class variables.
	 * 
	 * $author_name   - Name of comment author.
	 * $author_mail   - E-Mail of comment author.
	 * $text          - Comment text.
	 * $visible       - Should the comment be visible?
	 * $read_by_admin - Was the comment read by an admin.
	 */
	public $author_name;
	public $author_mail;
	public $text;
	public $visible;
	public $read_by_admin;
	
	/*
	 * Functions: Getters
	 * 
	 * get_id        - Gets the comment ID.
	 * get_article   - Gets the article.
	 * get_language  - Gets the language.
	 * get_timestamp - Gets the timestamp.
	 */
	public function get_id()        { return $this->id;                           }
	public function get_article()   { return Article::by_id($this->article_id);   }
	public function get_language()  { return $this->language;                     }
	public function get_timestamp() { return $this->timestamp;                    }
	
	/*
	 * Constructor: create
	 * Creates a new comment.
	 * Automatically sets the $timestamp and $visible (default from setting "comment_visible_default").
	 * 
	 * Parameters:
	 * 	$article  - An <Article> Object.
	 * 	$language - Which language? (see <languages.php>)
	 */
	public static function create($article, $language)
	{
		global $ratatoeskr_settings;
		global $db_con;
		
		$obj = new self();
		$obj->timestamp = time();
		
		qdb("INSERT INTO `PREFIX_comments` (`article`, `language`, `author_name`, `author_mail`, `text`, `timestamp`, `visible`, `read_by_admin`) VALUES (?, ?, '', '', '', ?, ?, 0)",
			$article->get_id(), $language, $obj->timestamp, $ratatoeskr_settings["comment_visible_default"] ? 1 : 0);
		
		$obj->id            = $db_con->lastInsertId();
		$obj->article_id    = $article->get_id();
		$obj->language      = $language;
		$obj->author_name   = "";
		$obj->author_mail   = "";
		$obj->text          = "";
		$obj->visible       = $ratatoeskr_settings["comment_visible_default"];
		$obj->read_by_admin = False;
		
		return $obj;
	}
	
	protected function populate_by_sqlrow($sqlrow)
	{
		$this->id            = $sqlrow["id"];
		$this->article_id    = $sqlrow["article"];
		$this->language      = $sqlrow["language"];
		$this->author_name   = $sqlrow["author_name"];
		$this->author_mail   = $sqlrow["author_mail"];
		$this->text          = $sqlrow["text"];
		$this->timestamp     = $sqlrow["timestamp"];
		$this->visible       = $sqlrow["visible"] == 1;
		$this->read_by_admin = $sqlrow["read_by_admin"] == 1;
	}
	
	/*
	 * Constructor: by_id
	 * Gets a Comment by ID.
	 * 
	 * Parameters:
	 * 	$id - The comments ID.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$stmt = qdb("SELECT `id`, `article`, `language`, `author_name`, `author_mail`, `text`, `timestamp`, `visible`, `read_by_admin` FROM `PREFIX_comments` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if($sqlrow === False)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: all
	 * Get all comments
	 * 
	 * Returns:
	 * 	Array of Comment objects
	 */
	public static function all()
	{
		$rv = array();
		$stmt = qdb("SELECT `id`, `article`, `language`, `author_name`, `author_mail`, `text`, `timestamp`, `visible`, `read_by_admin` FROM `PREFIX_comments` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$rv[] = self::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: htmlize_comment_text
	 * Creates the HTML representation of a comment text. It applys the page's comment textprocessor on it
	 * and filters some potentially harmful tags using kses.
	 * 
	 * Parameters:
	 * 	$text - Text to HTMLize.
	 * 
	 * Returns:
	 * 	HTML code.
	 */
	public static function htmlize_comment_text($text)
	{
		global $ratatoeskr_settings;
		
		return kses(textprocessor_apply($text, $ratatoeskr_settings["comment_textprocessor"]), array(
			"a" => array("href" => 1, "hreflang" => 1, "title" => 1, "rel" => 1, "rev" => 1),
			"b" => array(),
			"i" => array(),
			"u" => array(),
			"strong" => array(),
			"em" => array(),
			"p" => array("align" => 1),
			"br" => array(),
			"abbr" => array(),
			"acronym" => array(),
			"code" => array(),
			"pre" => array(),
			"blockquote" => array("cite" => 1),
			"h1" => array(),
			"h2" => array(),
			"h3" => array(),
			"h4" => array(),
			"h5" => array(),
			"h6" => array(), 
			"img" => array("src" => 1, "alt" => 1, "width" => 1, "height" => 1),
			"s" => array(),
			"q" => array("cite" => 1),
			"samp" => array(),
			"ul" => array(),
			"ol" => array(),
			"li" => array(),
			"del" => array(),
			"ins" => array(),
			"dl" => array(),
			"dd" => array(),
			"dt" => array(),
			"dfn" => array(),
			"div" => array(),
			"dir" => array(),
			"kbd" => array("prompt" => 1),
			"strike" => array(),
			"sub" => array(),
			"sup" => array(),
			"table" => array("style" => 1),
			"tbody" => array(), "thead" => array(), "tfoot" => array(),
			"tr" => array(),
			"td" => array("colspan" => 1, "rowspan" => 1),
			"th" => array("colspan" => 1, "rowspan" => 1),
			"tt" => array(),
			"var" => array()
		));
	}
	
	/*
	 * Function: create_html
	 * Applys <htmlize_comment_text> onto this comment's text.
	 *
	 * Returns:
	 * 	The HTML representation.
	 */
	public function create_html()
	{
		return self::htmlize_comment_text($this->text);
	}
	
	/*
	 * Function: save
	 * Save changes to database.
	 */
	public function save()
	{
		qdb("UPDATE `PREFIX_comments` SET `author_name` = ?, `author_mail` = ?, `text` = ?, `visible` = ?, `read_by_admin` = ? WHERE `id` = ?",
			$this->author_name, $this->author_mail, $this->text, ($this->visible ? 1 : 0), ($this->read_by_admin ? 1 : 0), $this->id);
	}
	
	/*
	 * Function: delete
	 */
	public function delete()
	{
		qdb("DELETE FROM `PREFIX_comments` WHERE `id` = ?", $this->id);
	}
}

/*
 * Class: Style
 * Represents a Style
 */
class Style extends BySQLRowEnabled
{
	private $id;
	
	/*
	 * Variables: Public class variables.
	 * 
	 * $name - The name of the style.
	 * $code - The CSS code.
	 */
	public $name;
	public $code;
	
	protected function populate_by_sqlrow($sqlrow)
	{
		$this->id   = $sqlrow["id"];
		$this->name = $sqlrow["name"];
		$this->code = $sqlrow["code"];
	}
	
	/*
	 * Function: test_name
	 * Test, if a name is a valid Style name.
	 * 
	 * Parameters:
	 * 	$name - The name to test
	 * 
	 * Returns:
	 * 	True, if the name is a valid style name, False if not.
	 */
	public static function test_name($name)
	{
		return preg_match("/^[a-zA-Z0-9\\-_\\.]+$/", $name) == 1;
	}
	
	/*
	 * Function: get_id
	 */
	public function get_id() { return $this->id; }
	
	/*
	 * Constructor: create
	 * Create a new style.
	 * 
	 * Parameters:
	 * 	$name - A name for the new style.
	 * 
	 * Throws:
	 * 	<AlreadyExistsError>
	 */
	public static function create($name)
	{
		global $db_con;
		
		if(!self::test_name($name))
			throw new InvalidDataError("invalid_style_name");
			
		try
		{
			self::by_name($name);
		}
		catch(DoesNotExistError $e)
		{
			$obj = new self();
			$obj->name = $name;
			$obj->code = "";
		
			qdb("INSERT INTO `PREFIX_styles` (`name`, `code`) VALUES (?, '')", $name);
		
			$obj->id = $db_con->lastInsertId();
			return $obj;
		}
		
		throw new AlreadyExistsError();
	}
	
	/*
	 * Constructor: by_id
	 * Gets a Style object by ID.
	 * 
	 * Parameters:
	 * 	$id - The ID
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$stmt = qdb("SELECT `id`, `name`, `code` FROM `PREFIX_styles` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if(!$sqlrow)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: by_name
	 * Gets a Style object by name.
	 * 
	 * Parameters:
	 * 	$name - The name.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_name($name)
	{
		$stmt = qdb("SELECT `id`, `name`, `code` FROM `PREFIX_styles` WHERE `name` = ?", $name);
		$sqlrow = $stmt->fetch();
		if(!$sqlrow)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: all
	 * Get all styles
	 * 
	 * Returns:
	 * 	Array of Style objects
	 */
	public static function all()
	{
		$rv = array();
		$stmt = qdb("SELECT `id`, `name`, `code` FROM `PREFIX_styles` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$rv[] = self::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: save
	 * Save changes to database.
	 * 
	 * Throws:
	 * 	<AlreadyExistsError>
	 */
	public function save()
	{
		if(!self::test_name($name))
			throw new InvalidDataError("invalid_style_name");
		
		transaction(function()
		{
			$stmt = qdb("SELECT COUNT(*) AS `n` FROM `PREFIX_styles` WHERE `name` = ? AND `id` != ?", $this->name, $this->id);
			$sqlrow = $stmt->fetch();
			if($sqlrow["n"] > 0)
				throw new AlreadyExistsError();
			
			qdb("UPDATE `PREFIX_styles` SET `name` = ?, `code` = ? WHERE `id` = ?",
				$this->name, $this->code, $this->id);
		});
	}
	
	/*
	 * Function: delete
	 */
	public function delete()
	{
		transaction(function(){
			qdb("DELETE FROM `PREFIX_styles` WHERE `id` = ?", $this->id);
			qdb("DELETE FROM `PREFIX_section_style_relations` WHERE `style` = ?", $this->id);
		});
	}
}

/*
 * Class: Plugin
 * The representation of a plugin in the database.
 */
class Plugin extends BySQLRowEnabled
{
	private $id;
	
	/*
	 * Variables: Public class variables.
	 *
	 * $name              - Plugin name.
	 * $code              - Plugin code.
	 * $classname         - Main class of the plugin.
	 * $active            - Is the plugin activated?
	 * $author            - Author of the plugin.
	 * $versiontext       - Version (text)
	 * $versioncount      - Version (counter)
	 * $short_description - A short description.
	 * $updatepath        - URL for updates.
	 * $web               - Webpage of the plugin.
	 * $help              - Help page.
	 * $license           - License text.
	 * $installed         - Is this plugin installed? Used during the installation process.
	 * $update            - Should the plugin be updated at next start?
	 * $api               - The API version this Plugin needs.
	 */
	
	public $name;
	public $code;
	public $classname;
	public $active;
	public $author;
	public $versiontext;
	public $versioncount;
	public $short_description;
	public $updatepath;
	public $web;
	public $help;
	public $license;
	public $installed;
	public $update;
	public $api;
	
	/*
	 * Function: clean_db
	 * Performs some datadase cleanup jobs on the plugin table.
	 */
	public static function clean_db()
	{
		qdb("DELETE FROM `PREFIX_plugins` WHERE `installed` = 0 AND `added` < ?", (time() - (60*5)));
	}
	
	/*
	 * Function: get_id
	 */
	public function get_id() { return $this->id; }
	
	/*
	 * Constructor: create
	 * Creates a new, empty plugin database entry
	 */
	public static function create()
	{
		global $db_con;
		
		$obj = new self();
		qdb("INSERT INTO `PREFIX_plugins` (`added`) VALUES (?)", time());
		$obj->id = $db_con->lastInsertId();
		return $obj;
	}
	
	/*
	 * Function: fill_from_pluginpackage
	 * Fills plugin data from an <PluginPackage> object.
	 * 
	 * Parameters:
	 * 	$pkg - The <PluginPackage> object.
	 */
	public function fill_from_pluginpackage($pkg)
	{
		$this->name              = $pkg->name;
		$this->code              = $pkg->code;
		$this->classname         = $pkg->classname;
		$this->author            = $pkg->author;
		$this->versiontext       = $pkg->versiontext;
		$this->versioncount      = $pkg->versioncount;
		$this->short_description = $pkg->short_description;
		$this->updatepath        = $pkg->updatepath;
		$this->web               = $pkg->web;
		$this->license           = $pkg->license;
		$this->help              = $pkg->help;
		$this->api               = $pkg->api;
		
		if(!empty($pkg->custompub))
			array2dir($pkg->custompub, dirname(__FILE__) . "/../plugin_extradata/public/" . $this->get_id());
		if(!empty($pkg->custompriv))
			array2dir($pkg->custompriv, dirname(__FILE__) . "/../plugin_extradata/private/" . $this->get_id());
		if(!empty($pkg->tpls))
			array2dir($pkg->tpls, dirname(__FILE__) . "/../templates/src/plugintemplates/" . $this->get_id());
	}
	
	protected function populate_by_sqlrow($sqlrow)
	{
		$this->id                = $sqlrow["id"];
		$this->name              = $sqlrow["name"];
		$this->code              = $sqlrow["code"];
		$this->classname         = $sqlrow["classname"];
		$this->active            = ($sqlrow["active"] == 1);
		$this->author            = $sqlrow["author"];
		$this->versiontext       = $sqlrow["versiontext"];
		$this->versioncount      = $sqlrow["versioncount"];
		$this->short_description = $sqlrow["short_description"];
		$this->updatepath        = $sqlrow["updatepath"];
		$this->web               = $sqlrow["web"];
		$this->help              = $sqlrow["help"];
		$this->license           = $sqlrow["license"];
		$this->installed         = ($sqlrow["installed"] == 1);
		$this->update            = ($sqlrow["update"] == 1);
		$this->api               = $sqlrow["api"];
	}
	
	/*
	 * Constructor: by_id
	 * Gets plugin by ID.
	 * 
	 * Parameters:
	 * 	$id - The ID
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$stmt = qdb("SELECT `id`, `name`, `author`, `versiontext`, `versioncount`, `short_description`, `updatepath`, `web`, `help`, `code`, `classname`, `active`, `license`, `installed`, `update`, `api` FROM `PREFIX_plugins` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if($sqlrow === False)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: all
	 * Gets all Plugins
	 * 
	 * Returns:
	 * 	List of <Plugin> objects.
	 */
	public static function all()
	{
		$rv = array();
		$stmt = qdb("SELECT `id`, `name`, `author`, `versiontext`, `versioncount`, `short_description`, `updatepath`, `web`, `help`, `code`, `classname`, `active`, `license`, `installed`, `update`, `api` FROM `PREFIX_plugins` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$rv[] = self::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: save
	 */
	public function save()
	{
		qdb("UPDATE `PREFIX_plugins` SET `name` = ?, `author` = ?, `code` = ?, `classname` = ?, `active` = ?, `versiontext` = ?, `versioncount` = ?, `short_description` = ?, `updatepath` = ?, `web` = ?, `help` = ?, `installed` = ?, `update` = ?, `license` = ?, `api` = ? WHERE `id` = ?",
			$this->name, $this->author, $this->code, $this->classname, ($this->active ? 1 : 0), $this->versiontext, $this->versioncount, $this->short_description, $this->updatepath, $this->web, $this->help, ($this->installed ? 1 : 0), ($this->update ? 1 : 0), $this->license, $this->api, $this->id);
	}
	
	/*
	 * Function: delete
	 */
	public function delete()
	{
		transaction(function()
		{
			qdb("DELETE FROM `PREFIX_plugins` WHERE `id` = ?", $this->id);
			qdb("DELETE FROM `PREFIX_plugin_kvstorage` WHERE `plugin` = ?", $this->id);
			qdb("DELETE FROM `PREFIX_article_extradata` WHERE `plugin` = ?", $this->id);
		});
		
		if(is_dir(SITE_BASE_PATH . "/ratatoeskr/plugin_extradata/private/" . $this->id))
			delete_directory(SITE_BASE_PATH . "/ratatoeskr/plugin_extradata/private/" . $this->id);
		if(is_dir(SITE_BASE_PATH . "/ratatoeskr/plugin_extradata/public/" . $this->id))
			delete_directory(SITE_BASE_PATH . "/ratatoeskr/plugin_extradata/public/" . $this->id);
		if(is_dir(SITE_BASE_PATH . "/ratatoeskr/templates/src/plugintemplates/" . $this->id))
			delete_directory(SITE_BASE_PATH . "/ratatoeskr/templates/src/plugintemplates/" . $this->id);
	}
	
	/*
	 * Function get_kvstorage
	 * Get the KeyValue Storage for the plugin.
	 * 
	 * Returns:
	 * 	An <PluginKVStorage> object.
	 */
	public function get_kvstorage()
	{
		return new PluginKVStorage($this->id);
	}
}

/*
 * Class: Section
 * Representing a section
 */
class Section extends BySQLRowEnabled
{
	private $id;
	
	/*
	 * Variables: Public class variables
	 * 
	 * $name     - The name of the section.
	 * $title    - The title of the section (a <Multilingual> object).
	 * $template - Name of the template.
	 */
	public $name;
	public $title;
	public $template;
	
	protected function populate_by_sqlrow($sqlrow)
	{
		$this->id       = $sqlrow["id"];
		$this->name     = $sqlrow["name"];
		$this->title    = Multilingual::by_id($sqlrow["title"]);
		$this->template = $sqlrow["template"];
	}
	
	/*
	 * Function: test_name
	 * Tests, if a name is a valid section name.
	 * 
	 * Parameters:
	 * 	$name - The name to test.
	 * 
	 * Returns:
	 * 	True, if the name is a valid section name, False otherwise.
	 */
	public static function test_name($name)
	{
		return preg_match("/^[a-zA-Z0-9\\-_]+$/", $name) != 0;
	}
	
	/*
	 * Function: get_id
	 */
	public function get_id() { return $this->id; }
	
	/*
	 * Constructor: create
	 * Creates a new section.
	 * 
	 * Parameters:
	 * 	$name - The name of the new section.
	 * 
	 * Throws:
	 * 	<AlreadyExistsError>, <InvalidDataError>
	 */
	public static function create($name)
	{
		global $db_con;
		
		if(!self::test_name($name))
			throw new InvalidDataError("invalid_section_name");
			
		try
		{
			$obj = self::by_name($name);
		}
		catch(DoesNotExistError $e)
		{
			$obj           = new self();
			$obj->name     = $name;
			$obj->title    = Multilingual::create();
			$obj->template = "";
			
			qdb("INSERT INTO `PREFIX_sections` (`name`, `title`, `template`) VALUES (?, ?, '')", $name, $obj->title->get_id());
			
			$obj->id = $db_con->lastInsertId();
			
			return $obj;
		}
		
		throw new AlreadyExistsError();
	}
	
	/*
	 * Constructor: by_id
	 * Gets section by ID.
	 * 
	 * Parameters:
	 * 	$id - The ID.
	 * 
	 * Returns: 
	 * 	A <Section> object.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$stmt = qdb("SELECT `id`, `name`, `title`, `template` FROM `PREFIX_sections` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if($sqlrow === False)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: by_name
	 * Gets section by name.
	 * 
	 * Parameters:
	 * 	$name - The name.
	 * 
	 * Returns: 
	 * 	A <Section> object.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_name($name)
	{
		$stmt = qdb("SELECT `id`, `name`, `title`, `template` FROM `PREFIX_sections` WHERE `name` = ?", $name);
		$sqlrow = $stmt->fetch();
		if($sqlrow === False)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: all
	 * Gets all sections.
	 * 
	 * Returns:
	 * 	Array of Section objects.
	 */
	public static function all()
	{
		$rv = array();
		$stmt = qdb("SELECT `id`, `name`, `title`, `template` FROM `PREFIX_sections` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$rv[] = self::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: get_styles
	 * Get all styles associated with this section.
	 * 
	 * Returns:
	 * 	List of <Style> objects.
	 */
	public function get_styles()
	{
		$rv = array();
		$stmt = qdb("SELECT `a`.`id` AS `id`, `a`.`name` AS `name`, `a`.`code` AS `code` FROM `PREFIX_styles` `a` INNER JOIN `PREFIX_section_style_relations` `b` ON `a`.`id` = `b`.`style` WHERE `b`.`section` = ?", $this->id);
		while($sqlrow = $stmt->fetch())
			$rv[] = Style::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: add_style
	 * Add a style to this section.
	 * 
	 * Parameters:
	 * 	$style - A <Style> object.
	 */
	public function add_style($style)
	{
		transaction(function() use ($style)
		{
			$stmt = qdb("SELECT COUNT(*) AS `n` FROM `PREFIX_section_style_relations` WHERE `style` = ? AND `section` = ?", $style->get_id(), $this->id);
			$sqlrow = $stmt->fetch();
			if($sqlrow["n"] == 0)
				qdb("INSERT INTO `PREFIX_section_style_relations` (`section`, `style`) VALUES (?, ?)", $this->id, $style->get_id());
		});
	}
	
	/*
	 * Function: remove_style
	 * Remove a style from this section.
	 * 
	 * Parameters:
	 * 	$style - A <Style> object.
	 */
	public function remove_style($style)
	{
		qdb("DELETE FROM `PREFIX_section_style_relations` WHERE `section` = ? AND `style` = ?", $this->id, $style->get_id());
	}
	
	/*
	 * Function: save
	 * 
	 * Throws:
	 * 	<AlreadyExistsError>, <InvalidDataError>
	 */
	public function save()
	{
		if(!self::test_name($name))
			throw new InvalidDataError("invalid_section_name");
		
		transaction(function()
		{
			$stmt = qdb("SELECT COUNT(*) AS `n` FROM `PREFIX_sections` WHERE `name` = ? AND `id` != ?", $this->name, $this->id);
			$sqlrow = $stmt->fetch();
			if($sqlrow["n"] > 0)
				throw new AlreadyExistsError();
			
			$this->title->save();
			qdb("UPDATE `PREFIX_sections` SET `name` = ?, `title` = ?, `template` = ? WHERE `id` = ?",
				$this->name, $this->title->get_id(), $this->template, $this->id);
		});
	}
	
	/*
	 * Function: delete
	 */
	public function delete()
	{
		transaction(function()
		{
			$this->title->delete();
			qdb("DELETE FROM `PREFIX_sections` WHERE `id` = ?", $this->id);
			qdb("DELETE FROM `PREFIX_section_style_relations` WHERE `section` = ?", $this->id);
		});
	}
	
	/*
	 * Function: get_articles
	 * Get all articles in this section.
	 * 
	 * Returns:
	 * 	Array of <Article> objects
	 */
	public function get_articles()
	{
		$rv = array();
		$stmt = qdb("SELECT `id`, `urlname`, `title`, `text`, `excerpt`, `meta`, `custom`, `article_image`, `status`, `section`, `timestamp`, `allow_comments` FROM `PREFIX_articles` WHERE `section` = ?", $this->id);
		while($sqlrow = $stmt->fetch())
			$rv[] = Article::by_sqlrow($sqlrow);
		return $rv;
	}
}

/*
 * Class: Tag
 * Representation of a tag
 */
class Tag extends BySQLRowEnabled
{
	private $id;
	
	/*
	 * Variables: Public class variables
	 * 
	 * $name - The name of the tag
	 * $title - The title (an <Multilingual> object)
	 */
	public $name;
	public $title;
	
	/*
	 * Function: test_name
	 * Test, if a name is a valid tag name.
	 * 
	 * Parameters:
	 * 	$name - Name to test.
	 * 
	 * Returns:
	 * 	True, if the name is valid, False otherwise.
	 */
	public static function test_name($name)
	{
		return (strpos($name, ",") === False) and (strpos($name, " ") === False);
	}
	
	/*
	 * Function: get_id
	 */
	public function get_id() { return $this->id; }
	
	protected function populate_by_sqlrow($sqlrow)
	{
		$this->id    = $sqlrow["id"];
		$this->name  = $sqlrow["name"];
		$this->title = Multilingual::by_id($sqlrow["title"]);
	}
	
	/*
	 * Constructor: create
	 * Create a new tag.
	 * 
	 * Parameters:
	 * 	$name - The name
	 * 
	 * Throws:
	 * 	<AlreadyExistsError>, <InvalidDataError>
	 */
	public static function create($name)
	{
		global $db_con;
		if(!self::test_name($name))
			throw new InvalidDataError("invalid_tag_name");
			
		try
		{
			$obj = self::by_name($name);
		}
		catch(DoesNotExistError $e)
		{
			$obj = new self();
			
			$obj->name  = $name;
			$obj->title = Multilingual::create();
			
			qdb("INSERT INTO `PREFIX_tags` (`name`, `title`) VALUES (?, ?)",
				$name, $obj->title->get_id());
			$obj->id = $db_con->lastInsertId();
			
			return $obj;
		}
		throw new AlreadyExistsError();
	}
	
	/*
	 * Constructor: by_id
	 * Get tag by ID
	 * 
	 * Parameters:
	 * 	$id - The ID
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$stmt = qdb("SELECT `id`, `name`, `title` FROM `PREFIX_tags` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if($sqlrow === False)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: by_name
	 * Get tag by name
	 * 
	 * Parameters:
	 * 	$name - The name
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_name($name)
	{
		$stmt = qdb("SELECT `id`, `name`, `title` FROM `PREFIX_tags` WHERE `name` = ?", $name);
		$sqlrow = $stmt->fetch();
		if($sqlrow === False)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: all
	 * Get all tags
	 * 
	 * Returns:
	 * 	Array of Tag objects.
	 */
	public static function all()
	{
		$rv = array();
		$stmt = qdb("SELECT `id`, `name`, `title` FROM `PREFIX_tags` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$rv[] = self::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: get_articles
	 * Get all articles that are tagged with this tag
	 * 
	 * Returns:
	 * 	Array of <Article> objects
	 */
	public function get_articles()
	{
		$rv = array();
		$stmt = qdb(
"SELECT `a`.`id` AS `id`, `a`.`urlname` AS `urlname`, `a`.`title` AS `title`, `a`.`text` AS `text`, `a`.`excerpt` AS `excerpt`, `a`.`meta` AS `meta`, `a`.`custom` AS `custom`, `a`.`article_image` AS `article_image`, `a`.`status` AS `status`, `a`.`section` AS `section`, `a`.`timestamp` AS `timestamp`, `a`.`allow_comments` AS `allow_comments`
FROM `PREFIX_articles` `a`
INNER JOIN `PREFIX_article_tag_relations` `b` ON `a`.`id` = `b`.`article`
WHERE `b`.`tag` = ?" , $this->id);
		while($sqlrow = $stmt->fetch())
			$rv[] = Article::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: count_articles
	 * 
	 * Returns:
	 * 	The number of articles that are tagged with this tag.
	 */
	public function count_articles()
	{
		$stmt = qdb("SELECT COUNT(*) AS `num` FROM `PREFIX_article_tag_relations` WHERE `tag` = ?", $this->id);
		$sqlrow = $stmt->fetch();
		return $sqlrow["num"];
	}
	
	/*
	 * Function: save
	 * 
	 * Throws:
	 * 	<AlreadyExistsError>, <InvalidDataError>
	 */
	public function save()
	{
		if(!self::test_name($name))
			throw new InvalidDataError("invalid_tag_name");
		
		transaction(function()
		{
			$stmt = qdb("SELECT COUNT(*) AS `n` FROM `PREFIX_tags` WHERE `name` = ? AND `id` != ?", $this->name, $this->id);
			$sqlrow = $stmt->fetch();
			if($sqlrow["n"] > 0)
				throw new AlreadyExistsError();
			
			$this->title->save();
			qdb("UPDATE `PREFIX_tags` SET `name` = ?, `title` = ? WHERE `id` = ?",
				$this->name, $this->title->get_id(), $this->id);
		});
	}
	
	/*
	 * Function: delete
	 */
	public function delete()
	{
		transaction(function()
		{
			$this->title->delete();
			qdb("DELETE FROM `PREFIX_article_tag_relations` WHERE `tag` = ?", $this->id);
			qdb("DELETE FROM `PREFIX_tags` WHERE `id` = ?", $this->id);
		});
	}
}

/*
 * Class: UnknownFileFormat
 * Exception that will be thrown, if a input file has an unsupported file format.
 */
class UnknownFileFormat extends Exception { }

/*
 * Class: IOError
 * This Exception is thrown, if a IO-Error occurs (file not available, no read/write acccess...).
 */
class IOError extends Exception { }

/*
 * Class: Image
 * Representation of an image entry.
 */
class Image extends BySQLRowEnabled
{
	private $id;
	private $filename;
	
	private static $pre_maxw = 150;
	private static $pre_maxh = 100;
	
	/*
	 * Variables: Public class variables
	 * 
	 * $name - The image name
	 */
	public $name;
	
	protected function populate_by_sqlrow($sqlrow)
	{
		$this->id   = $sqlrow["id"];
		$this->name = $sqlrow["name"];
		$this->file = $sqlrow["file"];
	}
	
	/*
	 * Functions: Getters
	 * 
	 * get_id - Get the ID
	 * get_filename - Get the filename
	 */
	public function get_id()       { return $this->id;   }
	public function get_filename() { return $this->file; }
	
	/*
	 * Constructor: create
	 * Create a new image
	 * 
	 * Parameters:
	 * 	$name - The name for the image
	 * 	$file - An uploaded image file (move_uploaded_file must be able to move the file!).
	 * 
	 * Throws:
	 * 	<IOError>, <UnknownFileFormat>
	 */
	public static function create($name, $file)
	{
		$obj = new self();
		$obj->name = $name;
		$obj->file = "0";
		
		transaction(function() use (&$obj, $name, $file)
		{
			global $db_con;
			
			qdb("INSERT INTO `PREFIX_images` (`name`, `file`) VALUES (?, '0')",	$name);
			$obj->id = $db_con->lastInsertId();
			$obj->exchange_image($file);
		});
		return $obj;
	}
	
	/*
	 * Constructor: by_id
	 * Get image by ID.
	 * 
	 * Parameters:
	 * 	$id - The ID
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$stmt = qdb("SELECT `id`, `name`, `file` FROM `PREFIX_images` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if($sqlrow === False)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: all
	 * Gets all images.
	 * 
	 * Returns:
	 * 	Array of <Image> objects.
	 */
	public function all()
	{
		$rv = array();
		$stmt = qdb("SELECT `id`, `name`, `file` FROM `PREFIX_images` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$rv[] = self::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: exchange_image
	 * Exchanges image file. Also saves object to database.
	 * 
	 * Parameters:
	 * 	$file - Location of new image.(move_uploaded_file must be able to move the file!)
	 * 
	 * Throws:
	 * 	<IOError>, <UnknownFileFormat>
	 */
	public function exchange_image($file)
	{
		global $imagetype_file_extensions;
		if(!is_file($file))
			throw new IOError("\"$file\" is not available");
		$imageinfo = getimagesize($file);
		if($imageinfo === False)
			throw new UnknownFileFormat();
		if(!isset($imagetype_file_extensions[$imageinfo[2]]))
			throw new UnknownFileFormat();
		if(is_file(SITE_BASE_PATH . "/images/" . $this->file))
			unlink(SITE_BASE_PATH . "/images/" . $this->file);
		$new_fn = $this->id . "." . $imagetype_file_extensions[$imageinfo[2]];
		if(!move_uploaded_file($file, SITE_BASE_PATH . "/images/" . $new_fn))
			throw new IOError("Can not move file.");
		$this->file = $new_fn;
		$this->save();
		
		/* make preview image */
		switch($imageinfo[2])
		{
			case IMAGETYPE_GIF:  $img = imagecreatefromgif (SITE_BASE_PATH . "/images/" . $new_fn); break;
			case IMAGETYPE_JPEG: $img = imagecreatefromjpeg(SITE_BASE_PATH . "/images/" . $new_fn); break;
			case IMAGETYPE_PNG:  $img = imagecreatefrompng (SITE_BASE_PATH . "/images/" . $new_fn); break;
			default: $img = imagecreatetruecolor(40, 40); imagefill($img, 1, 1, imagecolorallocate($img, 127, 127, 127)); break;
		}
		$w_orig = imagesx($img);
		$h_orig = imagesy($img);
		if(($w_orig > self::$pre_maxw) or ($h_orig > self::$pre_maxh))
		{
			$ratio = $w_orig / $h_orig;
			if($ratio > 1)
			{
				$w_new = round(self::$pre_maxw);
				$h_new = round(self::$pre_maxw / $ratio);
			}
			else
			{
				$h_new = round(self::$pre_maxh);
				$w_new = round(self::$pre_maxh * $ratio);
			}
			$preview = imagecreatetruecolor($w_new, $h_new);
			imagecopyresized($preview, $img, 0, 0, 0, 0, $w_new, $h_new, $w_orig, $h_orig);
			imagepng($preview, SITE_BASE_PATH . "/images/previews/{$this->id}.png");
		}
		else
			imagepng($img, SITE_BASE_PATH . "/images/previews/{$this->id}.png");
	}
	
	/*
	 * Function: save
	 */
	public function save()
	{
		qdb("UPDATE `PREFIX_images` SET `name` = ?, `file` = ? WHERE `id` = ?",
			$this->name, $this->file, $this->id);
	}
	
	/*
	 * Function: delete
	 */
	public function delete()
	{
		qdb("DELETE FROM `PREFIX_images` WHERE `id` = ?", $this->id);
		if(is_file(SITE_BASE_PATH . "/images/" . $this->file))
			unlink(SITE_BASE_PATH . "/images/" . $this->file);
		if(is_file(SITE_BASE_PATH . "/images/previews/{$this->id}.png"))
			unlink(SITE_BASE_PATH . "/images/previews/{$this->id}.png");
	}
}

/*
 * Class: RepositoryUnreachableOrInvalid
 * A Exception that will be thrown, if the repository is unreachable or seems to be an invalid repository.
 */
class RepositoryUnreachableOrInvalid extends Exception { }

/*
 * Class: Repository
 * Representation of an plugin repository.
 */
class Repository extends BySQLRowEnabled
{
	private $id;
	private $baseurl;
	private $name;
	private $description;
	private $lastrefresh;
	
	private $stream_ctx;
	
	/*
	 * Variables: Public class variables
	 * $packages - Array with all packages from this repository. A entry itself is an array: array(name, versioncounter, description)
	 */
	public $packages;
	
	protected function __construct()
	{
		$this->stream_ctx = stream_context_create(array("http" => array("timeout" => 5)));
	}
	
	/*
	 * Functions: Getters
	 * get_id          - Get internal ID.
	 * get_baseurl     - Get the baseurl of the repository.
	 * get_name        - Get repository name.
	 * get_description - Get repository description.
	 */
	public function get_id()          { return $this->id;          }
	public function get_baseurl()     { return $this->baseurl;     }
	public function get_name()        { return $this->name;        }
	public function get_description() { return $this->description; }
	
	/*
	 * Constructor: create
	 * Create a new repository entry from a base url.
	 * 
	 * Parameters:
	 * 	$baseurl - The baseurl of the repository.
	 * 
	 * Throws:
	 * 	Could throw a <RepositoryUnreachableOrInvalid> exception. In this case, nothing will be written to the database.
	 */
	public static function create($baseurl)
	{
		$obj = new self();
		
		if(preg_match('/^(http[s]?:\\/\\/.*?)[\\/]?$/', $baseurl, $matches) == 0)
			throw new RepositoryUnreachableOrInvalid();
		
		$obj->baseurl = $matches[1];
		$obj->refresh(True);
		
		transaction(function() use (&$obj)
		{
			global $db_con;
			
			qdb("INSERT INTO `ratatoeskr_repositories` () VALUES ()");
			$obj->id = $db_con->lastInsertId();
			$obj->save();
		});
		
		return $obj;
	}
	
	protected function populate_by_sqlrow($sqlrow)
	{
		$this->id          = $sqlrow["id"];
		$this->name        = $sqlrow["name"];
		$this->description = $sqlrow["description"];
		$this->baseurl     = $sqlrow["baseurl"];
		$this->packages    = unserialize(base64_decode($sqlrow["pkgcache"]));
		$this->lastrefresh = $sqlrow["lastrefresh"];
	}
	
	/*
	 * Constructor: by_id
	 * Get a repository entry by ID.
	 * 
	 * Parameters:
	 * 	$id - ID.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$stmt = qdb("SELECT `id`, `name`, `description`, `baseurl`, `pkgcache`, `lastrefresh` FROM `PREFIX_repositories` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if(!$sqlrow)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: all
	 * Gets all available repositories.
	 * 
	 * Returns:
	 * 	Array of <Repository> objects.
	 */
	public static function all()
	{
		$rv = array();
		$stmt = qdb("SELECT `id`, `name`, `description`, `baseurl`, `pkgcache`, `lastrefresh` FROM `PREFIX_repositories` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$rv[] = self::by_sqlrow($sqlrow);
		return $rv;
	}
	
	private function save()
	{
		qdb("UPDATE `PREFIX_repositories` SET `baseurl` = ?, `name` = ?, `description` = ?, `pkgcache` = ?, `lastrefresh` = ? WHERE `id` = ?",
		    $this->baseurl,
		    $this->name,
		    $this->description,
		    base64_encode(serialize($this->packages)),
		    $this->lastrefresh,
		    $this->id);
	}
	
	/* 
	 * Function: delete
	 * Delete the repository entry from the database.
	 */
	public function delete()
	{
		qdb("DELETE FROM `PREFIX_repositories` WHERE `id` = ?", $this->id);
	}
	
	/*
	 * Function: refresh
	 * Refresh the package cache and the name and description.
	 * 
	 * Parameters:
	 * 	$force - Force a refresh, even if the data was already fetched in the last 6 hours (default: False).
	 * 
	 * Throws:
	 * 	<RepositoryUnreachableOrInvalid>
	 */
	public function refresh($force = False)
	{
		if(($this->lastrefresh > (time() - (60*60*4))) and (!$force))
			return;
		
		$repometa = @file_get_contents($this->baseurl . "/repometa", False, $this->stream_ctx);
		if($repometa === FALSE)
			throw new RepositoryUnreachableOrInvalid();
		$repometa = @unserialize($repometa);
		if((!is_array($repometa)) or (!isset($repometa["name"])) or (!isset($repometa["description"])))
			throw new RepositoryUnreachableOrInvalid();
		
		$this->name        = $repometa["name"];
		$this->description = $repometa["description"];
		$this->packages    = @unserialize(@file_get_contents($this->baseurl . "/packagelist", False, $ctx));
		
		$this->lastrefresh = time();
		
		$this->save();
	}
	
	/*
	 * Function: get_package_meta
	 * Get metadata of a plugin package from this repository.
	 * 
	 * Parameters:
	 * 	$pkgname - The name of the package.
	 * 
	 * Throws:
	 * 	A <DoesNotExistError> Exception, if the package was not found.
	 * 
	 * Returns:
	 * 	A <PluginPackageMeta> object
	 */
	public function get_package_meta($pkgname)
	{
		$found = False;
		foreach($this->packages as $p)
		{
			if($p[0] == $pkgname)
			{
				$found = True;
				break;
			}
		}
		if(!$found)
			throw new DoesNotExistError("Package not in package cache.");
		
		$pkgmeta = @unserialize(@file_get_contents($this->baseurl . "/packages/" . urlencode($pkgname) . "/meta", False, $this->stream_ctx));
		
		if(!($pkgmeta instanceof PluginPackageMeta))
			throw new DoesNotExistError();
		
		return $pkgmeta;
	}
	
	/*
	 * Function: download_package
	 * Download a package from the repository
	 * 
	 * Parameters:
	 * 	$pkgname - Name of the package.
	 * 	$version - The version to download (defaults to "current").
	 * 
	 * Throws:
	 * 	* A <DoesNotExistError> Exception, if the package was not found.
	 * 	* A <InvalidPackage> Exception, if the package was malformed.
	 * 
	 * Returns:
	 * 	A <PluginPackage> object.
	 */
	public function download_package($pkgname, $version = "current")
	{
		$found = False;
		foreach($this->packages as $p)
		{
			if($p[0] == $pkgname)
			{
				$found = True;
				break;
			}
		}
		if(!$found)
			throw new DoesNotExistError("Package not in package cache.");
		
		$raw = @file_get_contents($this->baseurl . "/packages/" . urlencode($pkgname) . "/versions/" . urlencode($version), False, $this->stream_ctx);
		if($raw === False)
			throw new DoesNotExistError();
		
		return PluginPackage::load($raw);
	}
}

/*
 * Class: Article
 * Representation of an article
 */
class Article extends BySQLRowEnabled
{
	private $id;
	
	private $section_id;
	private $section_obj;
	
	/*
	 * Variables: Public class variables
	 * 
	 * $urlname        - URL name
	 * $title          - Title (an <Multilingual> object)
	 * $text           - The text (an <Multilingual> object)
	 * $excerpt        - Excerpt (an <Multilingual> object)
	 * $meta           - Keywords, comma seperated
	 * $custom         - Custom fields, is an array
	 * $article_image  - The article <Image>. If none: NULL
	 * $status         - One of the ARTICLE_STATUS_* constants
	 * $timestamp      - Timestamp
	 * $allow_comments - Are comments allowed?
	 */
	public $urlname;
	public $title;
	public $text;
	public $excerpt;
	public $meta;
	public $custom;
	public $article_image;
	public $status;
	public $timestamp;
	public $allow_comments;
	
	protected function __construct()
	{
		$this->section_obj = NULL;
	}
	
	protected function populate_by_sqlrow($sqlrow)
	{
		$this->id             = $sqlrow["id"];
		$this->urlname        = $sqlrow["urlname"];
		$this->title          = Multilingual::by_id($sqlrow["title"]);
		$this->text           = Multilingual::by_id($sqlrow["text"]);
		$this->excerpt        = Multilingual::by_id($sqlrow["excerpt"]);
		$this->meta           = $sqlrow["meta"];
		$this->custom         = unserialize(base64_decode($sqlrow["custom"]));
		$this->article_image  = $sqlrow["article_image"] == 0 ? NULL : Image::by_id($sqlrow["article_image"]);
		$this->status         = $sqlrow["status"];
		$this->section_id     = $sqlrow["section"];
		$this->timestamp      = $sqlrow["timestamp"];
		$this->allow_comments = $sqlrow["allow_comments"] == 1;
	}
	
	/*
	 * Function: get_id
	 */
	public function get_id() { return $this->id; }
	
	/*
	 * Function: test_urlname
	 * Test, if a urlname is a valid urlname.
	 * 
	 * Parameters:
	 * 	$urlname - Urlname to test
	 * 
	 * Returns:
	 * 	True, if the urlname is valid, False otherwise.
	 */
	public static function test_urlname($urlname)
	{
		return (bool) preg_match('/^[a-zA-Z0-9-_]+$/', $urlname);
	}
	
	/*
	 * Function: test_status
	 * Test, if a status is valid.
	 * 
	 * Parameters:
	 * 	$status - Status value to test.
	 * 
	 * Returns:
	 * 	True, if the status is a valid status value, False otherwise.
	 */
	public static function test_status($status)
	{
		return is_numeric($status) and ($status >= 0) and ($status <= 3);
	}
	
	/*
	 * Constructor: create
	 * Create a new Article object.
	 * 
	 * Parameters:
	 * 	urlname - A unique URL name
	 * 
	 * Throws:
	 * 	<AlreadyExistsError>, <InvalidDataError>
	 */
	public static function create($urlname)
	{
		global $ratatoeskr_settings;
		global $db_con;
		
		if(!self::test_urlname($urlname))
			throw new InvalidDataError("invalid_urlname");
		
		try
		{
			self::by_urlname($urlname);
		}
		catch(DoesNotExistError $e)
		{
			$obj = new self();
			$obj->urlname        = $urlname;
			$obj->title          = Multilingual::create();
			$obj->text           = Multilingual::create();
			$obj->excerpt        = Multilingual::create();
			$obj->meta           = "";
			$obj->custom         = array();
			$obj->article_image  = NULL;
			$obj->status         = ARTICLE_STATUS_HIDDEN;
			$obj->section_id     = $ratatoeskr_settings["default_section"];
			$obj->timestamp      = time();
			$obj->allow_comments = $ratatoeskr_settings["allow_comments_default"];
		
			qdb("INSERT INTO `PREFIX_articles` (`urlname`, `title`, `text`, `excerpt`, `meta`, `custom`, `article_image`, `status`, `section`, `timestamp`, `allow_comments`) VALUES ('', ?, ?, ?, '', ?, 0, ?, ?, ?, ?)",
				$obj->title->get_id(),
				$obj->text->get_id(),
				$obj->excerpt->get_id(),
				base64_encode(serialize($obj->custom)),
				$obj->status,
				$obj->section_id,
				$obj->timestamp,
				$obj->allow_comments ? 1 : 0);
			$obj->id = $db_con->lastInsertId();
			return $obj;
		}
		
		throw new AlreadyExistsError();
	}
	
	/*
	 * Constructor: by_id
	 * Get by ID.
	 * 
	 * Parameters:
	 * 	$id - The ID.
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_id($id)
	{
		$stmt = qdb("SELECT `id`, `urlname`, `title`, `text`, `excerpt`, `meta`, `custom`, `article_image`, `status`, `section`, `timestamp`, `allow_comments` FROM `PREFIX_articles` WHERE `id` = ?", $id);
		$sqlrow = $stmt->fetch();
		if($sqlrow === False)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: by_urlname
	 * Get by urlname
	 * 
	 * Parameters:
	 * 	$urlname - The urlname
	 * 
	 * Throws:
	 * 	<DoesNotExistError>
	 */
	public static function by_urlname($urlname)
	{
		$stmt = qdb("SELECT `id`, `urlname`, `title`, `text`, `excerpt`, `meta`, `custom`, `article_image`, `status`, `section`, `timestamp`, `allow_comments` FROM `PREFIX_articles` WHERE `urlname` = ?", $urlname);
		$sqlrow = $stmt->fetch();
		if($sqlrow === False)
			throw new DoesNotExistError();
		
		return self::by_sqlrow($sqlrow);
	}
	
	/*
	 * Constructor: by_multi
	 * Get Articles by multiple criterias
	 *
	 * Parameters:
	 * 	$criterias - Array that can have these keys: id (int) , urlname (string), section (<Section> object), status (int), onlyvisible, langavail(string), tag (<Tag> object)
	 * 	$sortby    - Sort by this field (id, urlname, timestamp or title)
	 * 	$sortdir   - Sorting directory (ASC or DESC)
	 * 	$count     - How many entries (NULL for unlimited)
	 * 	$offset    - How many entries should be skipped (NULL for none)
	 * 	$perpage   - How many entries per page (NULL for no paging)
	 * 	$page      - Page number (starting at 1, NULL for no paging)
	 * 	&$maxpage  - Number of pages will be written here, if paging is activated.
	 * 
	 * Returns:
	 * 	Array of Article objects
	 */
	public static function by_multi($criterias, $sortby, $sortdir, $count, $offset, $perpage, $page, &$maxpage)
	{
		$subqueries = array();
		$subparams = array();
		foreach($criterias as $k => $v)
		{
			switch($k)
			{
				case "id":
					$subqueries[] = "`a`.`id` = ?";
					$subparams[] = $v;
					break;
				case "urlname":
					$subqueries[] = "`a`.`urlname` = ?";
					$subparams[] = $v;
					break;
				case "section":
					$subqueries[] = "`a`.`section` = ?";
					$subparams[] = $v->get_id();
					break;
				case "status":
					$subqueries[] = "`a`.`status` = ?";
					$subparams[] = $v;
					break;
				case "onlyvisible":
					$subqueries[] = "`a`.`status` != 0";
					break;
				case "langavail":
					$subqueries[] = "`b`.`language` = ?";
					$subparams[] = $v;
					break;
				case "tag":
					$subqueries[] = "`c`.`tag` = ?";
					$subparams[] = $v->get_id();
					break;
				default: continue;
			}
		}
		
		if(($sortdir != "ASC") and ($sortdir != "DESC"))
			$sortdir = "ASC";
		$sorting = "";
		switch($sortby)
		{
			case "id":        $sorting = "ORDER BY `a`.`id` $sortdir";        break;
			case "urlname":   $sorting = "ORDER BY `a`.`urlname` $sortdir";   break;
			case "timestamp": $sorting = "ORDER BY `a`.`timestamp` $sortdir"; break;
			case "title":     $sorting = "ORDER BY `b`.`text` $sortdir";      break;
		}
		
		$stmt = prep_stmt("SELECT `a`.`id` AS `id`, `a`.`urlname` AS `urlname`, `a`.`title` AS `title`, `a`.`text` AS `text`, `a`.`excerpt` AS `excerpt`, `a`.`meta` AS `meta`, `a`.`custom` AS `custom`, `a`.`article_image` AS `article_image`, `a`.`status` AS `status`, `a`.`section` AS `section`, `a`.`timestamp` AS `timestamp`, `a`.`allow_comments` AS `allow_comments` FROM `PREFIX_articles` `a`
INNER JOIN `PREFIX_translations` `b` ON `a`.`title` = `b`.`multilingual`
LEFT OUTER JOIN `PREFIX_article_tag_relations` `c` ON `a`.`id` = `c`.`article`
WHERE " . implode(" AND ", $subqueries) . " $sorting");
		
		$stmt->execute($subparams);
		
		$rows = array();
		$fetched_ids = array();
		while($sqlrow = $stmt->fetch())
		{
			if(!in_array($sqlrow["id"], $fetched_ids))
			{
				$rows[]        = $sqlrow;
				$fetched_ids[] = $sqlrow["id"];
			}
		}
		
		if($count !== NULL)
			$rows = array_slice($rows, 0, $count);
		if($offset !== NULL)
			$rows = array_slice($rows, $offset);
		if(($perpage !== NULL) and ($page !== NULL))
		{
			$maxpage = ceil(count($rows) / $perpage);
			$rows = array_slice($rows, $perpage * ($page - 1), $perpage);
		}
		
		$rv = array();
		foreach($rows as $r)
			$rv[] = self::by_sqlrow($r);
		return $rv;
	}
	
	/*
	 * Constructor: all
	 * Get all articles
	 * 
	 * Returns:
	 * 	Array of Article objects
	 */
	public static function all()
	{
		$rv = array();
		$stmt = qdb("SELECT `id`, `urlname`, `title`, `text`, `excerpt`, `meta`, `custom`, `article_image`, `status`, `section`, `timestamp`, `allow_comments` FROM `PREFIX_articles` WHERE 1");
		while($sqlrow = $stmt->fetch())
			$rv[] = self::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: get_comments
	 * Getting comments for this article.
	 * 
	 * Parameters:
	 * 	$limit_lang   - Get only comments in a language (empty string for no limitation, this is the default).
	 * 	$only_visible - Do you only want the visible comments? (Default: False)
	 * 
	 * Returns:
	 * 	Array of <Comment> objects.
	 */
	public function get_comments($limit_lang = "", $only_visible = False)
	{
		$rv = array();
		
		$conditions = array("`article` = ?");
		$arguments = array($this->id);
		if($limit_lang != "")
		{
			$conditions[] = "`language` = ?";
			$arguments[] = $limit_lang;
		}
		if($only_visible)
			$conditions[] = "`visible` = 1";
		
		$stmt = prep_stmt("SELECT `id`, `article`, `language`, `author_name`, `author_mail`, `text`, `timestamp`, `visible`, `read_by_admin` FROM `PREFIX_comments` WHERE " . implode(" AND ", $conditions));
		$stmt->execute($arguments);
		while($sqlrow = $stmt->fetch())
			$rv[] = Comment::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: get_tags
	 * Get all Tags of this Article.
	 * 
	 * Returns:
	 * 	Array of <Tag> objects.
	 */
	public function get_tags()
	{
		$rv = array();
		$stmt = qdb("SELECT `a`.`id` AS `id`, `a`.`name` AS `name`, `a`.`title` AS `title` FROM `PREFIX_tags` `a` INNER JOIN `PREFIX_article_tag_relations` `b` ON `a`.`id` = `b`.`tag` WHERE `b`.`article` = ?", $this->id);
		while($sqlrow = $stmt->fetch())
			$rv[] = Tag::by_sqlrow($sqlrow);
		return $rv;
	}
	
	/*
	 * Function: set_tags
	 * Set the Tags that should be associated with this Article.
	 * 
	 * Parameters:
	 * 	$tags - Array of <Tag> objects.
	 */
	public function set_tags($tags)
	{
		transaction(function() use (&$tags)
		{
			foreach($tags as $tag)
				$tag->save();
			
			qdb("DELETE FROM `PREFIX_article_tag_relations` WHERE `article`= ?", $this->id);
			
			$articleid = $this->id;
			if(!empty($tags))
			{
				$stmt = prep_stmt(
					"INSERT INTO `PREFIX_article_tag_relations` (`article`, `tag`) VALUES " .
					implode(",", array_fill(0, count($tags), "(?,?)"))
				);
				$args = array();
				foreach($tags as $tag)
				{
					$args[] = $articleid;
					$args[] = $tag->get_id();
				}
				$stmt->execute($args);
			}
		});
	}
	
	/*
	 * Function: get_section
	 * Get the section of this article.
	 * 
	 * Returns:
	 * 	A <Section> object.
	 */
	public function get_section()
	{
		if($this->section_obj === NULL)
			$this->section_obj = Section::by_id($this->section_id);
		return $this->section_obj;
	}
	
	/*
	 * Function: set_section
	 * Set the section of this article.
	 * 
	 * Parameters:
	 * 	$section - A <Section> object.
	 */
	public function set_section($section)
	{
		$this->section_id  = $section->get_id();
		$this->section_obj = $section;
	}
	
	/*
	 * Function: get_extradata
	 * Get the extradata for this article and the given plugin.
	 * 
	 * Parameters:
	 * 	$plugin_id - The ID of the plugin.
	 * 
	 * Returns:
	 * 	An <ArticleExtradata> object.
	 */
	public function get_extradata($plugin_id)
	{
		return new ArticleExtradata($this->id, $plugin_id);
	}
	
	/*
	 * Function: save
	 * 
	 * Throws:
	 * 	<AlreadyExistsError>, <InvalidDataError>
	 */
	public function save()
	{
		if(!self::test_urlname($this->urlname))
			throw new InvalidDataError("invalid_urlname");
		
		if(!self::test_status($this->status))
			throw new InvalidDataError("invalid_article_status");
			
		transaction(function()
		{
			$stmt = qdb("SELECT COUNT(*) AS `n` FROM `PREFIX_articles` WHERE `urlname` = ? AND `id` != ?", $this->urlname, $this->id);
			$sqlrow = $stmt->fetch();
			if($sqlrow["n"] > 0)
				throw new AlreadyExistsError();
			
			$this->title->save();
			$this->text->save();
			$this->excerpt->save();
			
			qdb("UPDATE `PREFIX_articles` SET `urlname` = ?, `title` = ?, `text` = ?, `excerpt` = ?, `meta` = ?, `custom` = ?, `article_image` = ?, `status` = ?, `section` = ?, `timestamp` = ?, `allow_comments` = ? WHERE `id` = ?",
				$this->urlname,
				$this->title->get_id(),
				$this->text->get_id(),
				$this->excerpt->get_id(),
				$this->meta,
				base64_encode(serialize($this->custom)),
				$this->article_image === NULL ? 0 : $this->article_image->get_id(),
				$this->status,
				$this->section_id,
				$this->timestamp,
				$this->allow_comments ? 1 : 0,
				$this->id
			);
		});
	}
	
	/*
	 * Function: delete
	 */
	public function delete()
	{
		transaction(function()
		{
			$this->title->delete();
			$this->text->delete();
			$this->excerpt->delete();
			
			foreach($this->get_comments() as $comment)
				$comment->delete();
			
			qdb("DELETE FROM `PREFIX_article_tag_relations` WHERE `article` = ?", $this->id);
			qdb("DELETE FROM `PREFIX_article_extradata` WHERE `article` = ?", $this->id);
			qdb("DELETE FROM `PREFIX_articles` WHERE `id` = ?", $this->id);
		});
	}
}

/*
 * Class: ArticleExtradata
 * A Key-Value-Storage assigned to Articles for plugins to store additional data.
 * Can be accessed like an array.
 * Keys are strings and Values can be everything serialize() can process.
 * 
 * Extends the abstract <KVStorage> class.
 */
class ArticleExtradata extends KVStorage
{
	/*
	 * Constructor: __construct
	 * 
	 * Parameters:
	 * 	$article_id - The ID of the Article.
	 * 	$plugin_id  - The ID of the Plugin.
	 */
	public function __construct($article_id, $plugin_id)
	{
		$this->init("PREFIX_article_extradata", array(
			"article" => $article_id,
			"plugin" => $plugin_id,
		));
	}
}

/*
 * Function: dbversion
 * Get the version of the database structure currently used.
 * 
 * Returns:
 * 	The numerical version of the current database structure.
 */
function dbversion()
{
	/* Is the meta table present? If no, the version is 0. */
	$stmt = qdb("SELECT COUNT(*) FROM `information_schema`.`tables` WHERE `table_schema` = ? AND `table_name` = ?",
		$config["mysql"]["db"], sub_prefix("PREFIX_meta"));
	list($n) = $stmt->fetch();
	if($n == 0)
		return 0;
	
	$stmt = qdb("SELECT `value` FROM `PREFIX_meta` WHERE `key` = 'dbversion'");
	$sqlrow = $stmt->fetch();
	return unserialize(base64_decode($sqlrow["value"]));
}

/*
 * Function: clean_database
 * Clean up the database
 */
function clean_database()
{
	global $ratatoeskr_settings;
	if((!isset($ratatoeskr_settings["last_db_cleanup"])) or ($ratatoeskr_settings["last_db_cleanup"] < (time() - 86400)))
	{
		Plugin::clean_db();
		$ratatoeskr_settings["last_db_cleanup"] = time();
	}
}

?>