diff options
Diffstat (limited to 'inc')
-rw-r--r-- | inc/database.inc.php | 116 | ||||
-rw-r--r-- | inc/dictionary.inc.php | 4 | ||||
-rw-r--r-- | inc/download.inc.php | 1 | ||||
-rw-r--r-- | inc/event.inc.php | 30 | ||||
-rw-r--r-- | inc/module.inc.php | 36 | ||||
-rw-r--r-- | inc/property.inc.php | 11 | ||||
-rw-r--r-- | inc/render.inc.php | 52 | ||||
-rw-r--r-- | inc/session.inc.php | 9 | ||||
-rw-r--r-- | inc/util.inc.php | 30 |
9 files changed, 263 insertions, 26 deletions
diff --git a/inc/database.inc.php b/inc/database.inc.php index ff98f5ee..150f828a 100644 --- a/inc/database.inc.php +++ b/inc/database.inc.php @@ -45,7 +45,7 @@ class Database * * @return array|boolean Associative array representing row, or false if no row matches the query */ - public static function queryFirst($query, $args = array(), $ignoreError = false) + public static function queryFirst($query, $args = array(), $ignoreError = null) { $res = self::simpleQuery($query, $args, $ignoreError); if ($res === false) @@ -53,6 +53,20 @@ class Database return $res->fetch(PDO::FETCH_ASSOC); } + /** + * If you need all rows for a query as plain array you can use this. + * Don't use this if you want to do further processing of the data, to save some + * memory. + * + * @return array|bool List of associative arrays representing rows, or false on error + */ + public static function queryAll($query, $args = array(), $ignoreError = null) + { + $res = self::simpleQuery($query, $args, $ignoreError); + if ($res === false) + return false; + return $res->fetchAll(PDO::FETCH_ASSOC); + } /** * Execute the given query and return the number of rows affected. @@ -63,7 +77,7 @@ class Database * @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) + public static function exec($query, $args = array(), $ignoreError = null) { $res = self::simpleQuery($query, $args, $ignoreError); if ($res === false) @@ -95,11 +109,16 @@ class Database * 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 + * @return \PDOStatement|false The query result object */ - public static function simpleQuery($query, $args = array(), $ignoreError = false) + public static function simpleQuery($query, $args = array(), $ignoreError = null) { self::init(); + if (CONFIG_DEBUG && preg_match('/^\s*SELECT/is', $query)) { + self::explainQuery($query, $args); + } + // Support passing nested arrays for IN statements, automagically refactor + self::handleArrayArgument($query, $args); try { if (!isset(self::$statements[$query])) { self::$statements[$query] = self::$dbh->prepare($query); @@ -108,20 +127,105 @@ class Database } if (self::$statements[$query]->execute($args) === false) { self::$lastError = implode("\n", self::$statements[$query]->errorInfo()); - if ($ignoreError || self::$returnErrors) + if ($ignoreError === true || ($ignoreError === null && self::$returnErrors)) return false; Util::traceError("Database Error: \n" . self::$lastError); } return self::$statements[$query]; } catch (Exception $e) { self::$lastError = '(' . $e->getCode() . ') ' . $e->getMessage(); - if ($ignoreError || self::$returnErrors) + if ($ignoreError === true || ($ignoreError === null && self::$returnErrors)) return false; Util::traceError("Database Error: \n" . self::$lastError); } return false; } + private static function explainQuery($query, $args) + { + $res = self::simpleQuery('EXPLAIN ' . $query, $args, true); + if ($res === false) + return; + $rows = $res->fetchAll(PDO::FETCH_ASSOC); + if (empty($rows)) + return; + $log = false; + $lens = array(); + foreach (array_keys($rows[0]) as $key) { + $lens[$key] = strlen($key); + } + foreach ($rows as $row) { + if (!$log && preg_match('/filesort|temporary/i', $row['Extra'])) { + $log = true; + } + foreach ($row as $key => $col) { + $l = strlen($col); + if ($l > $lens[$key]) { + $lens[$key] = $l; + } + } + } + if (!$log) + return; + error_log('Possible slow query: ' . $query); + $border = $head = ''; + foreach ($lens as $key => $len) { + $border .= '+' . str_repeat('-', $len + 2); + $head .= '| ' . str_pad($key, $len) . ' '; + } + $border .= '+'; + $head .= '|'; + error_log("\n" . $border . "\n" . $head . "\n" . $border); + foreach ($rows as $row) { + $line = ''; + foreach ($lens as $key => $len) { + $line .= '| '. str_pad($row[$key], $len) . ' '; + } + error_log($line . "|"); + } + error_log($border); + } + + /** + * Convert nested array argument to multiple arguments. + * If you have: + * $query = 'SELECT * FROM tbl WHERE bcol = :bool AND col IN (:list) + * $args = ( 'bool' => 1, 'list' => ('foo', 'bar') ) + * it results in: + * $query = '...WHERE bcol = :bool AND col IN (:list_0, :list_1) + * $args = ( 'bool' => 1, 'list_0' => 'foo', 'list_1' => 'bar' ) + * + * @param string $query sql query string + * @param array $args query arguments + */ + private static function handleArrayArgument(&$query, &$args) + { + foreach (array_keys($args) as $key) { + if (is_numeric($key) || $key === '?') + continue; + if (is_array($args[$key])) { + if (empty($args[$key])) { + // Empty list - what to do? We try to generate a query string that will not yield any result + $args[$key] = 'asdf' . mt_rand(0,PHP_INT_MAX) . mt_rand(0,PHP_INT_MAX) + . mt_rand(0,PHP_INT_MAX) . '@' . microtime(true); + continue; + } + $newkey = $key; + if ($newkey{0} !== ':') { + $newkey = ":$newkey"; + } + $new = array(); + foreach ($args[$key] as $subIndex => $sub) { + $new[] = $newkey . '_' . $subIndex; + $args[$newkey . '_' . $subIndex] = $sub; + } + unset($args[$key]); + $new = implode(',', $new); + $query = preg_replace('/' . $newkey . '\b/', $new, $query); + } + } + } + /** * Simply calls PDO::prepare and returns the PDOStatement. * You must call PDOStatement::execute manually on it. diff --git a/inc/dictionary.inc.php b/inc/dictionary.inc.php index 634b1c3c..ca2811ff 100644 --- a/inc/dictionary.inc.php +++ b/inc/dictionary.inc.php @@ -81,11 +81,11 @@ class Dictionary return $strings[$tag]; } - public static function translateFile($path, $tag) + public static function translateFile($path, $tag, $returnTagOnMissing = false) { if (!class_exists('Page') || Page::getModule() === false) return false; // We have no page - return false for now, as we're most likely running in api or install mode - return self::translateFileModule(Page::getModule()->getIdentifier(), $path, $tag); + return self::translateFileModule(Page::getModule()->getIdentifier(), $path, $tag, $returnTagOnMissing); } public static function translate($tag, $returnTagOnMissing = false) diff --git a/inc/download.inc.php b/inc/download.inc.php index a2054f78..b3496e85 100644 --- a/inc/download.inc.php +++ b/inc/download.inc.php @@ -21,6 +21,7 @@ class Download curl_setopt($ch, CURLOPT_MAXREDIRS, 6); $tmpfile = tempnam('/tmp/', 'bwlp-'); $head = fopen($tmpfile, 'w+b'); + unlink($tmpfile); if ($head === false) Util::traceError("Could not open temporary head file $tmpfile for writing."); curl_setopt($ch, CURLOPT_WRITEHEADER, $head); diff --git a/inc/event.inc.php b/inc/event.inc.php index 7a7c48b0..a16be97b 100644 --- a/inc/event.inc.php +++ b/inc/event.inc.php @@ -20,12 +20,17 @@ class Event EventLog::info('System boot...'); $everythingFine = true; + // Delete job entries that might have been running when system rebooted + Property::clearList('cron.key.status'); + Property::clearList('cron.key.blocked'); + // Tasks: fire away + $mountStatus = false; $mountId = Trigger::mount(); $autoIp = Trigger::autoUpdateServerIp(); $ldadpId = Trigger::ldadp(); $ipxeId = Trigger::ipxe(); - + Taskmanager::submit('DozmodLauncher', array( 'operation' => 'start' )); @@ -36,11 +41,8 @@ class Event EventLog::info('No VM store type defined.'); $everythingFine = false; } else { - $res = Taskmanager::waitComplete($mountId, 5000); - if (Taskmanager::isFailed($res)) { - EventLog::failure('Mounting VM store failed', $res['data']['messages']); - $everythingFine = false; - } + $mountStatus = Taskmanager::waitComplete($mountId, 5000); + } // LDAP AD Proxy if ($ldadpId === false) { @@ -70,6 +72,22 @@ class Event } } + if ($mountStatus !== false && !Taskmanager::isFinished($mountStatus)) { + $mountStatus = Taskmanager::waitComplete($mountStatus, 5000); + } + if (Taskmanager::isFailed($mountStatus)) { + // One more time, network could've been down before + sleep(10); + $mountId = Trigger::mount(); + $mountStatus = Taskmanager::waitComplete($mountId, 10000); + } + if ($mountId !== false && Taskmanager::isFailed($mountStatus)) { + EventLog::failure('Mounting VM store failed', $mountStatus['data']['messages']); + $everythingFine = false; + } elseif ($mountId !== false && !Taskmanager::isFinished($mountStatus)) { + // TODO: Still running - create callback + } + // Just so we know booting is done (and we don't expect any more errors from booting up) if ($everythingFine) { EventLog::info('Bootup finished without errors.'); diff --git a/inc/module.inc.php b/inc/module.inc.php index 597cfb57..7211c68c 100644 --- a/inc/module.inc.php +++ b/inc/module.inc.php @@ -142,13 +142,23 @@ class Module private $activated = false; private $dependencies = array(); private $name; + /** + * @var array assoc list of 'filename.css' => true|false (true = always load, false = only if module is main module) + */ + private $css = array(); + /** + * @var array assoc list of 'filename.js' => true|false (true = always load, false = only if module is main module) + */ + private $scripts = array(); private function __construct($name) { $file = 'modules/' . $name . '/config.json'; $json = @json_decode(@file_get_contents($file), true); - if (isset($json['dependencies']) && is_array($json['dependencies'])) { - $this->dependencies = $json['dependencies']; + foreach (['dependencies', 'css', 'scripts'] as $key) { + if (isset($json[$key]) && is_array($json[$key])) { + $this->$key = $json[$key]; + } } if (isset($json['category']) && is_string($json['category'])) { $this->category = $json['category']; @@ -251,4 +261,26 @@ class Module return 'modules/' . $this->name; } + public function getScripts($externalOnly) + { + if (!$externalOnly) { + if (!isset($this->scripts['clientscript.js']) && file_exists($this->getDir() . '/clientscript.js')) { + $this->scripts['clientscript.js'] = false; + } + return array_keys($this->scripts); + } + return array_keys(array_filter($this->scripts)); + } + + public function getCss($externalOnly) + { + if (!$externalOnly) { + if (!isset($this->css['style.css']) && file_exists($this->getDir() . '/style.css')) { + $this->css['style.css'] = false; + } + return array_keys($this->css); + } + return array_keys(array_filter($this->css)); + } + } diff --git a/inc/property.inc.php b/inc/property.inc.php index b33e1bff..0b4ea7b3 100644 --- a/inc/property.inc.php +++ b/inc/property.inc.php @@ -105,6 +105,17 @@ class Property )); } + /** + * Delete entire list with given key. + * + * @param string $key Key of list + * @return int number of items removed + */ + public static function clearList($key) + { + return Database::exec("DELETE FROM property_list WHERE name = :key", compact('key')); + } + /* * Legacy getters/setters */ diff --git a/inc/render.inc.php b/inc/render.inc.php index 5515c659..53e2f314 100644 --- a/inc/render.inc.php +++ b/inc/render.inc.php @@ -40,19 +40,39 @@ class Render self::$mustache = new Mustache_Engine($options); } + private static function cssEsc($str) + { + return str_replace(array('"', '&', '<', '>'), array('\\000022', '\\000026', '\\00003c', '\\00003e'), $str); + } + /** * Output the buffered, generated page */ public static function output() { Header('Content-Type: text/html; charset=utf-8'); + /* @var $modules Module[] */ $modules = array_reverse(Module::getActivated()); + $pageModule = Page::getModule(); + $title = Property::get('page-title-prefix', ''); + $bgcolor = Property::get('logo-background', ''); + if (!empty($bgcolor) || !empty($title)) { + self::$header .= '<style type="text/css">' . "\n"; + if (!empty($bgcolor)) { + $fgcolor = self::readableColor($bgcolor); + self::$header .= ".navbar-header{background-color:$bgcolor}a.navbar-brand{color:$fgcolor!important}"; + } + if (!empty($title)) { + self::$header .= '#navbar-sub:after{content:"' . self::cssEsc($title) . '";margin:0}'; + } + self::$header .= "\n</style>"; + } ob_start('ob_gzhandler'); echo '<!DOCTYPE html> <html> <head> - <title>', self::$title, RENDER_DEFAULT_TITLE, '</title> + <title>', $title, self::$title, RENDER_DEFAULT_TITLE, '</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> @@ -61,9 +81,9 @@ class Render '; // Include any module specific styles foreach ($modules as $module) { - $file = $module->getDir() . '/style.css'; - if (file_exists($file)) { - echo '<link href="', $file, '" rel="stylesheet" media="screen">'; + $files = $module->getCss($module != $pageModule); + foreach ($files as $file) { + echo '<link href="', $module->getDir(), '/', $file, '" rel="stylesheet" media="screen">'; } } echo ' @@ -91,9 +111,9 @@ class Render <script src="script/collapse.js"></script> '; foreach ($modules as $module) { - $file = $module->getDir() . '/clientscript.js'; - if (file_exists($file)) { - echo '<script src="', $file, '"></script>'; + $files = $module->getScripts($module != $pageModule); + foreach ($files as $file) { + echo '<script src="', $module->getDir(), '/', $file, '"></script>'; } } echo @@ -285,4 +305,22 @@ class Render self::$dashboard = $params; } + public static function readableColor($hex) { + if (strlen($hex) <= 4) { + $cnt = 1; + } else { + $cnt = 2; + } + if (preg_match('/^#?([a-f0-9]{'.$cnt.'})([a-f0-9]{'.$cnt.'})([a-f0-9]{'.$cnt.'})$/i', $hex, $out) != 1) + return '#000'; + $chans = array(); + $f = ($cnt === 1 ? 17 : 1); + for ($i = 1; $i <= 3; ++$i) { + $out[$i] = (hexdec($out[$i]) * $f); + $chans[] = $out[$i] ^ 0x80; + } + $b = (255 - (0.299 * $out[1] + 0.587 * $out[2] + 0.114 * $out[3])) * 2; + return sprintf("#%02x%02x%02x", ($chans[0] + $b) / 3, ($chans[1] + $b) / 3, ($chans[2] + $b) / 3); + } + } diff --git a/inc/session.inc.php b/inc/session.inc.php index 26effa3f..24bf6ac0 100644 --- a/inc/session.inc.php +++ b/inc/session.inc.php @@ -74,10 +74,15 @@ class Session { if (self::$sid === false) return; @unlink(self::getSessionFile()); - @setcookie('sid', '', time() - 8640000, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true); + self::deleteCookie(); self::$sid = false; self::$data = false; } + + public static function deleteCookie() + { + setcookie('sid', '', time() - 8640000, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true); + } private static function getSessionFile() { @@ -104,7 +109,7 @@ class Session $sessionfile = self::getSessionFile(); $ret = @file_put_contents($sessionfile, @serialize(self::$data)); if (!$ret) Util::traceError('Storing session data in ' . $sessionfile . ' failed.'); - $ret = @setcookie('sid', self::$sid, time() + CONFIG_SESSION_TIMEOUT, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true); + $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/util.inc.php b/inc/util.inc.php index 5d1a4563..9bcfdf13 100644 --- a/inc/util.inc.php +++ b/inc/util.inc.php @@ -12,7 +12,7 @@ class Util */ public static function traceError($message) { - if ((defined('API') && API) || (defined('AJAX') && AJAX)) { + if ((defined('API') && API) || (defined('AJAX') && AJAX) || php_sapi_name() === 'cli') { error_log('API ERROR: ' . $message); error_log(self::formatBacktracePlain(debug_backtrace())); } @@ -434,4 +434,32 @@ SADFACE; return $bytes; } + /** + * @return string a random UUID, v4. + */ + public static function randomUuid() + { + $b = unpack('h8a/h4b/h12c', self::randomBytes(12)); + return sprintf('%08s-%04s-%04x-%04x-%012s', + + // 32 bits for "time_low" + $b['a'], + + // 16 bits for "time_mid" + $b['b'], + + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 4 + mt_rand(0, 0x0fff) | 0x4000, + + // 16 bits, 8 bits for "clk_seq_hi_res", + // 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + mt_rand(0, 0x3fff) | 0x8000, + + // 48 bits for "node" + $b['c'] + ); + } + } |