From c4cc87d9d1557ddd4cae4b06b79696712f61a2ad Mon Sep 17 00:00:00 2001 From: Kevin Chabowski Date: Thu, 5 Jan 2012 14:43:33 +0100 Subject: New directory hierachy. index and setup implemented. --- r7r_repo/.htaccess | 4 + r7r_repo/config.php | 12 + r7r_repo/css/common.css | 54 ++ r7r_repo/css/images/error.png | Bin 0 -> 1072 bytes r7r_repo/css/images/error.svg | 44 ++ r7r_repo/css/images/notice.png | Bin 0 -> 1320 bytes r7r_repo/css/images/notice.svg | 106 +++ r7r_repo/css/images/success.png | Bin 0 -> 1518 bytes r7r_repo/css/images/success.svg | 102 +++ r7r_repo/css/main.css | 114 +++ r7r_repo/css/setup.css | 11 + r7r_repo/db.php | 106 +++ r7r_repo/main.php | 201 ++++++ r7r_repo/models.php | 396 +++++++++++ r7r_repo/pluginpackage.php | 288 ++++++++ r7r_repo/pwhash.php | 74 ++ r7r_repo/stupid_template_engine.php | 1297 +++++++++++++++++++++++++++++++++++ r7r_repo/templates/.htaccess | 4 + r7r_repo/templates/src/home.html | 35 + r7r_repo/templates/src/master.html | 90 +++ r7r_repo/templates/src/setup.html | 53 ++ r7r_repo/urlprocess.php | 179 +++++ r7r_repo/utils.php | 229 +++++++ 23 files changed, 3399 insertions(+) create mode 100644 r7r_repo/.htaccess create mode 100644 r7r_repo/config.php create mode 100644 r7r_repo/css/common.css create mode 100644 r7r_repo/css/images/error.png create mode 100644 r7r_repo/css/images/error.svg create mode 100644 r7r_repo/css/images/notice.png create mode 100644 r7r_repo/css/images/notice.svg create mode 100644 r7r_repo/css/images/success.png create mode 100644 r7r_repo/css/images/success.svg create mode 100644 r7r_repo/css/main.css create mode 100644 r7r_repo/css/setup.css create mode 100644 r7r_repo/db.php create mode 100644 r7r_repo/main.php create mode 100644 r7r_repo/models.php create mode 100644 r7r_repo/pluginpackage.php create mode 100644 r7r_repo/pwhash.php create mode 100644 r7r_repo/stupid_template_engine.php create mode 100755 r7r_repo/templates/.htaccess create mode 100644 r7r_repo/templates/src/home.html create mode 100644 r7r_repo/templates/src/master.html create mode 100644 r7r_repo/templates/src/setup.html create mode 100644 r7r_repo/urlprocess.php create mode 100644 r7r_repo/utils.php (limited to 'r7r_repo') diff --git a/r7r_repo/.htaccess b/r7r_repo/.htaccess new file mode 100644 index 0000000..dec4e25 --- /dev/null +++ b/r7r_repo/.htaccess @@ -0,0 +1,4 @@ + + Order Allow,Deny + Deny from all + diff --git a/r7r_repo/config.php b/r7r_repo/config.php new file mode 100644 index 0000000..7a05f24 --- /dev/null +++ b/r7r_repo/config.php @@ -0,0 +1,12 @@ + diff --git a/r7r_repo/css/common.css b/r7r_repo/css/common.css new file mode 100644 index 0000000..767d017 --- /dev/null +++ b/r7r_repo/css/common.css @@ -0,0 +1,54 @@ +* { + font-family: sans-serif; + font-size: 10pt; +} + +html { + margin: 0px; + padding: 0px; +} + +body { + margin: 0px; + padding: 0px; +} + +#maincontainer { + width: 80%; + margin: 0px auto 0px; + padding: 0px; +} + +div.error { + border: 1px solid #8d8d8d; + background: #ddd url(images/error.png) no-repeat top left; + min-height: 40px; + color: #222; + padding: 1em; + margin: 3mm 0mm 3mm; + text-align: center; +} + +div.notice { + border: 1px solid #8d8d8d; + background: #ddd url(images/notice.png) no-repeat top left; + min-height: 40px; + color: #222; + padding: 1em; + margin: 3mm 0mm 3mm; + text-align: center; +} + +div.success { + border: 1px solid #8d8d8d; + background: #ddd url(images/success.png) no-repeat top left; + min-height: 40px; + color: #222; + padding: 1em; + margin: 3mm 0mm 3mm; + text-align: center; +} + +.fullwidth { + width: 100%; +} diff --git a/r7r_repo/css/images/error.png b/r7r_repo/css/images/error.png new file mode 100644 index 0000000..de5e5f9 Binary files /dev/null and b/r7r_repo/css/images/error.png differ diff --git a/r7r_repo/css/images/error.svg b/r7r_repo/css/images/error.svg new file mode 100644 index 0000000..6ebe085 --- /dev/null +++ b/r7r_repo/css/images/error.svg @@ -0,0 +1,44 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/r7r_repo/css/images/notice.png b/r7r_repo/css/images/notice.png new file mode 100644 index 0000000..30957e8 Binary files /dev/null and b/r7r_repo/css/images/notice.png differ diff --git a/r7r_repo/css/images/notice.svg b/r7r_repo/css/images/notice.svg new file mode 100644 index 0000000..1025847 --- /dev/null +++ b/r7r_repo/css/images/notice.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/r7r_repo/css/images/success.png b/r7r_repo/css/images/success.png new file mode 100644 index 0000000..dc79180 Binary files /dev/null and b/r7r_repo/css/images/success.png differ diff --git a/r7r_repo/css/images/success.svg b/r7r_repo/css/images/success.svg new file mode 100644 index 0000000..39a606d --- /dev/null +++ b/r7r_repo/css/images/success.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/r7r_repo/css/main.css b/r7r_repo/css/main.css new file mode 100644 index 0000000..7061c42 --- /dev/null +++ b/r7r_repo/css/main.css @@ -0,0 +1,114 @@ +#heading { + text-align: center; + border-bottom: 1px solid black; + margin: 0px auto 0px; + padding: 10mm 3mm 5mm +} + +h1 { + font-size: 24pt; + font-weight: bold; + padding: 0px; + margin: 0px auto 2mm; +} + +h2 { + font-size: 12pt; + font-weight: bold; +} + +#mainmenu { + border-bottom: 1px solid black; + list-style: none; + height: 10mm; + padding: 0px; + margin: 0px 0px 2mm; + overflow: hidden; +} + +#mainmenu li { + float: left; + margin: 0px 0px 2mm; +} + +#mainmenu li a { + color: #444; + text-decoration: none; + font-size: 12pt; + margin: 0px; + padding: 2mm 7.5mm 0px; + background: white; + display: block; + height: 10mm; +} + +#mainmenu li.active a { + color: black; + font-weight: bold; +} + +#mainmenu li a:hover { + background: #eee; + color: #000; +} + +#metabar { + float: right; + width: 50mm; + margin: 0px; + padding: 0px 0px 0px 5mm; + border-left: 1px solid black; +} + +div.metabar_module { + border-top: 1px solid black; + padding: 2mm 0px 0px; + margin: 2mm 0px 0px; +} + +div.metabar_module:first-child { + border-top: none; + margin: 0px; + padding: 0px; +} + +div.metabar_module h2 { + font-size: 10pt; + font-weight: bold; + padding: 0px; + margin: 0px 0px 2mm; +} + +#content { + border-right: 1px solid black; + margin: 0px 55mm 0px 0px; + padding: 0px 2mm 0px 0px; +} + +#footer { + clear: both; + height: 5mm; +} + +table.listtab { + border-collapse: collapse; +} + +table.listtab th { + font-weight: bold; + color: black; + text-align: left; + padding-left: 1ex; +} + +table.listtab td { + border-top: 1px solid #eee; +} + +table.listtab tr:first-child td { + border-top: 1px solid #666; +} + +table.listtab tbody tr:hover { + background: #eee; +} diff --git a/r7r_repo/css/setup.css b/r7r_repo/css/setup.css new file mode 100644 index 0000000..2466a4a --- /dev/null +++ b/r7r_repo/css/setup.css @@ -0,0 +1,11 @@ +body { + text-align: center; +} + +h1 { + font-weight: bold; + font-size: 16pt; + border-bottom: 1px solid gray; + margin: 0px 0px 1em; + padding: 1em 0px 0.5em; +} diff --git a/r7r_repo/db.php b/r7r_repo/db.php new file mode 100644 index 0000000..459a973 --- /dev/null +++ b/r7r_repo/db.php @@ -0,0 +1,106 @@ +, but needs arguments as single array. + * + * Parameters: + * $args - The arguments as an array. + * + * Returns: + * The formatted string. + */ +function qdb_vfmt($args) +{ + global $config; + + if(count($args) < 1) + throw new InvalidArgumentException('Need at least one parameter'); + + $query = $args[0]; + + $data = array_map(function($x) { return is_string($x) ? sqlesc($x) : $x; }, array_slice($args, 1)); + $query = str_replace("PREFIX_", $config["mysql"]["prefix"], $query); + + return vsprintf($query, $data); +} + +/* + * Function: qdb_fmt + * Formats a string like , that means it replaces "PREFIX_" and 's everything before sends everything to vsprintf. + * + * Returns: + * The formatted string. + */ +function qdb_fmt() +{ + return qdb_vfmt(func_get_args()); +} + + +/* + * Function: qdb + * Query Database. + * + * This function replaces mysql_query and should eliminate SQL-Injections. + * Use it like this: + * + * $result = qdb("SELECT `foo` FROM `bar` WHERE `id` = %d AND `baz` = '%s'", 100, "lol"); + * + * It will also replace "PREFIX_" with the prefix defined in 'config.php'. + */ +function qdb() +{ + $query = qdb_vfmt(func_get_args()); + $rv = mysql_query($query); + if($rv === false) + throw new MySQLException(mysql_errno() . ': ' . mysql_error() . (__DEBUG__ ? ("[[FULL QUERY: " . $query . "]]") : "" )); + return $rv; +} + +/* + * Class: MySQLException + * Will be thrown by qdb*, if the query induced an MySQL error. + */ +class MySQLException extends Exception { } + +?> diff --git a/r7r_repo/main.php b/r7r_repo/main.php new file mode 100644 index 0000000..af19397 --- /dev/null +++ b/r7r_repo/main.php @@ -0,0 +1,201 @@ +register_tag( + "loremipsum", + function($ste, $params, $sub) + { + $repeats = empty($params["repeat"]) ? 1 : $params["repeat"] + 0; + return implode("\n\n", array_repeat("

Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

