diff options
author | Simon Rettberg | 2015-03-02 16:51:04 +0100 |
---|---|---|
committer | Simon Rettberg | 2015-03-02 16:51:04 +0100 |
commit | ab23338fe9f1b3ed21455867f1c032d7b146ceb8 (patch) | |
tree | 33d27fd848e25ae24a57b309348b5d73a811f223 /inc | |
download | bwlp-webadmin-ab23338fe9f1b3ed21455867f1c032d7b146ceb8.tar.gz bwlp-webadmin-ab23338fe9f1b3ed21455867f1c032d7b146ceb8.tar.xz bwlp-webadmin-ab23338fe9f1b3ed21455867f1c032d7b146ceb8.zip |
Initial Commit
Diffstat (limited to 'inc')
-rw-r--r-- | inc/database.inc.php | 128 | ||||
-rw-r--r-- | inc/message.inc.php | 119 | ||||
-rw-r--r-- | inc/render.inc.php | 221 | ||||
-rw-r--r-- | inc/request.inc.php | 45 | ||||
-rw-r--r-- | inc/rpc.inc.php | 59 | ||||
-rw-r--r-- | inc/session.inc.php | 99 | ||||
-rw-r--r-- | inc/user.inc.php | 176 | ||||
-rw-r--r-- | inc/util.inc.php | 289 |
8 files changed, 1136 insertions, 0 deletions
diff --git a/inc/database.inc.php b/inc/database.inc.php new file mode 100644 index 0000000..efc330f --- /dev/null +++ b/inc/database.inc.php @@ -0,0 +1,128 @@ +<?php + +/** + * Handle communication with the database + * This is a very thin layer between you and PDO. + */ +class Database +{ + + /** + * + * @var \PDO Database handle + */ + private static $dbh = false; + private static $statements = array(); + + /** + * Get database schema version - used for checking for updates + * @return int Version of db schema + */ + public static function getExpectedSchemaVersion() + { + return 9; + } + + public static function needSchemaUpdate() + { + return Property::getCurrentSchemaVersion() < self::getExpectedSchemaVersion(); + } + + /** + * Connect to the DB if not already connected. + */ + private static function init() + { + if (self::$dbh !== false) + return; + try { + if (CONFIG_SQL_FORCE_UTF8) + self::$dbh = new PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASS, array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8")); + else + self::$dbh = new PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASS); + } catch (PDOException $e) { + Util::traceError('Connecting to the local database failed: ' . $e->getMessage()); + } + } + + /** + * If you just need the first row of a query you can use this. + * + * @return array|boolean Associative array representing row, or false if no row matches the query + */ + public static function queryFirst($query, $args = array(), $ignoreError = false) + { + $res = self::simpleQuery($query, $args, $ignoreError); + if ($res === false) + return false; + return $res->fetch(PDO::FETCH_ASSOC); + } + + + /** + * Execute the given query and return the number of rows affected. + * Mostly useful for UPDATEs or INSERTs + * + * @param string $query Query to run + * @param array $args Arguments to query + * @param boolean $ignoreError Ignore query errors and just return false + * @return int|boolean Number of rows affected, or false on error + */ + public static function exec($query, $args = array(), $ignoreError = false) + { + $res = self::simpleQuery($query, $args, $ignoreError); + if ($res === false) + return false; + return $res->rowCount(); + } + + /** + * Get id (promary key) of last row inserted. + * + * @return int the id + */ + public static function lastInsertId() + { + return self::$dbh->lastInsertId(); + } + + /** + * Execute the given query and return the corresponding PDOStatement object + * Note that this will re-use PDOStatements, so if you run the same + * query again with different params, do not rely on the first PDOStatement + * still being valid. If you need to do something fancy, use Database::prepare + * @return \PDOStatement The query result object + */ + public static function simpleQuery($query, $args = array(), $ignoreError = false) + { + self::init(); + try { + if (!isset(self::$statements[$query])) { + self::$statements[$query] = self::$dbh->prepare($query); + } else { + self::$statements[$query]->closeCursor(); + } + if (self::$statements[$query]->execute($args) === false) { + if ($ignoreError) + return false; + Util::traceError("Database Error: \n" . implode("\n", self::$statements[$query]->errorInfo())); + } + return self::$statements[$query]; + } catch (Exception $e) { + if ($ignoreError) + return false; + Util::traceError("Database Error: \n" . $e->getMessage()); + } + } + + /** + * Simply calls PDO::prepare and returns the PDOStatement. + * You must call PDOStatement::execute manually on it. + */ + public static function prepare($query) + { + self:init(); + return self::$dbh->prepare($query); + } + +} diff --git a/inc/message.inc.php b/inc/message.inc.php new file mode 100644 index 0000000..7decc12 --- /dev/null +++ b/inc/message.inc.php @@ -0,0 +1,119 @@ +<?php + +class Message +{ + private static $list = array(); + private static $alreadyDisplayed = array(); + private static $flushed = false; + + /** + * Add error message to page. If messages have not been flushed + * yet, it will be added to the queue, otherwise it will be added + * in place during rendering. + */ + public static function addError($id) + { + self::add('error', $id, func_get_args()); + } + + public static function addWarning($id) + { + self::add('warning', $id, func_get_args()); + } + + public static function addInfo($id) + { + self::add('info', $id, func_get_args()); + } + + public static function addSuccess($id) + { + self::add('success', $id, func_get_args()); + } + + /** + * Internal function that adds a message. Used by + * addError/Success/Info/... above. + */ + private static function add($type, $id, $params) + { + self::$list[] = array( + 'type' => $type, + 'id' => $id, + 'params' => array_slice($params, 1) + ); + if (self::$flushed) self::renderList(); + } + + /** + * Render all currently queued messages, flushing the queue. + * After calling this, any further calls to add* will be rendered in + * place in the current page output. + */ + public static function renderList() + { + // Non-Ajax + foreach (self::$list as $item) { + $message = $item['id']; // Dictionary::getMessage($item['id']); + foreach ($item['params'] as $index => $text) { + $message = str_replace('{{' . $index . '}}', '<b>' . htmlspecialchars($text) . '</b>', $message); + } + Render::addTemplate('messagebox-' . $item['type'], array('message' => $message)); + self::$alreadyDisplayed[] = $item; + } + self::$list = array(); + self::$flushed = true; + } + + /** + * Get all queued messages, flushing the queue. + * Useful in api/ajax mode. + */ + public static function asString() + { + $return = ''; + foreach (self::$list as $item) { + $message = $item['id']; // Dictionary::getMessage($item['id']); + foreach ($item['params'] as $index => $text) { + $message = str_replace('{{' . $index . '}}', $text, $message); + } + $return .= '[' . $item['type'] . ']: ' . $message . "\n"; + self::$alreadyDisplayed[] = $item; + } + self::$list = array(); + return $return; + } + + /** + * Deserialize any messages from the current HTTP request and + * place them in the message queue. + */ + public static function fromRequest() + { + $messages = is_array($_REQUEST['message']) ? $_REQUEST['message'] : array($_REQUEST['message']); + foreach ($messages as $message) { + $data = explode('|', $message); + if (count($data) < 2 || !preg_match('/^(error|warning|info|success)$/', $data[0])) continue; + self::add($data[0], $data[1], array_slice($data, 1)); + } + } + + /** + * Turn the current message queue into a serialized version, + * suitable for appending to a GET or POST request + */ + public static function toRequest() + { + $parts = array(); + foreach (array_merge(self::$list, self::$alreadyDisplayed) as $item) { + $str = 'message[]=' . urlencode($item['type'] . '|' .$item['id']); + if (!empty($item['params'])) { + $str .= '|' . implode('|', $item['params']); + } + $parts[] = $str; + } + return implode('&', $parts); + } + +} + diff --git a/inc/render.inc.php b/inc/render.inc.php new file mode 100644 index 0000000..0147709 --- /dev/null +++ b/inc/render.inc.php @@ -0,0 +1,221 @@ +<?php + +define('RENDER_DEFAULT_TITLE', 'bwLehrpool'); + + +require_once('Mustache/Autoloader.php'); +Mustache_Autoloader::register(); + +/** + * HTML rendering helper class + */ +Render::init(); + +class Render +{ + + private static $mustache = false; + private static $body = ''; + private static $header = ''; + private static $footer = ''; + private static $title = ''; + private static $templateCache = array(); + private static $tags = array(); + + public static function init() + { + if (self::$mustache !== false) + Util::traceError('Called Render::init() twice!'); + self::$mustache = new Mustache_Engine; + } + + /** + * Output the buffered, generated page + */ + public static function output() + { + Header('Content-Type: text/html; charset=utf-8'); + $zip = isset($_SERVER['HTTP_ACCEPT_ENCODING']) && (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false); + if ($zip) + ob_start(); + echo + '<!DOCTYPE html> + <html> + <head> + <title>', RENDER_DEFAULT_TITLE, self::$title, '</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <!-- Bootstrap --> + <link href="', CONFIG_PREFIX, 'style/bootstrap.min.css" rel="stylesheet" media="screen"> + <link href="', CONFIG_PREFIX, 'style/default.css" rel="stylesheet" media="screen"> + <script type="text/javascript"> + var TOKEN = "', Session::get('token'), '"; + </script> + ', + self::$header + , + ' </head> + <body> + <div class="container" id="mainpage"> + ', + self::$body + , + ' </div> + <script src="', CONFIG_PREFIX, 'script/jquery.js"></script> + <script src="', CONFIG_PREFIX, 'script/bootstrap.min.js"></script> + ', + self::$footer + , + '</body> + </html>' + ; + if ($zip) { + Header('Content-Encoding: gzip'); + ob_implicit_flush(false); + $gzip_contents = ob_get_contents(); + ob_end_clean(); + echo "\x1f\x8b\x08\x00\x00\x00\x00\x00"; + echo substr(gzcompress($gzip_contents, 5), 0, -4); + } + } + + /** + * Set the page title (title-tag) + */ + public static function setTitle($title) + { + self::$title = ' - ' . $title; + } + + /** + * Add raw html data to the header-section of the generated page + */ + public static function addHeader($html) + { + self::$header .= $html . "\n"; + } + + /** + * Add raw html data to the footer-section of the generated page (right before the closing body tag) + */ + public static function addFooter($html) + { + self::$footer .= $html . "\n"; + } + + /** + * Add given js script file from the script directory to the header + * + * @param string $file file name of script + */ + public static function addScriptTop($file) + { + self::addHeader('<script src="', CONFIG_PREFIX, 'script/' . $file . '.js"></script>'); + } + + /** + * Add given js script file from the script directory to the bottom + * + * @param string $file file name of script + */ + public static function addScriptBottom($file) + { + self::addFooter('<script src="', CONFIG_PREFIX, 'script/' . $file . '.js"></script>'); + } + + /** + * Add the given template to the output, using the given params for placeholders in the template + */ + public static function addTemplate($template, $params = false) + { + self::$body .= self::parse($template, $params); + } + + /** + * Add a dialog to the page output. + * + * @param string $title Title of the dialog window + * @param boolean $next URL to next dialog step, or false to hide the next button + * @param string $template template used to fill the dialog body + * @param array $params parameters for rendering the body template + */ + public static function addDialog($title, $next, $template, $params = false) + { + self::addTemplate('dialog-generic', array( + 'title' => $title, + 'next' => $next, + 'body' => self::parse($template, $params) + )); + } + + /** + * Add error message to page + */ + public static function addError($message) + { + self::addTemplate('messagebox-error', array('message' => $message)); + } + + /** + * Parse template with given params and return; do not add to body + * @param string $template name of template, relative to templates/, without .html extension + * @return string Rendered template + */ + public static function parse($template, $params = false) + { + // Load html snippet + $html = self::getTemplate($template); + // Always add token to parameter list + if (is_array($params) || $params === false || is_null($params)) + $params['token'] = Session::get('token'); + // Return rendered html + return self::$mustache->render($html, $params); + } + + /** + * Open the given html tag, optionally adding the passed assoc array of params + */ + public static function openTag($tag, $params = false) + { + array_push(self::$tags, $tag); + if (!is_array($params)) { + self::$body .= '<' . $tag . '>'; + } else { + self::$body .= '<' . $tag; + foreach ($params as $key => $val) { + self::$body .= ' ' . $key . '="' . htmlspecialchars($val) . '"'; + } + self::$body .= '>'; + } + } + + /** + * Close the given tag. Will check if it maches the tag last opened + */ + public static function closeTag($tag) + { + if (empty(self::$tags)) + Util::traceError('Tried to close tag ' . $tag . ' when no open tags exist.'); + $last = array_pop(self::$tags); + if ($last !== $tag) + Util::traceError('Tried to close tag ' . $tag . ' when last opened tag was ' . $last); + self::$body .= '</' . $tag . '>'; + } + + /** + * Private helper: Load the given template and return it + */ + private static function getTemplate($template) + { + if (isset(self::$templateCache[$template])) { + return self::$templateCache[$template]; + } + // Load from disk + $data = @file_get_contents('templates/' . $template . '.html'); + if ($data === false) + $data = '<b>Non-existent template ' . $template . ' requested!</b>'; + self::$templateCache[$template] = & $data; + return $data; + } + +} diff --git a/inc/request.inc.php b/inc/request.inc.php new file mode 100644 index 0000000..bb212df --- /dev/null +++ b/inc/request.inc.php @@ -0,0 +1,45 @@ +<?php + +/** + * Wrapper for getting fields from the request (GET, POST, ...) + */ +class Request +{ + + /** + * + * @param string $key Key of field to get from $_GET + * @param string $default Value to return if $_GET does not contain $key + * @return mixed Field from $_GET, or $default if not set + */ + public static function get($key, $default = false) + { + if (!isset($_GET[$key])) return $default; + return $_GET[$key]; + } + + /** + * + * @param string $key Key of field to get from $_POST + * @param string $default Value to return if $_POST does not contain $key + * @return mixed Field from $_POST, or $default if not set + */ + public static function post($key, $default = false) + { + if (!isset($_POST[$key])) return $default; + return $_POST[$key]; + } + + /** + * + * @param string $key Key of field to get from $_REQUEST + * @param string $default Value to return if $_REQUEST does not contain $key + * @return mixed Field from $_REQUEST, or $default if not set + */ + public static function any($key, $default = false) + { + if (!isset($_REQUEST[$key])) return $default; + return $_REQUEST[$key]; + } + +} diff --git a/inc/rpc.inc.php b/inc/rpc.inc.php new file mode 100644 index 0000000..83029e4 --- /dev/null +++ b/inc/rpc.inc.php @@ -0,0 +1,59 @@ +<?php + +/** + * Interface to the external rpc. + */ +class RPC +{ + + /** + * UDP socket used for communication with the rpc + * @var resource + */ + private static $sock = false; + + private static function init() + { + if (self::$sock !== false) + return; + self::$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + socket_set_option(self::$sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => 0, 'usec' => 300000)); + socket_set_option(self::$sock, SOL_SOCKET, SO_SNDTIMEO, array('sec' => 1, 'usec' => 0)); + socket_connect(self::$sock, '127.0.0.1', 1333); + } + + public static function submit($data) + { + self::init(); + if (empty($data)) { + $data = '{}'; + } else { + $data = json_encode($data); + } + $sent = socket_send(self::$sock, $data, strlen($data), 0); + if ($sent != strlen($data)) { + return 'RPC send error'; + } + $reply = self::readReply(); + if ($reply === false) { + return 'RPC receive error'; + } + return $reply; + } + + /** + * Read reply from socket. + * + * @return mixed read reply as astring, or error message + */ + private static function readReply() + { + for ($i = 0; $i < 3; ++$i) { + $bytes = socket_recvfrom(self::$sock, $buf, 90000, 0, $bla1, $bla2); + if ($bytes !== false) + return $buf; + } + return false; + } + +} diff --git a/inc/session.inc.php b/inc/session.inc.php new file mode 100644 index 0000000..b9adfcb --- /dev/null +++ b/inc/session.inc.php @@ -0,0 +1,99 @@ +<?php + + +class Session +{ + private static $sid = false; + private static $uid = false; + private static $data = false; + + private static function generateSessionId() + { + if (self::$sid !== false) Util::traceError('Error: Asked to generate session id when already set.'); + self::$sid = sha1( + mt_rand(0, 65535) + . $_SERVER['REMOTE_ADDR'] + . mt_rand(0, 65535) + . $_SERVER['REMOTE_PORT'] + . mt_rand(0, 65535) + . $_SERVER['HTTP_USER_AGENT'] + . mt_rand(0, 65535) + . microtime(true) + . mt_rand(0, 65535) + ); + } + + public static function create() + { + self::generateSessionId(); + self::$uid = 0; + self::$data = array(); + } + + public static function load() + { + // Try to load session id from cookie + if (!self::loadSessionId()) return false; + // Succeded, now try to load session data. If successful, job is done + if (self::readSessionData()) return true; + // Loading session data failed + self::delete(); + } + + public static function getUid() + { + return self::$uid; + } + + public static function setUid($value) + { + if (self::$uid === false) + Util::traceError('Tried to set session data with no active session'); + if (!is_numeric($value) || $value < 1) + Util::traceError('Invalid user id: ' . $value); + self::$uid = $value; + } + + public static function get($key) + { + if (isset(self::$data[$key])) + return self::$data[$key]; + return false; + } + + private static function loadSessionId() + { + if (self::$sid !== false) + die('Error: Asked to load session id when already set.'); + if (empty($_COOKIE['sid'])) + return false; + $id = preg_replace('/[^a-zA-Z0-9]/', '', $_COOKIE['sid']); + if (empty($id)) + return false; + self::$sid = $id; + return true; + } + + public static function delete() + { + if (self::$sid === false) return; + Database::exec('DELETE FROM websession WHERE sid = :sid', array('sid' => self::$sid)); + @setcookie('sid', '', time() - 8640000, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true); + self::$sid = false; + self::$uid = false; + } + + public static function save() + { + if (self::$sid === false || self::$uid === false || self::$uid === 0) + return; + $ret = Database::exec('INSERT INTO websession (sid, userid, dateline) ' + . ' VALUES (:sid, :uid, UNIX_TIMESTAMP()) ' + . ' ON DUPLICATE KEY UPDATE userid = VALUES(userid), dateline = VALUES(dateline)', + array('sid' => self::$sid, 'uid' => self::$uid)); + if (!$ret) Util::traceError('Storing session data in dahdähbank failed.'); + $ret = @setcookie('sid', self::$sid, time() + CONFIG_SESSION_TIMEOUT, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true); + if (!$ret) Util::traceError('Error: Could not set Cookie for Client (headers already sent)'); + } +} + diff --git a/inc/user.inc.php b/inc/user.inc.php new file mode 100644 index 0000000..30630d4 --- /dev/null +++ b/inc/user.inc.php @@ -0,0 +1,176 @@ +<?php + +class User +{ + + private static $user = false; + private static $organization = NULL; + private static $isShib = false; + private static $isInDb = false; + + public static function isLoggedIn() + { + return self::$user !== false; + } + + public static function isShibbolethAuth() + { + return self::$isShib; + } + + public static function isInDatabase() + { + return self::$isInDb; + } + + public static function isLocalOnly() + { + return self::$user !== false && self::$isShib === false; + } + + public static function getName() + { + if (!self::isLoggedIn()) + return false; + return self::$user['firstname'] . ' ' . self::$user['lastname']; + } + + public static function getLastName() + { + if (!self::isLoggedIn()) + return false; + return self::$user['lastname']; + } + + public static function hasFullName() + { + return self::$user !== false && !empty(self::$user['firstname']) && !empty(self::$user['lastname']); + } + + public static function isTutor() + { + return isset(self::$user['role']) && self::$user['role'] === 'tutor'; + } + + public static function getOrganizationId() + { + $org = self::getOrganization(); + if (!isset($org['organizationid'])) + return false; + return $org['organizationid']; + } + + public static function getRemoteOrganizationId() + { + if (empty(self::$user['organization'])) + return false; + return self::$user['organization']; + } + + public static function getOrganization() + { + if (!self::isLoggedIn()) + return false; + if (is_null(self::$organization)) { + self::$organization = Database::queryFirst('SELECT organizationid, name FROM satellite_suffix ' + . ' INNER JOIN satellite USING (organizationid) ' + . ' WHERE suffix = :org LIMIT 1', + array('org' => self::$user['organization'])); + } + return self::$organization; + } + + public static function load() + { + if (self::isLoggedIn()) + return true; + Session::load(); + if (empty($_SERVER['persistent-id'])) { + if (Session::getUid() === false) + return false; + // Try user from local DB + self::$user = Database::queryFirst('SELECT userid, shibid, login, firstname, lastname, email FROM user WHERE userid = :uid LIMIT 1', array('uid' => Session::getUid())); + return self::$user !== false; + } + // Try bwIDM etc. + self::$isShib = true; + if (!isset($_SERVER['sn'])) $_SERVER['sn'] = ''; + if (!isset($_SERVER['givenName'])) $_SERVER['givenName'] = ''; + if (!isset($_SERVER['mail'])) $_SERVER['mail'] = ''; + $shibId = md5($_SERVER['persistent-id']); + self::$user = array( + 'userid' => 0, + 'shibid' => $shibId, + 'login' => NULL, + 'firstname' => $_SERVER['givenName'], + 'lastname' => $_SERVER['sn'], + 'email' => $_SERVER['mail'], + ); + // Figure out whether the user should be considered a tutor + if (isset($_SERVER['affiliation']) && preg_match('/(^|;)employee@/', $_SERVER['affiliation'])) + self::$user['role'] = 'tutor'; + elseif (isset($_SERVER['entitlement']) && strpos(";{$_SERVER['entitlement']};", ';http://bwidm.de/entitlement/bwLehrpool;') !== false) + self::$user['role'] = 'tutor'; + // Try to figure out organization + if (isset($_SERVER['affiliation']) && preg_match('/@([a-zA-Z\-\._]+)(;|$)/', $_SERVER['affiliation'], $out)) + self::$user['organization'] = $out[1]; + // Get matching db entry if any + $user = Database::queryFirst('SELECT userid, login, firstname, lastname, email, fixedname FROM user WHERE shibid = :shibid LIMIT 1', array('shibid' => $shibId)); + if ($user === false) { + // No match in database, user is not signed up + return true; + } + // Already signed up, see if we can fetch missing fields from DB + self::$user['login'] = $user['login']; + self::$isInDb = true; + foreach (array('firstname', 'lastname', 'email') as $key) { + if (empty(self::$user[$key])) + self::$user[$key] = $user[$key]; + } + return true; + } + + public static function deploy($anonymous) + { + if (empty(self::$user['shibid'])) + Util::traceError('NO SHIBID'); + if ($anonymous) { + Database::exec("INSERT INTO user (shibid, login, organizationid, firstname, lastname, email) " + . " VALUES (:shibid, :shibid, :org, '', '', '')", array( + 'shibid' => self::$user['shibid'], + 'org' => self::getOrganizationId() + )); + } else { + Database::exec("INSERT INTO user (shibid, login, organizationid, firstname, lastname, email) " + . " VALUES (:shibid, :shibid, :org, :firstname, :lastname, :email)", array( + 'shibid' => self::$user['shibid'], + 'firstname' => self::$user['firstname'], + 'lastname' => self::$user['lastname'], + 'email' => self::$user['email'], + 'org' => self::getOrganizationId() + )); + } + } + + public static function login($user, $pass) + { + $ret = Database::queryFirst('SELECT userid, password FROM user WHERE login = :user LIMIT 1', array(':user' => $user)); + if ($ret === false) + return false; + if (!Crypto::verify($pass, $ret['passwd'])) + return false; + Session::create(); + Session::setUid($ret['userid']); + Session::set('token', md5(rand() . time() . mt_rand() . $_SERVER['REMOTE_ADDR'] . rand() . $_SERVER['REMOTE_PORT'] . rand() . $_SERVER['HTTP_USER_AGENT'] . microtime(true))); + Session::save(); + return true; + } + + public static function logout() + { + Session::delete(); + Header('Location: ?do=Main&fromlogout'); + exit(0); + } + +} diff --git a/inc/util.inc.php b/inc/util.inc.php new file mode 100644 index 0000000..4378a08 --- /dev/null +++ b/inc/util.inc.php @@ -0,0 +1,289 @@ +<?php + +class Util +{ + private static $redirectParams = array(); + + /** + * Displays an error message and stops script execution. + * If CONFIG_DEBUG is true, it will also dump a stack trace + * and all globally defined variables. + * (As this might reveal sensistive data you should never enable it in production) + */ + public static function traceError($message) + { + Header('HTTP/1.1 500 Internal Server Error'); + Header('Content-Type: text/plain; charset=utf-8'); + echo "--------------------\nFlagrant system error:\n$message\n--------------------\n\n"; + if (defined('CONFIG_DEBUG') && CONFIG_DEBUG) { + debug_print_backtrace(); + echo "\n\nSome variables for your entertainment:\n"; + print_r($GLOBALS); + } else { + echo <<<SADFACE + +________________________¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶________ +____________________¶¶¶___________________¶¶¶¶_____ +________________¶¶¶_________________________¶¶¶¶___ +______________¶¶______________________________¶¶¶__ +___________¶¶¶_________________________________¶¶¶_ +_________¶¶_____________________________________¶¶¶ +________¶¶_________¶¶¶¶¶___________¶¶¶¶¶_________¶¶ +______¶¶__________¶¶¶¶¶¶__________¶¶¶¶¶¶_________¶¶ +_____¶¶___________¶¶¶¶____________¶¶¶¶___________¶¶ +____¶¶___________________________________________¶¶ +___¶¶___________________________________________¶¶_ +__¶¶____________________¶¶¶¶____________________¶¶_ +_¶¶_______________¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶______________¶¶__ +_¶¶____________¶¶¶¶___________¶¶¶¶¶___________¶¶___ +¶¶¶_________¶¶¶__________________¶¶__________¶¶____ +¶¶_________¶______________________¶¶________¶¶_____ +¶¶¶______¶________________________¶¶_______¶¶______ +¶¶¶_____¶_________________________¶¶_____¶¶________ +_¶¶¶___________________________________¶¶__________ +__¶¶¶________________________________¶¶____________ +___¶¶¶____________________________¶¶_______________ +____¶¶¶¶______________________¶¶¶__________________ +_______¶¶¶¶¶_____________¶¶¶¶¶_____________________ +SADFACE; + } + exit(0); + } + + /** + * Redirects the user via a '302 Moved' header. + * An active session will be saved, any messages that haven't + * been displayed yet will be appended to the redirect. + * @param string $location Location to redirect to. "false" to redirect to same URL (useful after POSTs) + */ + public static function redirect($location = false) + { + if ($location === false) { + $location = preg_replace('/(&|\?)message\[\]\=[^&]*/', '\1', $_SERVER['REQUEST_URI']); + } + Session::save(); + $messages = Message::toRequest(); + if (!empty($messages)) { + if (strpos($location, '?') === false) { + $location .= '?' . $messages; + } else { + $location .= '&' . $messages; + } + } + if (!empty(self::$redirectParams)) { + if (strpos($location, '?') === false) { + $location .= '?' . implode('&', self::$redirectParams); + } else { + $location .= '&' . implode('&', self::$redirectParams); + } + } + Header('Location: ' . $location); + exit(0); + } + + public static function addRedirectParam($key, $value) + { + self::$redirectParams[] = $key .= '=' . urlencode($value); + } + + /** + * Verify the user's token that protects agains CSRF. + * If the user is logged in and there is no token variable set in + * the request, or the submitted token does not match the user's + * token, this function will return false and display an error. + * If the token matches, or the user is not logged in, it will return true. + */ + public static function verifyToken() + { + if (Session::get('token') === false) + return true; + if (isset($_REQUEST['token']) && Session::get('token') === $_REQUEST['token']) + return true; + Message::addError('token'); + return false; + } + + /** + * Simple markup "rendering": + * *word* is bold + * /word/ is italics + * _word_ is underlined + * \n is line break + */ + public static function markup($string) + { + $string = htmlspecialchars($string); + $string = preg_replace('#(^|[\n \-_/\.])\*(.+?)\*($|[ \-_/\.\!\?,:])#is', '$1<b>$2</b>$3', $string); + $string = preg_replace('#(^|[\n \-\*/\.])_(.+?)_($|[ \-\*/\.\!\?,:])#is', '$1<u>$2</u>$3', $string); + $string = preg_replace('#(^|[\n \-_\*\.])/(.+?)/($|[ \-_\*\.\!\?,:])#is', '$1<i>$2</i>$3', $string); + return nl2br($string); + } + + /** + * Convert given number to human readable file size string. + * Will append Bytes, KiB, etc. depending on magnitude of number. + * + * @param type $bytes numeric value of the filesize to make readable + * @param type $decimals number of decimals to show, -1 for automatic + * @return type human readable string representing the given filesize + */ + public static function readableFileSize($bytes, $decimals = -1) + { + static $sz = array('Byte', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'); + $factor = floor((strlen($bytes) - 1) / 3); + if ($factor == 0) { + $decimals = 0; + } elseif ($decimals === -1) { + $decimals = 2 - floor((strlen($bytes) - 1) % 3); + } + return sprintf("%.{$decimals}f ", $bytes / pow(1024, $factor)) . $sz[$factor]; + } + + public static function sanitizeFilename($name) + { + return preg_replace('/[^a-zA-Z0-9_\-]+/', '_', $name); + } + + public static function safePath($path, $prefix = '') + { + if (empty($path)) + return false; + $path = trim($path); + if ($path{0} == '/' || preg_match('/[\x00-\x19\?\*]/', $path)) + return false; + if (strpos($path, '..') !== false) + return false; + if (substr($path, 0, 2) !== './') + $path = "./$path"; + if (empty($prefix)) + return $path; + if (substr($prefix, 0, 2) !== './') + $prefix = "./$prefix"; + if (substr($path, 0, strlen($prefix)) !== $prefix) + return false; + return $path; + } + + /** + * Create human readable error description from a $_FILES[<..>]['error'] code + * + * @param int $code the code to turn into an error description + * @return string the error description + */ + public static function uploadErrorString($code) + { + switch ($code) { + case UPLOAD_ERR_INI_SIZE: + $message = "The uploaded file exceeds the upload_max_filesize directive in php.ini"; + break; + case UPLOAD_ERR_FORM_SIZE: + $message = "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form"; + break; + case UPLOAD_ERR_PARTIAL: + $message = "The uploaded file was only partially uploaded"; + break; + case UPLOAD_ERR_NO_FILE: + $message = "No file was uploaded"; + break; + case UPLOAD_ERR_NO_TMP_DIR: + $message = "Missing a temporary folder"; + break; + case UPLOAD_ERR_CANT_WRITE: + $message = "Failed to write file to disk"; + break; + case UPLOAD_ERR_EXTENSION: + $message = "File upload stopped by extension"; + break; + + default: + $message = "Unknown upload error"; + break; + } + return $message; + } + + /** + * Is given string a public ipv4 address? + * + * @param string $ip_addr input to check + * @return boolean true iff $ip_addr is a valid public ipv4 address + */ + public static function isPublicIpv4($ip_addr) + { + if (!preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $ip_addr)) + return false; + + $parts = explode(".", $ip_addr); + foreach ($parts as $part) { + if (!is_numeric($part) || $part > 255 || $part < 0) + return false; + } + + if ($parts[0] == 0 || $parts[0] == 10 || $parts[0] == 127 || ($parts[0] > 223 && $parts[0] < 240)) + return false; + if (($parts[0] == 192 && $parts[1] == 168) || ($parts[0] == 169 && $parts[1] == 254)) + return false; + if ($parts[0] == 172 && $parts[1] > 15 && $parts[1] < 32) + return false; + + return true; + } + + /** + * Check whether $arrax contains all keys given in $keyList + * @param array $array An array + * @param array $keyList A list of strings which must all be valid keys in $array + * @return boolean + */ + public static function hasAllKeys($array, $keyList) + { + if (!is_array($array)) + return false; + foreach ($keyList as $key) { + if (!isset($array[$key])) + return false; + } + return true; + } + + /** + * Send a file to user for download. + * + * @param type $file path of local file + * @param type $name name of file to send to user agent + * @param type $delete delete the file when done? + * @return boolean false: file could not be opened. + * true: error while reading the file + * - on success, the function does not return + */ + public static function sendFile($file, $name, $delete = false) + { + while ((@ob_get_level()) > 0) + @ob_end_clean(); + $fh = @fopen($file, 'rb'); + if ($fh === false) { + Message::addError('error-read', $file); + return false; + } + Header('Content-Type: application/octet-stream', true); + Header('Content-Disposition: attachment; filename=' . str_replace(array(' ', '=', ',', '/', '\\', ':', '?'), '_', iconv('UTF-8', 'ASCII//TRANSLIT', $name))); + Header('Content-Length: ' . @filesize($file)); + while (!feof($fh)) { + $data = fread($fh, 16000); + if ($data === false) { + echo "\r\n\nDOWNLOAD INTERRUPTED!\n"; + if ($delete) + @unlink($file); + return true; + } + echo $data; + @ob_flush(); + @flush(); + } + @fclose($fh); + if ($delete) + @unlink($file); + exit(0); + } + +} |