\n\n

Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.

\n\n

Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.

\n\n

Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.

\n\n

Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.

\n\n

At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.

\n\n

Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.

", $repeats)); + } +); + +/* start session and check auth. */ +session_start(); +$user = NULL; +if(isset($_SESSION["r7r_repo_login_name"])) +{ + try + { + $user = User::by_name($_SESSION["r7r_repo_login_name"]); + } + catch(DoesNotExistError $e) + { + unset($_SESSION["r7r_repo_login_name"]); + } +} + +/* url handlers */ +$url_handlers = array( + "_prelude" => function(&$data, $url_now, &$url_next) + { + global $ste, $settings, $user; + + if(@$settings["setup_finished"]) + $ste->vars["repo"] = array( + "name" => $settings["repo_name"], + "description" => $settings["repo_description"], + "baseurl" => $settings["repo_baseurl"], + "public" => ($settings["repo_mode"] == "public") + ); + + if($user === NULL) + $ste->vars["user"] = array( + "logged_in" => False, + "admin" => False + ); + else + $ste->vars["user"] = array( + "logged_in" => True, + "name" => $user->name, + "admin" => $user->isadmin + ); + }, + "_notfound" => url_action_simple(function($data) + { + header("HTTP/1.1 404 Not Found"); + header("Content-Type: text/plain"); + echo "404 Not Found\nThe resource \"{$_SERVER["REQUEST_URI"]}\" could not be found.\n"; + }), + "_index" => url_action_alias(array("index")), + "index" => function(&$data, $url_now, &$url_next) + { + global $ste; + $url_next = array(); + + $ste->vars["menu"] = "home"; + + $latest = Package::latest(); + $ste->vars["latest_pkgs"] = array_map(function($pkg) { return array( + "name" => $pkg->get_name(), + "version" => $pkg->txtversion, + "description" => $pkg->description, + "last_update" => $pkg->lastversion + ); }, $latest); + + echo $ste->exectemplate("home.html"); + }, + "setup" => function(&$data, $url_now, &$url_next) + { + global $settings, $ste; + + /* If initial setup was already finished, nobody should be allowed to access this. */ + if(@$settings["setup_finished"]) + throw new NotFoundError(); + + $url_next = array(); + + /* Test file permissions */ + $permissions_missing = array_filter(array("/packages", "/packagelist", "/repometa"), function($f) { return !@is_writable(dirname(__FILE__) . "/..$f"); }); + if(!empty($permissions_missing)) + $ste->vars["error"] = "No writing permissions on these files/directories: \"" . implode("\", \"", $permissions_missing) . "\""; + else + { + /* Check input */ + if(!empty($_POST["send_data"])) + { + if(empty($_POST["admin_name"]) or empty($_POST["admin_password"]) or empty($_POST["repo_name"]) or empty($_POST["repo_description"]) or empty($_POST["repo_baseurl"]) or (($_POST["repo_mode"] != "public") and ($_POST["repo_mode"] != "private"))) + $ste->vars["error"] = "Form not filled out completely"; + else + { + /* Insert data */ + $admin = User::create($_POST["admin_name"]); + $admin->pwhash = PasswordHash::create($_POST["admin_password"]); + $admin->isadmin = True; + $admin->save(); + $settings["repo_name"] = $_POST["repo_name"]; + $settings["repo_description"] = $_POST["repo_description"]; + $settings["repo_baseurl"] = $_POST["repo_baseurl"]; + $settings["repo_mode"] = $_POST["repo_mode"]; + + update_repometa(); + + $settings["setup_finished"] = True; + + $url_next = array("index"); + return; + } + } + } + + $ste->vars["baseurl_predicted"] = self_url(); + echo $ste->exectemplate("setup.html"); + } +); + +/* bootstrapping... */ +$urlpath = explode("/", $_GET["action"]); +$rel_path_to_root = implode("/", array_merge(array("."), array_repeat("..", count($urlpath) - 1))); +$GLOBALS["rel_path_to_root"] = $rel_path_to_root; +$data = array("rel_path_to_root" => $rel_path_to_root); +$ste->vars["rel_path_to_root"] = $rel_path_to_root; +/* Enforce setup */ +if(!@$settings["setup_finished"]) + $urlpath = array("setup"); +/*try +{*/ + url_process($urlpath, $url_handlers, $data); +/*} +catch(Exception $e) +{ + header("HTTP/1.1 500 Internal Server Error"); + header("Content-Type: text/plain"); + echo "Internal Server Error\nReason: " . get_class($e) . "(" . $e->getMessage() . ") thrown.\n"; +}*/ + +/* save settings */ +$settings->save(); + +?> diff --git a/r7r_repo/models.php b/r7r_repo/models.php new file mode 100644 index 0000000..1eb7caf --- /dev/null +++ b/r7r_repo/models.php @@ -0,0 +1,396 @@ +populate_by_sqlrow($sqlrow); + return $obj; + } +} + +/* SettingsIterator ans Settings copied from Ratatöskr */ + +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(); + $result = qdb("SELECT `key`, `value` FROM `PREFIX_settings_kvstorage` WHERE 1"); + while($sqlrow = mysql_fetch_assoc($result)) + $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() + { + foreach($this->to_be_deleted as $k) + qdb("DELETE FROM `PREFIX_settings_kvstorage` WHERE `key` = '%s'", $k); + foreach($this->to_be_updated as $k) + qdb("UPDATE `PREFIX_settings_kvstorage` SET `value` = '%s' WHERE `key` = '%s'", base64_encode(serialize($this->buffer[$k])), $k); + foreach($this->to_be_created as $k) + qdb("INSERT INTO `PREFIX_settings_kvstorage` (`key`, `value`) VALUES ('%s', '%s')", $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); } +} + +$settings = Settings::get_instance(); + +/* users */ +class User extends BySQLRowEnabled +{ + private $name; + private $id; + + public $pwhash; + public $isadmin; + + protected function __construct() {} + + protected function populate_by_sqlrow($sqlrow) + { + $this->id = $sqlrow["id"]; + $this->name = $sqlrow["name"]; + $this->pwhash = $sqlrow["pwhash"]; + $this->isadmin = $sqlrow["isadmin"] == 1; + } + + function get_id() { return $this->id; } + function get_name() { return $this->name; } + + public static function create($name) + { + try + { + self::by_name($name); + throw new AlreadyExistsError(); + } + catch(DoesNotExistError $e) + { + $obj = new self; + $obj->name = $name; + $obj->pwhash = ""; + $obj->isadmin = False; + qdb("INSERT INTO `PREFIX_users` (`name`, `pwhash`, `isadmin`) VALUES ('%s', '', 0)", $name); + $obj->id = mysql_insert_id(); + return $obj; + } + } + + public static function by_id($id) + { + $result = qdb("SELECT `id`, `name`, `pwhash`, `isadmin` FROM `PREFIX_users` WHERE `id` = %d", $id); + $sqlrow = mysql_fetch_assoc($result); + if($sqlrow === False) + throw new DoesNotExistError(); + return self::by_sqlrow($sqlrow); + } + + public static function by_name($name) + { + $result = qdb("SELECT `id`, `name`, `pwhash`, `isadmin` FROM `PREFIX_users` WHERE `name` = '%s'", $name); + $sqlrow = mysql_fetch_assoc($result); + if($sqlrow === False) + throw new DoesNotExistError(); + return self::by_sqlrow($sqlrow); + } + + public static function all() + { + $rv = array(); + $result = qdb("SELECT `id`, `name`, `pwhash`, `isadmin` FROM `PREFIX_users` WHERE 1"); + while($sqlrow = mysql_fetch_assoc($result)) + $rv[] = self::by_sqlrow($sqlrow); + return $rv; + } + + public function get_packages() + { + $rv = array(); + $result = qdb("SELECT `id`, `name`, `user`, `lastversion`, `description`, `lastupdate`, `txtversion` FROM `PREFIX_packages` WHERE `user` = %d", $this->id); + while($sqlrow = mysql_fetch_assoc($result)) + $rv[] = Package::by_sqlrow($result); + return $rv; + } + + public function save() + { + qdb("UPDATE `PREFIX_users` SET `isadmin` = %d, `pwhash` = '%s' WHERE `id` = %d", ($this->isadmin ? 1 : 0), $this->pwhash, $this->id); + } + + public function delete() + { + qdb("DELETE FROM `PREFIX_users` WHERE `id` = %d", $this->id); + } +} + +class Package extends BySQLRowEnabled +{ + private $id; + private $name; + private $user; + + public $lastversion; + public $description; + public $lastupdate; + public $txtversion; + + public function get_id() { return $id; } + public function get_name() { return $name; } + public function get_user() { return $user; } + + protected function __construct() {} + + protected function populate_by_sqlrow($sqlrow) + { + $this->id = $sqlrow["id"]; + $this->name = $sqlrow["name"]; + $this->user = User::by_id($sqlrow["user"]); + $this->lastversion = $sqlrow["lastversion"]; + $this->description = $sqlrow["description"]; + $this->lastupdate = $sqlrow["lastupdate"]; + $this->txtversion = $sqlrow["txtversion"]; + } + + public static function create($name, $user) + { + if(preg_match("/^[0-9a-zA-Z_\\-]+$/", $name) != 1) + throw new InvalidArgumentException("Invalid package name (must be min 1 char, 0-9A-Za-z_-"); + try + { + self::by_name($name); + throw new AlreadyExistsError(); + } + catch(DoesNotExistError $e) + { + $obj = new self; + $obj->name = $name; + $obj->user = $user; + $obj->lastupdate = time(); + $obj->lastversion = 0; + $obj->txtversion = ""; + $obj->description = ""; + + qdb("INSERT INTO `PREFIX_packages` (`name`, `user`, `lastupdate`, `lastversion`, `txtversion`, description`, `description`) VALUES ('%s', %d, UNIX_TIMESTAMP(), 0, '', '')", $name, $user->get_id()); + $obj->id = mysql_insert_id(); + + mkdir(dirname(__FILE__) . "/../packages/" . $this->name); + + return $obj; + } + } + + public static function by_id($id) + { + $result = qdb("SELECT `id`, `name`, `user`, `lastversion`, `description`, `lastupdate`, `txtversion` FROM `PREFIX_packages` WHERE `id` = %d", $id); + $sqlrow = mysql_fetch_assoc($result); + if($sqlrow === False) + throw new DoesNotExistError(); + return self::by_sqlrow($sqlrow); + } + + public static function by_name($name) + { + $result = qdb("SELECT `id`, `name`, `user`, `lastversion`, `description`, `lastupdate`, `txtversion` FROM `PREFIX_packages` WHERE `name` = '%s'", $name); + $sqlrow = mysql_fetch_assoc($result); + if($sqlrow === False) + throw new DoesNotExistError(); + return self::by_sqlrow($sqlrow); + } + + public static function update_lists() + { + $packagelist = array(); + $result = qdb("SELECT `id`, `name`, `user`, `lastversion`, `description`, `lastupdate`, `txtversion` FROM `PREFIX_packages` WHERE 1"); + while($sqlrow = mysql_fetch_assoc($result)) + $packagelist[] = array($sqlrow["name"], $sqlrow["lastversion"], $sqlrow["description"]); + file_put_contents(dirname(__FILE__) . "/../packagelist", serialize($packagelist)); + } + + public static function all() + { + $rv = array(); + $result = qdb("SELECT `id`, `name`, `user`, `lastversion`, `description`, `lastupdate`, `txtversion` FROM `PREFIX_packages` WHERE 1"); + while($sqlrow = mysql_fetch_assoc($result)) + $rv[] = self::by_sqlrow($sqlrow); + return $rv; + } + + public static function latest() + { + $rv = array(); + $result = qdb("SELECT `id`, `name`, `user`, `lastversion`, `description`, `lastupdate`, `txtversion` FROM `PREFIX_packages` WHERE 1 ORDER BY `lastupdate` DESC LIMIT 0,15"); + while($sqlrow = mysql_fetch_assoc($result)) + $rv[] = self::by_sqlrow($sqlrow); + return $rv; + } + + public static function search($search) + { + $rv = array(); + $result = qdb("SELECT `id`, `name`, `user`, `lastversion`, `description`, `lastupdate`, `txtversion` FROM `PREFIX_packages` WHERE `name` LIKE '%%%s%%' OR `description` LIKE '%%%s%%'", $search, $search); + while($sqlrow = mysql_fetch_assoc($result)) + $rv[] = self::by_sqlrow($sqlrow); + return $rv; + } + + public function newversion($pgk) + { + global $settings; + if($pkg->name != $this->name) + throw new NotAllowedError("Package name not equal."); + if($pkg->versioncount <= $this->lastversion) + throw new NotAllowedError("Older or same version."); + $pkg->updatepath = $settings["root_url"] . "/packages/" . urlencode($this->name) . "/update"; + + $pkg_ser = $pkg->save(); + file_put_contents(dirname(__FILE__) . "/../packages/" . urlencode($this>name) . "/versions/" . $pkg->versioncount, $pkg_ser); + file_put_contents(dirname(__FILE__) . "/../packages/" . urlencode($this>name) . "/versions/current", $pkg_ser); + $meta = $pkg->extract_meta(); + file_put_contents(dirname(__FILE__) . "/../packages/" . urlencode($this>name) . "/meta", serialize($meta)); + + $this->lastversion = $pkg->versioncount; + $this->txtversion = $pkg->versiontext; + $this->description = $pkg->short_description; + $this->lastupdate = time(); + $this->save(); + + $update_info = array( + "current-version" => $this->lastversion, + "dl-path" => $settings["root_url"] . "/packages/" . urlencode($this->name) . "/versions/" . $this->lastversion + ); + + file_put_contents(dirname(__FILE__) . "/../packages/" . urlencode($this>name) . "/update", serialize($update_info)); + + self::update_lists(); + } + + public function save() + { + qdb("UPDATE `PREFIX_packages` SET `lastversion` = %d, `lastupdate` = %d, `txtversion` = '%s', `description` = '%s' WHERE `id` = %d", $this->lastversion, $this->lastupdate, $this->txtversion, $this->description, $this->id); + } + + public function delete() + { + qdb("DELETE FROM `PREFIX_packages` WHERE `id` = %d", $this->id); + delete_directory(dirname(__FILE__) . "/../packages/" . $this->name); + self::update_lists(); + } +} + +function update_repometa() +{ + global $settings; + file_put_contents(dirname(__FILE__) . "/../repometa", serialize(array( + "name" => $settings["repo_name"], + "description" => $settings["repo_description"] + ))); +} + +?> diff --git a/r7r_repo/pluginpackage.php b/r7r_repo/pluginpackage.php new file mode 100644 index 0000000..d8ce170 --- /dev/null +++ b/r7r_repo/pluginpackage.php @@ -0,0 +1,288 @@ + $v) + { + $k = "$dir/$k"; + if(is_array($v)) + array2dir($v, $k); + else + file_put_contents($k, $v); + } +} + +/* + * Class: InvalidPackage + * An Exception that 's function can throw, if the package is invalid. + */ +class InvalidPackage extends Exception {} + +/* + * Class: PluginPackage + * A plugin package representation. + */ +class PluginPackage +{ + public static $magic = "R7RPLGPACKV001"; + + /* + * Variables: Mandatory values + * + * $code - The plugin code + * $classname - The name of the plugins main class + * $name - Name of the plugin (must be at least one character, allowed chars: a-z A-Z 0-9 - _) + * $author - The author of the plugin (preferably in the format: Name) + * $versiontext - A text to describe the current version, something like "1.1 Beta" + * $versioncount - A number for this version, should be increased with every release + * $api - The used API version + * $short_description - A short description. + */ + public $code = NULL; + public $classname = NULL; + public $name = NULL; + public $author = NULL; + public $versiontext = NULL; + public $versioncount = NULL; + public $api = NULL; + public $short_description = NULL; + + /* + * Variables: Optional values + * + * $updatepath - A URL that points to a update information resource (serialize'd array("current-version" => VERSIONCOUNT, "dl-path" => DOWNLOAD PATH); will get overwritten/set by the default repository software. + * $web - A URL to the webpage for the plugin. If left empty, the default repository software will set this to the description page of your plugin. + * $license - The license text of your plugin. + * $help - A help / manual (formatted in HTML) for your plugin. + * $custompub - 'd directory that contains custom public(i.e. can later be accessed from the web) data. + * $custompriv - 'd directory that contains custom private data. + * $tpls - 'd directory containing custom STE templates. + */ + public $updatepath = NULL; + public $web = NULL; + public $license = NULL; + public $help = NULL; + public $custompub = NULL; + public $custompriv = NULL; + public $tpls = NULL; + + /* + * Function: validate + * Validate, if the variables are set correctly. + * Will throw an exception if invalid. + */ + public function validate() + { + function validate_url ($u) { return preg_match("/^http[s]{0,1}:\\/\\/.*$/", $u) != 0; } + function validate_arraydir($a) + { + if(!is_array($a)) + return False; + foreach($a as $k=>$v) + { + if(!is_string($k)) + return False; + if(is_array($v) and (!validate_arraydir($v))) + return False; + elseif(!is_string($v)) + return False; + } + return True; + } + + if(!is_string($this->code)) + throw new InvalidPackage("Invalid code value."); + if(!is_string($this->classname)) + throw new InvalidPackage("Invalid classname value."); + if(preg_match("/^[a-zA-Z0-9_\\-]+$/", $this->name) == 0) + throw new InvalidPackage("Invalid name value (must be at least 1 character, accepted chars: a-z A-Z 0-9 - _)."); + if(!is_string($this->author)) + throw new InvalidPackage("Invalid author value."); + if(!is_string($this->versiontext)) + throw new InvalidPackage("Invalid versiontext value."); + if(!is_numeric($this->versioncount)) + throw new InvalidPackage("Invalid versioncount value. Must be a number."); + if(!is_numeric($this->api)) + throw new InvalidPackage("Invalid api value. Must be a number."); + if(!is_string($this->short_description)) + throw new InvalidPackage("Invalid short_description value."); + + if(($this->updatepath !== NULL) and (!validate_url($this->updatepath))) + throw new InvalidPackage("Invalid updatepath value. Must be an URL."); + if(($this->web !== NULL) and (!validate_url($this->web))) + throw new InvalidPackage("Invalid web value. Must be an URL."); + if(($this->license !== NULL) and (!is_string($this->license))) + throw new InvalidPackage("Invalid license value."); + if(($this->help !== NULL) and (!is_string($this->help))) + throw new InvalidPackage("Invalid help value."); + if(($this->custompub !== NULL) and (!validate_arraydir($this->custompub))) + throw new InvalidPackage("Invalid custompub value."); + if(($this->custompriv !== NULL) and (!validate_arraydir($this->custompriv))) + throw new InvalidPackage("Invalid custompriv value."); + if(($this->tpls !== NULL) and (!validate_arraydir($this->tpls))) + throw new InvalidPackage("Invalid tpls value."); + return True; + } + + /* + * Function: load + * Load a plugin package from binary data. + * + * Parameters: + * $plugin_raw - The raw package to load. + * + * Returns: + * The object. + * + * Throws: + * if package is invalid. + */ + public static function load($plugin_raw) + { + /* Read and compare magic number */ + $magic = substr($plugin_raw, 0, strlen(self::$magic)); + if($magic != self::$magic) + throw new InvalidPackage("Wrong magic number"); + + /* Read sha1sum and uncompress serialized plugin, then compare the hash */ + $sha1sum = substr($plugin_raw, strlen(self::$magic), 20); + $pluginser = gzuncompress(substr($plugin_raw, strlen(self::$magic) + 20)); + if(sha1($pluginser, True) != $sha1sum) + throw new InvalidPackage("Wrong SHA1 hash"); + + $plugin = @unserialize($pluginser); + if(!($plugin instanceof self)) + throw new InvalidPackage("Not the correct class or not unserializeable."); + + $plugin->validate(); + + return $plugin; + } + + /* + * Function: save + * Save the plugin. + * + * Returns: + * A binary plugin package. + * + * Throws: + * if package is invalid. + */ + public function save() + { + $this->validate(); + $ser = serialize($this); + return self::$magic . sha1($ser, True) . gzcompress($ser, 9); + } + + /* + * Function: extract_meta + * Get just the metadata of this package. + * + * Returns: + * A object. + */ + public function extract_meta() + { + $meta = new PluginPackageMeta(); + + $meta->name = $this>name; + $meta->author = $this>author; + $meta->versiontext = $this>versiontext; + $meta->versioncount = $this>versioncount; + $meta->api = $this>api; + $meta->short_description = $this>short_description; + $meta->updatepath = $this>updatepath; + $meta->web = $this>web; + $meta->license = $this>license; + + return $meta; + } +} + +/* + * Class: PluginPackageMeta + * Only the metadata of a . + */ +class PluginPackageMeta +{ + /* + * Variables: Mandatory values + * + * $name - Name of the plugin (must be at least one character, allowed chars: a-z A-Z 0-9 - _) + * $author - The author of the plugin (preferably in the format: Name) + * $versiontext - A text to describe the current version, something like "1.1 Beta" + * $versioncount - A number for this version, should be increased with every release + * $api - The used API version + * $short_description - A short description. + */ + public $name = NULL; + public $author = NULL; + public $versiontext = NULL; + public $versioncount = NULL; + public $api = NULL; + public $short_description = NULL; + + /* + * Variables: Optional values + * + * $updatepath - A URL that points to a update information resource (serialize'd array("current-version" => VERSIONCOUNT, "dl-path" => DOWNLOAD PATH); will get overwritten/set by the default repository software. + * $web - A URL to the webpage for the plugin. If left empty, the default repository software will set this to the description page of your plugin. + * $license - The license text of your plugin. + */ + public $updatepath = NULL; + public $web = NULL; + public $license = NULL; +} + +?> diff --git a/r7r_repo/pwhash.php b/r7r_repo/pwhash.php new file mode 100644 index 0000000..8ec4762 --- /dev/null +++ b/r7r_repo/pwhash.php @@ -0,0 +1,74 @@ + diff --git a/r7r_repo/stupid_template_engine.php b/r7r_repo/stupid_template_engine.php new file mode 100644 index 0000000..0edb880 --- /dev/null +++ b/r7r_repo/stupid_template_engine.php @@ -0,0 +1,1297 @@ +tpl = $tpl; + $this->offset = $off; + } +} + +class TextNode extends ASTNode +{ + public $text; +} + +class TagNode extends ASTNode +{ + public $name; + public $params; + public $sub; +} + +class VariableNode extends ASTNode +{ + public $name; + public $arrayfields; + public function transcompile() + { + $varaccess = '@$ste->vars[' . (is_numeric($this->name) ? $this->name : '\'' . escape_text($this->name) . '\''). ']'; + foreach($this->arrayfields as $af) + { + if((count($af) == 1) and ($af[0] instanceof TextNode) and is_numeric($af->text)) + $varaccess .= '[' . $af->text . ']'; + else + $varaccess .= '[' . implode(".", + array_map( + function($node) + { + if($node instanceof TextNode) + return "\"" . escape_text($node->text) . "\""; + else if($node instanceof VariableNode) + return $node->transcompile(); + }, $af + ) + ). ']'; + } + return $varaccess; + } +} + +class ParseCompileError extends \Exception +{ + public $msg; + public $tpl; + public $off; + + public function __construct($msg, $tpl, $offset, $code = 0, $previous = NULL) + { + $this->msg = $msg; + $this->tpl = $tpl; + $this->off = $offset; + $this->message = "$msg (Template $tpl, Offset $offset)"; + } + + public function rewrite($code) + { + $line = substr_count(str_replace("\r\n", "\n", $code), "\n", 0, $this->off + 1) + 1; + $this->message = "{$this->msg} (Template $tpl, Line $line)"; + $this->is_rewritten = True; + } +} + +/* $text must start after the first opening bracket */ +function find_closing_bracket($text, $opening, $closing) +{ + $counter = 1; + $len = strlen($text); + for($i = 0; $i < $len; ++$i) + { + switch($text[$i]) + { + case $opening: + ++$counter; + break; + case $closing: + --$counter; + break; + } + if($counter == 0) + break; + } + + if($counter > 0) + throw new \Exception("Missing closing \"$closing\". Stop."); + + return $i; +} + +function instance_in_array($classname, $a) +{ + foreach($a as $v) + { + if($v instanceof $classname) + return True; + } + return False; +} + +function unescape_text($text) +{ + return stripcslashes($text); +} + +function tokenize_text($text, $tpl, $off) +{ + $tokens = array(); + /* Find next non-escaped $-char */ + if(preg_match("/(?:(?text = preg_replace("/^\\n\\s*/s", "", unescape_text($text)); + return (strlen($node->text) == 0) ? array() : array($node); + } + + if($match[0][1] > 0) + { + $node = new TextNode($tpl, $off); + $node->text = unescape_text(substr($text, 0, $match[0][1])); + $tokens[] = $node; + } + + if($text[$match[0][1] + 1] == "{") + { + try + { + $varend = find_closing_bracket(substr($text, $match[0][1] + 2), "{", "}") + $match[0][1] + 2; + } + catch(\Exception $e) + { + throw new ParseCompileError("Parse Error: Missing closing '}'", $tpl, $off + $match[0][1] + 1); + } + return array_merge( + $tokens, + tokenize_text("\$" . substr($text, $match[0][1] + 2, ($varend - 1) - ($match[0][1] + 1)), $tpl, $off + $match[0][1] + 2), + tokenize_text(substr($text, $varend + 1), $tpl, $off + $varend + 1) + ); + } + + $text = substr($text, $match[0][1] + 1); + $off += $match[0][1] + 1; + if(preg_match("/^[a-zA-Z0-9_]+/s", $text, $match, PREG_OFFSET_CAPTURE) == 0) + { + $nexttokens = tokenize_text($text, $tpl, $off); + if($nexttokens[0] instanceof TextNode) + $nexttokens[0]->text = "\$" . $nexttokens[0]->text; + else + { + $node = new TextNode($tpl, $off); + $node->text = "\$"; + $tokens[] = $node; + } + return array_merge($tokens, $nexttokens); + } + + $node = new VariableNode($tpl, $off + $match[0][1]); + $node->name = $match[0][0]; + $node->arrayfields = array(); + + $text = substr($text, $match[0][1] + strlen($match[0][0])); + $off += $match[0][1] + strlen($match[0][0]); + while(@$text[0] == "[") + { + $text = substr($text, 1); + $off += 1; + try + { + $fieldend = find_closing_bracket($text, "[", "]"); + } + catch(\Exception $e) + { + throw new ParseCompileError("Parse Error: Missing closing ']'", $tpl, $off - 1); + } + $node->arrayfields[] = tokenize_text(substr($text, 0, $fieldend), $tpl, $off); + $text = substr($text, $fieldend + 1); + $off += $fieldend + 1; + } + + $tokens[] = $node; + + return strlen($text) > 0 ? array_merge($tokens, tokenize_text($text, $tpl, $off)) : $tokens; +} + +function mk_ast($code, $tpl, $err_off) +{ + $ast = array(); + + if(preg_match("/\\<\\s*ste:([a-zA-Z0-9_]*)/s", $code, $matches, PREG_OFFSET_CAPTURE) == 0) + return tokenize_text($code, $tpl, $err_off); + + $ast = tokenize_text(substr($code, 0, $matches[0][1]), $tpl, $err_off); + $tag = new TagNode($tpl, $err_off + $matches[0][1]); + $tag->name = $matches[1][0]; + + $code = substr($code, $matches[0][1] + strlen($matches[0][0])); + $err_off += $matches[0][1] + strlen($matches[0][0]); + + $tag->params = array(); + + while(preg_match("/^\\s+([a-zA-Z0-9_]+)=((?:\"(?:.*?)(? 0) + { + $paramval = substr($code, $matches[2][1] + 1, strlen($matches[2][0]) - 2); + $paramval = str_replace("\\\"", "\"", $paramval); + $paramval = str_replace("\\'", "'", $paramval); + $tag->params[$matches[1][0]] = tokenize_text($paramval, $tpl, $err_off + $matches[2][1] + 1); + $code = substr($code, strlen($matches[0][0])); + $err_off += strlen($matches[0][0]); + } + + if(preg_match("/^\\s*([\\/]?)\\s*\\>/s", $code, $matches) == 0) + throw new ParseCompileError("Parse Error: Missing closing '>' in \"" . $tag->name . "\"-Tag.", $tpl, $tag->offset); + + $code = substr($code, strlen($matches[0])); + $err_off += strlen($matches[0]); + + $tag->sub = array(); + + if($matches[1][0] != "/") + { + $off = 0; + $last_tag_start = 0; + $tagstack = array(array($tag->name, $tag->offset)); + while(preg_match("/\\<((?:\\s*)|(?:\\s*\\/\\s*))ste:([a-zA-Z0-9_]*)(?:\\s+(?:[a-zA-Z0-9_]+)=(?:(?:\"(?:.*?)(?/s", $code, $matches, PREG_OFFSET_CAPTURE, $off) > 0) /* RegEx from hell! Matches all Tags. Opening, closing and self-closing ones. */ + { + if(trim($matches[3][0]) != "/") + { + $closingtag = trim($matches[1][0]); + if($closingtag[0] == "/") + { + list($matching_opentag, $mo_off) = array_pop($tagstack); + if($matching_opentag != $matches[2][0]) + throw new ParseCompileError("Parse Error: Missing closing \"ste:$matching_opentag\"-Tag.", $tpl, $mo_off + $err_off); + } + else + $tagstack[] = array($matches[2][0], $matches[0][1]); + } + $last_tag_start = $matches[0][1]; + $off = $last_tag_start + strlen($matches[0][0]); + if(empty($tagstack)) + break; + } + + if((!empty($tagstack)) or ($tag->name != $matches[2][0])) + throw new ParseCompileError("Parse Error: Missing closing \"ste:" . $tag->name . "\"-Tag.", $tpl, $tag->offset); + + if($tag->name == "rawtext") + { + $tag = new TextNode($tpl, $err_off); + $tag->text = substr($code, 0, $last_tag_start); + } + else if($tag->name == "comment") + $tag = NULL; /* All this work to remove a comment ... */ + else + $tag->sub = mk_ast(substr($code, 0, $last_tag_start), $tpl, $err_off); + $code = substr($code, $off); + $err_off += $off; + } + + if($tag !== NULL) + $ast[] = $tag; + return array_merge($ast, strlen($code) > 0 ? mk_ast($code, $tpl, $err_off) : array()); +} + +/* + * Function: precompile + * Precompiling STE T/PL templates. + * You only need this function, if you want to manually transcompile a template. + * + * Parameters: + * $code - The input code + * + * Returns: + * The precompiled code. + */ +function precompile($code) +{ + $code = preg_replace( /* Transform short form of comparison (~{a|op|b}) to long form */ + "/(?:(?", + $code + ); + $code = preg_replace( /* Transform short form of if-clause (?{cond|then|else}) to long form */ + "/(?:(?\$1\$2\$3", + $code + ); + /* Unescape \? \~ \{ \} \| */ + $code = preg_replace("/(?:(?. + */ +function parse($code, $tpl) +{ + return mk_ast($code, $tpl, 0); +} + +function indent_code($code) +{ + return implode( + "\n", + array_map( + function($line) { return "\t$line"; }, + explode("\n", $code) + ) + ); +} + +/* We could also just eval() the $infix_math code, but this is much cooler :-D (Parser inception) */ +function shunting_yard($infix_math) +{ + $operators = array( + "+" => array("l", 2), + "-" => array("l", 2), + "*" => array("l", 3), + "/" => array("l", 3), + "^" => array("r", 4), + "_" => array("r", 5), + "(" => array("", 0), + ")" => array("", 0) + ); + + preg_match_all("/\s*(?:(?:[+\\-\\*\\/\\^\\(\\)])|(\\d*[\\.]?\\d*))\\s*/s", $infix_math, $tokens, PREG_PATTERN_ORDER); + $tokens_raw = array_filter(array_map('trim', $tokens[0]), function($x) { return ($x === "0") or (!empty($x)); }); + $output_queue = array(); + $op_stack = array(); + + $lastpriority = NULL; + /* Make - unary, if neccessary */ + $tokens = array(); + foreach($tokens_raw as $token) + { + $priority = isset($operators[$token]) ? $operators[$token][1] : -1; + if(($token == "-") and (($lastpriority === NULL) or ($lastpriority >= 0))) + { + $priority = $operators["_"][1]; + $tokens[] = "_"; + } + else + $tokens[] = $token; + $lastpriority = $priority; + } + + while(!empty($tokens)) + { + $token = array_shift($tokens); + if(is_numeric($token)) + $output_queue[] = $token; + else if($token == "(") + $op_stack[] = $token; + else if($token == ")") + { + $lbr_found = False; + while(!empty($op_stack)) + { + $op = array_pop($op_stack); + if($op == "(") + { + $lbr_found = True; + break; + } + $output_queue[] = $op; + } + if(!$lbr_found) + throw new \Exception("Bracket mismatch."); + } + else if(!isset($operators[$token])) + throw new \Exception("Invalid token ($token): Not a number, bracket or operator. Stop."); + else + { + $priority = $operators[$token][1]; + if($operators[$token][0] == "l") + while((!empty($op_stack)) and ($priority <= $operators[$op_stack[count($op_stack)-1]][1])) + $output_queue[] = array_pop($op_stack); + else + while((!empty($op_stack)) and ($priority < $operators[$op_stack[count($op_stack)-1]][1])) + $output_queue[] = array_pop($op_stack); + $op_stack[] = $token; + } + } + + while(!empty($op_stack)) + { + $op = array_pop($op_stack); + if($op == "(") + throw new \Exception("Bracket mismatch..."); + $output_queue[] = $op; + } + + return $output_queue; +} + +function pop2(&$array) +{ + $rv = array(array_pop($array), array_pop($array)); + if(array_search(NULL, $rv, True) !== False) + throw new \Exception("Not enough numbers on stack. Invalid formula."); + return $rv; +} + +function calc_rpn($rpn) +{ + $stack = array(); + foreach($rpn as $token) + { + switch($token) + { + case "+": + list($b, $a) = pop2($stack); + $stack[] = $a + $b; + break; + case "-": + list($b, $a) = pop2($stack); + $stack[] = $a - $b; + break; + case "*": + list($b, $a) = pop2($stack); + $stack[] = $a * $b; + break; + case "/": + list($b, $a) = pop2($stack); + $stack[] = $a / $b; + break; + case "^": + list($b, $a) = pop2($stack); + $stack[] = pow($a, $b); + break; + case "_": + $a = array_pop($stack); + if($a === NULL) + throw new \Exception("Not enough numbers on stack. Invalid formula."); + $stack[] = -$a; + break; + default: + $stack[] = $token; + break; + } + } + return array_pop($stack); +} + +function loopbody($code) +{ + return "try\n{\n" . indent_code($code) . "\n}\ncatch(\ste\BreakException \$e) { break; }\ncatch(\ste\ContinueException \$e) { continue; }\n"; +} + +$ste_builtins = array( + "if" => function($ast) + { + $output = ""; + $condition = array(); + $then = NULL; + $else = NULL; + + foreach($ast->sub as $node) + { + if(($node instanceof TagNode) and ($node->name == "then")) + $then = $node->sub; + else if(($node instanceof TagNode) and ($node->name == "else")) + $else = $node->sub; + else + $condition[] = $node; + } + + if($then === NULL) + throw new ParseCompileError("Transcompile error: Missing in .", $ast->tpl, $ast->offset); + + $output .= "\$outputstack[] = \"\";\n\$outputstack_i++;\n"; + $output .= _transcompile($condition); + $output .= "\$outputstack_i--;\nif(\$ste->evalbool(array_pop(\$outputstack)))\n{\n"; + $output .= indent_code(_transcompile($then)); + $output .= "\n}\n"; + if($else !== NULL) + { + $output .= "else\n{\n"; + $output .= indent_code(_transcompile($else)); + $output .= "\n}\n"; + } + return $output; + }, + "cmp" => function($ast) + { + $operators = array( + array('eq', '=='), + array('neq', '!='), + array('lt', '<'), + array('lte', '<='), + array('gt', '>'), + array('gte', '>=') + ); + + $code = ""; + + if(isset($ast->params["var_b"])) + $b = '$ste->get_var_by_name(' . _transcompile($ast->params["var_b"], True) . ')'; + else if(isset($ast->params["text_b"])) + $b = _transcompile($ast->params["text_b"], True); + else + throw new ParseCompileError("Transcompile error: neiter var_b nor text_b set in .", $ast->tpl, $ast->offset); + + if(isset($ast->params["var_a"])) + $a = '$ste->get_var_by_name(' . _transcompile($ast->params["var_a"], True) . ')'; + else if(isset($ast->params["text_a"])) + $a = _transcompile($ast->params["text_a"], True); + else + throw new ParseCompileError("Transcompile error: neiter var_a nor text_a set in .", $ast->tpl, $ast->offset); + + if(!isset($ast->params["op"])) + throw new ParseCompileError("Transcompile error: op not given in .", $ast->tpl, $ast->offset); + if((count($ast->params["op"]) == 1) and ($ast->params["op"][0] instanceof TextNode)) + { + /* Operator is known at compile time, this saves *a lot* of output code! */ + $op = trim($ast->params["op"][0]->text); + $op_php = NULL; + foreach($operators as $v) + { + if($v[0] == $op) + { + $op_php = $v[1]; + break; + } + } + if($op_php === NULL) + throw new ParseCompileError("Transcompile Error: Unknown operator in ", $ast->tpl, $ast->offset); + $code .= "\$outputstack[\$outputstack_i] .= (($a) $op_php ($b)) ? 'yes' : '';\n"; + } + else + { + $code .= "switch(trim(" . _transcompile($ast->params["op"], True) . "))\n{\n\t"; + $code .= implode("", array_map( + function($op) use ($a,$b) + { + list($op_stetpl, $op_php) = $op; + return "case '$op_stetpl':\n\t\$outputstack[\$outputstack_i] .= (($a) $op_php ($b)) ? 'yes' : '';\n\tbreak;\n\t"; + }, $operators + )); + $code .= "default: throw new \Exception('Runtime Error: Unknown operator in .');\n}\n"; + } + return $code; + }, + "not" => function($ast) + { + $code = "\$outputstack[] = '';\n\$outputstack_i++;\n"; + $code .= _transcompile($ast->sub); + $code .= "\$outputstack_i--;\n\$outputstack[\$outputstack_i] .= (!\$ste->evalbool(array_pop(\$outputstack))) ? 'yes' : '';\n"; + return $code; + }, + "even" => function($ast) + { + $code = "\$outputstack[] = '';\n\$outputstack_i++;\n"; + $code .= _transcompile($ast->sub); + $code .= "\$outputstack_i--;\n\$tmp_even = array_pop(\$outputstack);\n\$outputstack[\$outputstack_i] .= (is_numeric(\$tmp_even) and (\$tmp_even % 2 == 0)) ? 'yes' : '';\n"; + return $code; + }, + "for" => function($ast) + { + $code = ""; + $loopname = "forloop_" . str_replace(".", "_", uniqid("",True)); + if(empty($ast->params["start"])) + throw new ParseCompileError("Transcompile error: Missing 'start' parameter in .", $ast->tpl, $ast->offset); + $code .= "\$${loopname}_start = " . _transcompile($ast->params["start"], True) . ";\n"; + + if(empty($ast->params["stop"])) + throw new ParseCompileError("Transcompile error: Missing 'end' parameter in .", $ast->tpl, $ast->offset); + $code .= "\$${loopname}_stop = " . _transcompile($ast->params["stop"], True) . ";\n"; + + $step = NULL; /* i.e. not known at compilation time */ + if(empty($ast->params["step"])) + $step = 1; + else if((count($ast->params["step"]) == 1) and ($ast->params["step"][0] instanceof TextNode)) + $step = $ast->params["step"][0]->text + 0; + else + $code .= "\$${loopname}_step = " . _transcompile($ast->params["step"], True) . ";\n"; + + if(!empty($ast->params["counter"])) + $code .= "\$${loopname}_countername = " . _transcompile($ast->params["counter"], True) . ";\n"; + + $loopbody = empty($ast->params["counter"]) ? "" : "\$ste->set_var_by_name(\$${loopname}_countername, \$${loopname}_counter);\n"; + $loopbody .= _transcompile($ast->sub); + $loopbody = indent_code("{\n" . loopbody(indent_code($loopbody)) . "\n}\n"); + + if($step === NULL) + { + $code .= "if(\$${loopname}_step == 0)\n\tthrow new \Exception('Runtime Error: step can not be 0 in .');\n"; + $code .= "if(\$${loopname}_step > 0)\n{\n"; + $code .= "\tfor(\$${loopname}_counter = \$${loopname}_start; \$${loopname}_counter <= \$${loopname}_stop; \$${loopname}_counter += \$${loopname}_step)\n"; + $code .= $loopbody; + $code .= "\n}\nelse\n{\n"; + $code .= "\tfor(\$${loopname}_counter = \$${loopname}_start; \$${loopname}_counter >= \$${loopname}_stop; \$${loopname}_counter += \$${loopname}_step)\n"; + $code .= $loopbody; + $code .= "\n}\n"; + } + else if($step == 0) + throw new ParseCompileError("Transcompile Error: step can not be 0 in .", $ast->tpl, $ast->offset); + else if($step > 0) + $code .= "for(\$${loopname}_counter = \$${loopname}_start; \$${loopname}_counter <= \$${loopname}_stop; \$${loopname}_counter += $step)\n$loopbody\n"; + else + $code .= "for(\$${loopname}_counter = \$${loopname}_start; \$${loopname}_counter >= \$${loopname}_stop; \$${loopname}_counter += $step)\n$loopbody\n"; + + return $code; + }, + "foreach" => function($ast) + { + $loopname = "foreachloop_" . str_replace(".", "_", uniqid("",True)); + $code = ""; + + if(empty($ast->params["array"])) + throw new ParseCompileError("Transcompile Error: array not given in .", $ast->tpl, $ast->offset); + $code .= "\$${loopname}_arrayvar = " . _transcompile($ast->params["array"], True) . ";\n"; + + if(empty($ast->params["value"])) + throw new ParseCompileError("Transcompile Error: value not given in .", $ast->tpl, $ast->offset); + $code .= "\$${loopname}_valuevar = " . _transcompile($ast->params["value"], True) . ";\n"; + + if(!empty($ast->params["key"])) + $code .= "\$${loopname}_keyvar = " . _transcompile($ast->params["key"], True) . ";\n"; + + if(!empty($ast->params["counter"])) + $code .= "\$${loopname}_countervar = " . _transcompile($ast->params["counter"], True) . ";\n"; + + $loopbody = ""; + $code .= "\$${loopname}_array = \$ste->get_var_by_name(\$${loopname}_arrayvar);\n"; + $code .= "if(!is_array(\$${loopname}_array))\n\t\$${loopname}_array = array();\n"; + if(!empty($ast->params["counter"])) + { + $code .= "\$${loopname}_counter = -1;\n"; + $loopbody .= "\$${loopname}_counter++;\n\$ste->set_var_by_name(\$${loopname}_countervar, \$${loopname}_counter);\n"; + } + + $loopbody .= "\$ste->set_var_by_name(\$${loopname}_valuevar, \$${loopname}_value);\n"; + if(!empty($ast->params["key"])) + $loopbody .= "\$ste->set_var_by_name(\$${loopname}_keyvar, \$${loopname}_key);\n"; + $loopbody .= "\n"; + $loopbody .= _transcompile($ast->sub); + $loopbody = "{\n" . loopbody(indent_code($loopbody)) . "\n}\n"; + + $code .= "foreach(\$${loopname}_array as \$${loopname}_key => \$${loopname}_value)\n$loopbody\n"; + + return $code; + }, + "infloop" => function($ast) + { + return "while(True)\n{\n" . loopbody(indent_code(_transcompile($ast->sub)) . "\n}") . "\n"; + }, + "break" => function($ast) + { + return "throw new \\ste\\BreakException();\n"; + }, + "continue" => function($ast) + { + return "throw new \\ste\\ContinueException();\n"; + }, + "block" => function($ast) + { + if(empty($ast->params["name"])) + throw new ParseCompileError("Transcompile Error: name missing in .", $ast->tpl, $ast->offset); + + $blknamevar = "blockname_" . str_replace(".", "_", uniqid("", True)); + + $code = "\$${blknamevar} = " . _transcompile($ast->params["name"], True) . ";\n"; + + $tmpblk = uniqid("", True); + $code .= "\$ste->blocks['$tmpblk'] = array_pop(\$outputstack);\n\$ste->blockorder[] = '$tmpblk';\n\$outputstack = array('');\n\$outputstack_i = 0;\n"; + + $code .= _transcompile($ast->sub); + + $code .= "\$ste->blocks[\$${blknamevar}] = array_pop(\$outputstack);\n"; + $code .= "if(array_search(\$${blknamevar}, \$ste->blockorder) === FALSE)\n\t\$ste->blockorder[] = \$${blknamevar};\n\$outputstack = array('');\n\$outputstack_i = 0;\n"; + + return $code; + }, + "load" => function($ast) + { + if(empty($ast->params["name"])) + throw new ParseCompileError("Transcompile Error: name missing in .", $ast->tpl, $ast->offset); + + return "\$outputstack[\$outputstack_i] .= \$ste->load(" . _transcompile($ast->params["name"], True) . ");\n"; + }, + "mktag" => function($ast) + { + if(empty($ast->params["name"])) + throw new ParseCompileError("Transcompile Error: name missing in .", $ast->tpl, $ast->offset); + + $tagname = _transcompile($ast->params["name"], True); + + $fxbody = "\$outputstack = array(); \$outputstack_i = 0;\$ste->vars['_tag_parameters'] = \$params;\n"; + + if(!empty($ast->params["mandatory"])) + { + $code .= "\$outputstack[] = '';\n\$outputstack_i++;\n"; + $code .= _transcompile($ast->params["mandatory"]); + $code .= "\$outputstack_i--;\n\$mandatory_params = explode('|', array_pop(\$outputstack));\n"; + + $fxbody .= "foreach(\$mandatory_params as \$mp)\n{\n\tif(!isset(\$params[\$mp]))\n\t\tthrow new \Exception(\"Runtime Error: \$mp missing in . Stop.\");\n}"; + } + + $fxbody .= _transcompile($ast->sub); + $fxbody .= "return array_pop(\$outputstack);"; + + $code .= "\$tag_fx = function(\$ste, \$params, \$sub) use (\$mandatory_params)\n{\n" . indent_code($fxbody) . "\n};\n"; + $code .= "\$ste->register_tag($tagname, \$tag_fx);\n"; + + return $code; + }, + "tagcontent" => function($ast) + { + return "\$outputstack[\$outputstack_i] .= \$sub(\$ste);"; + }, + "set" => function($ast) + { + if(empty($ast->params["var"])) + throw new ParseCompileError("Transcompile Error: var missing in .", $ast->tpl, $ast->offset); + + $code = "\$outputstack[] = '';\n\$outputstack_i++;\n"; + $code .= _transcompile($ast->sub); + $code .= "\$outputstack_i--;\n"; + + $code .= "\$ste->set_var_by_name(" . _transcompile($ast->params["var"], True) . ", array_pop(\$outputstack));\n"; + + return $code; + }, + "calc" => function($ast) + { + $code = "\$outputstack[] = '';\n\$outputstack_i++;\n"; + $code .= _transcompile($ast->sub); + $code .= "\$outputstack_i--;\n\$outputstack[\$outputstack_i] .= \$ste->calc(array_pop(\$outputstack));\n"; + + return $code; + } +); + +function escape_text($text) +{ + return addcslashes($text, "\r\n\t\$\0..\x1f\\'\"\x7f..\xff"); +} + +function _transcompile($ast, $no_outputstack = False) /* The real transcompile function, does not add boilerplate code. */ +{ + $code = ""; + global $ste_builtins; + + $text_and_var_buffer = array(); + + foreach($ast as $node) + { + if($node instanceof TextNode) + $text_and_var_buffer[] = '"' . escape_text($node->text) . '"'; + else if($node instanceof VariableNode) + $text_and_var_buffer[] = $node->transcompile(); + else if($node instanceof TagNode) + { + if(!empty($text_and_var_buffer)) + { + $code .= "\$outputstack[\$outputstack_i] .= " . implode (" . ", $text_and_var_buffer) . ";\n"; + $text_and_var_buffer = array(); + } + if(isset($ste_builtins[$node->name])) + $code .= $ste_builtins[$node->name]($node); + else + { + $paramarray = "parameters_" . str_replace(".", "_", uniqid("", True)); + $code .= "\$$paramarray = array();\n"; + + foreach($node->params as $pname => $pcontent) + $code .= "\$${paramarray}['" . escape_text($pname) . "'] = " . _transcompile($pcontent, True) . ";\n"; + + $code .= "\$outputstack[\$outputstack_i] .= \$ste->call_tag('" . escape_text($node->name) . "', \$${paramarray}, "; + $code .= empty($node->sub) ? "function(\$ste) { return ''; }" : transcompile($node->sub); + $code .= ");\n"; + } + } + } + + if(!empty($text_and_var_buffer)) + { + if(!$no_outputstack) + $code .= "\$outputstack[\$outputstack_i] .= "; + $code .= implode (" . ", $text_and_var_buffer); + if(!$no_outputstack) + $code .= ";\n"; + $text_and_var_buffer = array(); + } + else if($no_outputstack) + { + $code = "\"\""; + } + + return $code; +} + +$ste_transc_boilerplate = "\$outputstack = array('');\n\$outputstack_i = 0;\n"; + +/* + * Function: transcompile + * Transcompiles an abstract syntax tree to PHP. + * You only need this function, if you want to manually transcompile a template. + * + * Parameters: + * $ast - The abstract syntax tree to transcompile. + * + * Returns: + * PHP code. The PHP code is an anonymous function expecting a instance as its parameter and returns a string (everything that was not pached into a section). + */ +function transcompile($ast) /* Transcompile and add some boilerplate code. */ +{ + global $ste_transc_boilerplate; + return "function(\$ste)\n{\n" . indent_code($ste_transc_boilerplate . _transcompile($ast) . "return array_pop(\$outputstack);") . "\n}"; +} + +/* + * Constants: Template modes + * + * MODE_SOURCE - The Templates source + * MODE_TRANSCOMPILED - The transcompiled template + */ +const MODE_SOURCE = 0; +const MODE_TRANSCOMPILED = 1; + +/* + * Class: StorageAccess + * An interface. + * A StorageAccess implementation is used to access the templates from any storage. + * This means, that you are not limited to store the Templates inside directories, you can also use a database or something else. + */ +interface StorageAccess +{ + /* + * Function: load + * Loading a template. + * + * Parameters: + * $tpl - The name of the template. + * &$mode - Which mode is preferred? One of the