summaryrefslogtreecommitdiffstats
path: root/inc
diff options
context:
space:
mode:
Diffstat (limited to 'inc')
-rw-r--r--inc/crypto.inc.php6
-rw-r--r--inc/database.inc.php422
-rw-r--r--inc/image.inc.php7
-rw-r--r--inc/message.inc.php24
-rw-r--r--inc/render.inc.php21
-rw-r--r--inc/session.inc.php43
-rw-r--r--inc/shibauth.inc.php202
-rw-r--r--inc/user.inc.php109
-rw-r--r--inc/util.inc.php40
9 files changed, 717 insertions, 157 deletions
diff --git a/inc/crypto.inc.php b/inc/crypto.inc.php
index 56f5073..75a0a01 100644
--- a/inc/crypto.inc.php
+++ b/inc/crypto.inc.php
@@ -8,7 +8,7 @@ class Crypto
* which translates to ~130 bit salt
* and 5000 rounds of hashing with SHA-512.
*/
- public static function hash6($password)
+ public static function hash6(string $password): string
{
$salt = substr(str_replace('+', '.', base64_encode(pack('N4', mt_rand(), mt_rand(), mt_rand(), mt_rand()))), 0, 16);
$hash = crypt($password, '$6$' . $salt);
@@ -17,10 +17,10 @@ class Crypto
}
/**
- * Check if the given password matches the given cryp hash.
+ * Check if the given password matches the given crypt hash.
* Useful for checking a hashed password.
*/
- public static function verify($password, $hash)
+ public static function verify(string $password, string $hash): bool
{
return crypt($password, $hash) === $hash;
}
diff --git a/inc/database.inc.php b/inc/database.inc.php
index f76c9e7..6518631 100644
--- a/inc/database.inc.php
+++ b/inc/database.inc.php
@@ -8,28 +8,43 @@ class Database
{
/**
- *
* @var \PDO Database handle
*/
- private static $dbh = false;
- private static $statements = array();
-
-
- /**
+ private static ?PDO $dbh = null;
+
+ private static bool $returnErrors;
+ private static ?string $lastError = null;
+ private static array $explainList = array();
+ private static int $queryCount = 0;
+ private static float $queryTime = 0;
+
+ /**
* Connect to the DB if not already connected.
*/
- private static function init()
+ public static function init(bool $returnErrors = false): bool
{
- if (self::$dbh !== false)
- return;
+ if (self::$dbh !== null)
+ return true;
+ self::$returnErrors = $returnErrors;
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);
+ self::$dbh = new PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASS, [
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => true,
+ PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', // Somehow needed, even if charset=utf8mb4 is in DSN?
+ ]);
} catch (PDOException $e) {
+ if (self::$returnErrors)
+ return false;
Util::traceError('Connecting to the local database failed: ' . $e->getMessage());
}
+ if (CONFIG_DEBUG) {
+ Database::exec("SET SESSION sql_mode='STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION,ERROR_FOR_DIVISION_BY_ZERO'");
+ Database::exec("SET SESSION innodb_strict_mode=ON");
+ register_shutdown_function(function() {
+ self::examineLoggedQueries();
+ });
+ }
+ return true;
}
/**
@@ -37,25 +52,100 @@ 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(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetch();
+ }
+
+ /**
+ * 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(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll();
+ }
+
+ /**
+ * Fetch the first column of the query as a plain list-of-values array.
+ *
+ * @return array|bool List of values representing first column of query
+ */
+ public static function queryColumnArray(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_COLUMN, 0);
+ }
+
+ /**
+ * Fetch two columns as key => value list.
+ *
+ * @return array|bool Associative array, first column is key, second column is value
+ */
+ public static function queryKeyValueList(string $query, array $args = [], bool $ignoreError = null)
{
$res = self::simpleQuery($query, $args, $ignoreError);
if ($res === false)
return false;
- return $res->fetch(PDO::FETCH_ASSOC);
+ return $res->fetchAll(PDO::FETCH_KEY_PAIR);
}
+ /**
+ * Fetch and group by first column. First column is key, value is a list of rows with remaining columns.
+ * [
+ * col1 => [
+ * [col2, col3],
+ * [col2, col3],
+ * ],
+ * ...,
+ * ]
+ *
+ * @return array|bool Associative array, first column is key, remaining columns are array values
+ */
+ public static function queryGroupList(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_GROUP);
+ }
+
+ /**
+ * Fetch and use first column as key of returned array.
+ * This is like queryGroup list, but it is assumed that the first column is unique, so
+ * the remaining columns won't be wrapped in another array.
+ *
+ * @return array|bool Associative array, first column is key, remaining columns are array values
+ */
+ public static function queryIndexedList(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_UNIQUE);
+ }
/**
* 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
+ * @param ?bool $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(string $query, array $args = [], bool $ignoreError = null)
{
$res = self::simpleQuery($query, $args, $ignoreError);
if ($res === false)
@@ -64,41 +154,194 @@ class Database
}
/**
- * Get id (promary key) of last row inserted.
- *
+ * Get id (primary key) of last row inserted.
+ *
* @return int the id
*/
- public static function lastInsertId()
+ public static function lastInsertId(): int
{
return self::$dbh->lastInsertId();
}
/**
+ * @return ?string return last error returned by query
+ */
+ public static function lastError(): ?string
+ {
+ return self::$lastError;
+ }
+
+ /**
* 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
+ *
+ * @return \PDOStatement|false The query result object
*/
- public static function simpleQuery($query, $args = array(), $ignoreError = false)
+ public static function simpleQuery(string $query, array $args = [], bool $ignoreError = null)
{
self::init();
- try {
- if (!isset(self::$statements[$query])) {
- self::$statements[$query] = self::$dbh->prepare($query);
- } else {
- self::$statements[$query]->closeCursor();
+ if (CONFIG_DEBUG && !isset(self::$explainList[$query]) && preg_match('/^\s*SELECT/is', $query)) {
+ self::$explainList[$query] = [$args];
+ }
+ // Support passing nested arrays for IN statements, automagically refactor
+ $oquery = $query;
+ self::handleArrayArgument($query, $args);
+ // Now turn any bools into 0 or 1, since PDO unfortunately only does (string)<bool>, which
+ // results in an empty string for false
+ foreach ($args as &$arg) {
+ if ($arg === false) {
+ $arg = '0';
+ } elseif ($arg === true) {
+ $arg = '1';
}
- if (self::$statements[$query]->execute($args) === false) {
- if ($ignoreError)
+ }
+ try {
+ $stmt = self::$dbh->prepare($query);
+ $start = microtime(true);
+ if ($stmt->execute($args) === false) {
+ self::$lastError = implode("\n", $stmt->errorInfo());
+ if ($ignoreError === true || ($ignoreError === null && self::$returnErrors))
return false;
- Util::traceError("Database Error: \n" . implode("\n", self::$statements[$query]->errorInfo()));
+ Util::traceError("Database Error: \n" . self::$lastError);
}
- return self::$statements[$query];
+ if (CONFIG_DEBUG) {
+ $duration = microtime(true) - $start;
+ self::$queryTime += $duration;
+ $duration = round($duration, 3);
+ if (isset(self::$explainList[$oquery])) {
+ self::$explainList[$oquery][] = $duration;
+ } elseif ($duration > 0.1) {
+ error_log('SLOW ****** ' . $duration . "s *********\n" . $query);
+ }
+ self::$queryCount += 1;
+ }
+ return $stmt;
} catch (Exception $e) {
- if ($ignoreError)
- return false;
- Util::traceError("Database Error: \n" . $e->getMessage());
+ self::$lastError = '(' . $e->getCode() . ') ' . $e->getMessage();
+ if ($ignoreError === true || ($ignoreError === null && self::$returnErrors))
+ return false;
+ Util::traceError("Database Error: \n" . self::$lastError);
+ }
+ return false;
+ }
+
+ public static function examineLoggedQueries()
+ {
+ foreach (self::$explainList as $q => $a) {
+ self::explainQuery($q, $a);
+ }
+ }
+
+ private static function explainQuery(string $query, array $data)
+ {
+ $args = array_shift($data);
+ $slow = false;
+ $veryslow = false;
+ foreach ($data as &$ts) {
+ if ($ts > 0.004) {
+ $slow = true;
+ if ($ts > 0.015) {
+ $ts = "[$ts]";
+ $veryslow = true;
+ }
+ }
+ }
+ if (!$slow)
+ return;
+ unset($ts);
+ $res = self::simpleQuery('EXPLAIN ' . $query, $args, true);
+ if ($res === false)
+ return;
+ $rows = $res->fetchAll();
+ if (empty($rows))
+ return;
+ $log = $veryslow;
+ $lens = array();
+ foreach (array_keys($rows[0]) as $key) {
+ $lens[$key] = strlen($key);
+ }
+ foreach ($rows as $row) {
+ if (!$log && $row['rows'] > 20 && 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);
+ error_log('Times: ' . implode(', ', $data));
+ $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
+ * @return void
+ */
+ private static function handleArrayArgument(string &$query, array &$args)
+ {
+ $again = false;
+ 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) {
+ if (is_array($sub)) {
+ $new[] = '(' . $newkey . '_' . $subIndex . ')';
+ $again = true;
+ } else {
+ $new[] = $newkey . '_' . $subIndex;
+ }
+ $args[$newkey . '_' . $subIndex] = $sub;
+ }
+ unset($args[$key]);
+ $new = implode(',', $new);
+ $query = preg_replace('/' . $newkey . '\b/', $new, $query);
+ }
+ }
+ if ($again) {
+ self::handleArrayArgument($query, $args);
}
}
@@ -106,10 +349,115 @@ class Database
* Simply calls PDO::prepare and returns the PDOStatement.
* You must call PDOStatement::execute manually on it.
*/
- public static function prepare($query)
+ public static function prepare(string $query)
{
- self:init();
+ self::init();
+ self::$queryCount += 1; // Cannot know actual count
return self::$dbh->prepare($query);
}
+ /**
+ * Insert row into table, returning the generated key.
+ * This requires the table to have an AUTO_INCREMENT column and
+ * usually requires the given $uniqueValues to span across a UNIQUE index.
+ * The code first tries to SELECT the key for the given values without
+ * inserting first. This means this function is best used for cases
+ * where you expect that the entry already exists in the table, so
+ * only one SELECT will run. For all the entries that do not exist,
+ * an INSERT or INSERT IGNORE is run, depending on whether $additionalValues
+ * is empty or not. Another reason we don't run the INSERT (IGNORE) first
+ * is that it will increase the AUTO_INCREMENT value on InnoDB, even when
+ * no INSERT took place. So if you expect a lot of collisions you might
+ * use this function to prevent your A_I value from counting up too
+ * quickly.
+ * Other than that, this is just a dumb version of running INSERT and then
+ * getting the LAST_INSERT_ID(), or doing a query for the existing ID in
+ * case of a key collision.
+ *
+ * @param string $table table to insert into
+ * @param string $aiKey name of the AUTO_INCREMENT column
+ * @param array $uniqueValues assoc array containing columnName => value mapping
+ * @param ?array $additionalValues assoc array containing columnName => value mapping
+ * @return int AUTO_INCREMENT value matching the given unique values entry
+ */
+ public static function insertIgnore(string $table, string $aiKey, array $uniqueValues, array $additionalValues = null): int
+ {
+ // Sanity checks
+ if (array_key_exists($aiKey, $uniqueValues)) {
+ Util::traceError("$aiKey must not be in \$uniqueValues");
+ }
+ if (is_array($additionalValues) && array_key_exists($aiKey, $additionalValues)) {
+ Util::traceError("$aiKey must not be in \$additionalValues");
+ }
+ // Simple SELECT first
+ $selectSql = 'SELECT ' . $aiKey . ' FROM ' . $table . ' WHERE 1';
+ foreach ($uniqueValues as $key => $value) {
+ $selectSql .= ' AND ' . $key . ' = :' . $key;
+ }
+ $selectSql .= ' LIMIT 1';
+ $res = self::queryFirst($selectSql, $uniqueValues);
+ if ($res !== false) {
+ // Exists
+ if (!empty($additionalValues)) {
+ // Simulate ON DUPLICATE KEY UPDATE ...
+ $updateSql = 'UPDATE ' . $table . ' SET ';
+ $first = true;
+ foreach ($additionalValues as $key => $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $updateSql .= ', ';
+ }
+ $updateSql .= $key . ' = :' . $key;
+ }
+ $updateSql .= ' WHERE ' . $aiKey . ' = :' . $aiKey;
+ $additionalValues[$aiKey] = $res[$aiKey];
+ Database::exec($updateSql, $additionalValues);
+ }
+ return $res[$aiKey];
+ }
+ // Does not exist:
+ if (empty($additionalValues)) {
+ $combined =& $uniqueValues;
+ } else {
+ $combined = $uniqueValues + $additionalValues;
+ }
+ // Aight, try INSERT or INSERT IGNORE
+ $insertSql = 'INTO ' . $table . ' (' . implode(', ', array_keys($combined))
+ . ') VALUES (:' . implode(', :', array_keys($combined)) . ')';
+ if (empty($additionalValues)) {
+ // Simple INSERT IGNORE
+ $insertSql = 'INSERT IGNORE ' . $insertSql;
+ } else {
+ // INSERT ... ON DUPLICATE (in case we have a race)
+ $insertSql = 'INSERT ' . $insertSql . ' ON DUPLICATE KEY UPDATE ';
+ $first = true;
+ foreach ($additionalValues as $key => $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $insertSql .= ', ';
+ }
+ $insertSql .= $key . ' = VALUES(' . $key . ')';
+ }
+ }
+ self::exec($insertSql, $combined);
+ // Insert done, retrieve key again
+ $res = self::queryFirst($selectSql, $uniqueValues);
+ if ($res === false) {
+ Util::traceError('Could not find value in table ' . $table . ' that was just inserted');
+ }
+ return $res[$aiKey];
+ }
+
+ public static function getQueryCount(): int
+ {
+ return self::$queryCount;
+ }
+
+ public static function getQueryTime(): int
+ {
+ return self::$queryTime;
+ }
+
}
diff --git a/inc/image.inc.php b/inc/image.inc.php
index 1bad04f..03af811 100644
--- a/inc/image.inc.php
+++ b/inc/image.inc.php
@@ -3,21 +3,22 @@
class Image
{
- public static function deleteOwnedBy($userid)
+ public static function deleteOwnedBy(string $userid): bool
{
if ($userid === false || !is_numeric($userid))
return false;
//return Database::exec('DELETE FROM image WHERE ownerid = :userid', array('userid' => $userid));
// TODO
+ return false;
}
- public static function getImageCount($login)
+ public static function getImageCount(string $login): int
{
$ret = Database::queryFirst('SELECT Count(*) AS cnt FROM imagebase '
. ' WHERE imagebase.ownerid = :userid', array('userid' => $login));
if ($ret === false)
return 0;
- return $ret['cnt'];
+ return (int)$ret['cnt'];
}
}
diff --git a/inc/message.inc.php b/inc/message.inc.php
index 7decc12..67f7b08 100644
--- a/inc/message.inc.php
+++ b/inc/message.inc.php
@@ -2,31 +2,31 @@
class Message
{
- private static $list = array();
- private static $alreadyDisplayed = array();
- private static $flushed = false;
+ private static array $list = array();
+ private static array $alreadyDisplayed = array();
+ private static bool $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)
+ public static function addError(string $id): void
{
self::add('error', $id, func_get_args());
}
- public static function addWarning($id)
+ public static function addWarning(string $id): void
{
self::add('warning', $id, func_get_args());
}
- public static function addInfo($id)
+ public static function addInfo(string $id): void
{
self::add('info', $id, func_get_args());
}
- public static function addSuccess($id)
+ public static function addSuccess(string $id): void
{
self::add('success', $id, func_get_args());
}
@@ -35,7 +35,7 @@ class Message
* Internal function that adds a message. Used by
* addError/Success/Info/... above.
*/
- private static function add($type, $id, $params)
+ private static function add(string $type, string $id, array $params): void
{
self::$list[] = array(
'type' => $type,
@@ -50,7 +50,7 @@ class Message
* After calling this, any further calls to add* will be rendered in
* place in the current page output.
*/
- public static function renderList()
+ public static function renderList(): void
{
// Non-Ajax
foreach (self::$list as $item) {
@@ -69,7 +69,7 @@ class Message
* Get all queued messages, flushing the queue.
* Useful in api/ajax mode.
*/
- public static function asString()
+ public static function asString(): string
{
$return = '';
foreach (self::$list as $item) {
@@ -88,7 +88,7 @@ class Message
* Deserialize any messages from the current HTTP request and
* place them in the message queue.
*/
- public static function fromRequest()
+ public static function fromRequest(): void
{
$messages = is_array($_REQUEST['message']) ? $_REQUEST['message'] : array($_REQUEST['message']);
foreach ($messages as $message) {
@@ -102,7 +102,7 @@ class Message
* Turn the current message queue into a serialized version,
* suitable for appending to a GET or POST request
*/
- public static function toRequest()
+ public static function toRequest(): string
{
$parts = array();
foreach (array_merge(self::$list, self::$alreadyDisplayed) as $item) {
diff --git a/inc/render.inc.php b/inc/render.inc.php
index 0147709..6a22d3f 100644
--- a/inc/render.inc.php
+++ b/inc/render.inc.php
@@ -14,7 +14,7 @@ Render::init();
class Render
{
- private static $mustache = false;
+ private static ?Mustache_Engine $mustache = null;
private static $body = '';
private static $header = '';
private static $footer = '';
@@ -24,7 +24,7 @@ class Render
public static function init()
{
- if (self::$mustache !== false)
+ if (self::$mustache !== null)
Util::traceError('Called Render::init() twice!');
self::$mustache = new Mustache_Engine;
}
@@ -32,15 +32,13 @@ class Render
/**
* Output the buffered, generated page
*/
- public static function output()
+ public static function output(): void
{
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();
+ ob_start('ob_gzhandler');
echo
'<!DOCTYPE html>
- <html>
+ <html lang="de">
<head>
<title>', RENDER_DEFAULT_TITLE, self::$title, '</title>
<meta charset="utf-8">
@@ -69,14 +67,7 @@ class Render
'</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);
- }
+ ob_end_flush();
}
/**
diff --git a/inc/session.inc.php b/inc/session.inc.php
index 17e3184..f89d87e 100644
--- a/inc/session.inc.php
+++ b/inc/session.inc.php
@@ -3,13 +3,13 @@
class Session
{
- private static $sid = false;
- private static $data = false;
- private static $needUpdate = true;
+ private static ?string $sid = null;
+ private static ?array $data = null;
+ private static bool $needUpdate = true;
- private static function generateSessionId()
+ private static function generateSessionId(): void
{
- if (self::$sid !== false)
+ if (self::$sid !== null)
Util::traceError('Error: Asked to generate session id when already set.');
self::$sid = sha1(
mt_rand(0, 65535)
@@ -24,13 +24,13 @@ class Session
);
}
- public static function create()
+ public static function create(): void
{
self::generateSessionId();
self::$data = array();
}
- public static function load()
+ public static function load(): bool
{
// Try to load session id from cookie
if (!self::loadSessionId()) return false;
@@ -46,7 +46,7 @@ class Session
return self::get('uid');
}
- public static function setUid($value)
+ public static function setUid($value): void
{
if (strlen($value) < 5)
Util::traceError('Invalid user id: ' . $value);
@@ -60,7 +60,7 @@ class Session
return false;
}
- public static function set($key, $value)
+ public static function set(string $key, $value): void
{
if (!is_array(self::$data))
Util::traceError('Tried to set session data with no active session');
@@ -70,9 +70,9 @@ class Session
self::$needUpdate = true;
}
- private static function loadSessionId()
+ private static function loadSessionId(): bool
{
- if (self::$sid !== false)
+ if (self::$sid !== null)
Util::traceError('Error: Asked to load session id when already set.');
if (empty($_COOKIE['sid']))
return false;
@@ -83,18 +83,18 @@ class Session
return true;
}
- public static function delete()
+ public static function delete(): void
{
- if (self::$sid === false) return;
+ if (self::$sid === null) return;
Database::exec('DELETE FROM websession WHERE sid = :sid', array('sid' => self::$sid));
setcookie('sid', '', time() - CONFIG_SESSION_TIMEOUT, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
- self::$sid = false;
- self::$data = false;
+ self::$sid = null;
+ self::$data = null;
}
- public static function save()
+ public static function save(): void
{
- if (self::$sid === false || self::$data === false || !self::$needUpdate)
+ if (self::$sid === null || self::$data === null || !self::$needUpdate)
return;
$data = json_encode(self::$data);
$ret = Database::exec('INSERT INTO websession (sid, dateline, data) '
@@ -108,9 +108,9 @@ class Session
Util::traceError('Error: Could not set Cookie for Client (headers already sent)');
}
- public static function readSessionData()
+ public static function readSessionData(): bool
{
- if (self::$sid === false || self::$data !== false)
+ if (self::$sid === null || self::$data !== null)
Util::traceError('Tried to readSessionData on an active session!');
$data = Database::queryFirst('SELECT dateline, data FROM websession WHERE sid = :sid LIMIT 1', array('sid' => self::$sid));
if ($data === false) {
@@ -121,9 +121,8 @@ class Session
return false;
}
self::$needUpdate = ($data['dateline'] + 3600 < time());
- self::$data = @json_decode($data['data'], true);
- if (!is_array(self::$data))
- self::$data = array();
+ $data = @json_decode($data['data'], true);
+ self::$data = is_array($data) ? $data : [];
return true;
}
diff --git a/inc/shibauth.inc.php b/inc/shibauth.inc.php
new file mode 100644
index 0000000..6ae3a89
--- /dev/null
+++ b/inc/shibauth.inc.php
@@ -0,0 +1,202 @@
+<?php
+
+class ShibAuth
+{
+
+ /**
+ * Log user into master-server using the data provided by the current shibboleth session
+ * @param ?string $accessCode optional one-time access code to retreive session data via thrift
+ * @return array{status: string, firstName: string, lastName: string, mail: string, token: string, sessionId: string, userId: string, organizationId: string, url: string, error: string}
+ */
+ private static function loginInternal(?string $accessCode = null): array
+ {
+ if ($accessCode !== null) {
+ $entrop = strlen(count_chars($accessCode, 3));
+ if (strlen($accessCode) < 32 || strlen($accessCode) > 64 || $entrop < 10) {
+ return ['status' => 'error', 'error' => 'Malformed accessCode'];
+ }
+ }
+ // Handle
+ if (empty($_SERVER['persistent-id'])) {
+ // No persistent id given, should not happen!
+ file_put_contents('/tmp/shib-nopid-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
+ return ['status' => 'error', 'error' => 'Shibboleth metadata missing!'];
+ }
+ // Query database for user
+ // First, use persistent-id as-is
+ $shibId = [ md5($_SERVER['persistent-id']) ];
+ // ... but we might have multiple persistent IDs, split
+ if (strpos($_SERVER['persistent-id'], ';') !== false) {
+ foreach (explode(';', $_SERVER['persistent-id']) as $s) {
+ if (empty($s))
+ continue;
+ $shibId[] = md5($s);
+ }
+ }
+ // Figure out role
+ if (strpos(";{$_SERVER['entitlement']};", CONFIG_ENTITLEMENT) !== false) {
+ $role = 'TUTOR';
+ } else if (strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';employee@') !== false
+ || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';staff@') !== false
+ || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';faculty@') !== false) {
+ $role = 'TUTOR';
+ } else {
+ file_put_contents('/tmp/shib-student-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
+ $role = 'STUDENT';
+ // NEW: Ignore students for now
+ return [
+ 'status' => 'error',
+ 'error' => "Sie wurden als Student eingestuft und können sich daher nicht an der " . CONFIG_SUITE . "-Suite anmelden."
+ . "\nFalls Ihr Nutzerkonto kein Studentenkonto ist stellen Sie sicher, dass Ihr IdP für berechtigte"
+ . "\nAccounts entweder das " . CONFIG_SUITE . "-Entitlement ausliefert, oder das Attribut " . CONFIG_SCOPED_AFFILIATION
+ . "\nausgeliefert wird, und es entweder 'employee@..', 'staff@..' oder 'faculty@..' enthält."
+ . "\n\nMehr Informationen finden Sie unter " . CONFIG_HELPURL
+ ];
+ // end IGNORE STUDENTS
+ }
+ // Now we have an array of all persistent-ids, plus the raw string; see if any of those match
+ $user = Database::queryFirst("SELECT user.userid, user.organizationid, user.firstname, user.lastname, user.email "
+ . " FROM user "
+ . " INNER JOIN organization USING (organizationid) "
+ . " WHERE user.shibid IN (:shibid) LIMIT 1", array('shibid' => $shibId));
+ if ($user === false) {
+ // Not found, so we don't know which satellite to use
+ if ($role === 'STUDENT') {
+ $response['status'] = 'ok';
+ $response['firstName'] = $_SERVER['givenName'] ?? 'Anonymous';
+ $response['lastName'] = $_SERVER[CONFIG_SURNAME] ?? 'Student';
+ $response['mail'] = $_SERVER['mail'] ?? 'void@none.invalid';
+ $response['userId'] = $shibId;
+ // Try to figure out orgId
+ if (!isset($response['organizationId']) && isset($_SERVER[CONFIG_EPPN])) {
+ if (preg_match('/@(.+)$/', $_SERVER[CONFIG_EPPN], $out)) {
+ $out = Database::queryFirst("SELECT organizationid FROM organization_suffix WHERE suffix = :suffix", [
+ 'suffix' => $out[1]
+ ]);
+ if ($out !== false) {
+ $response['organizationId'] = $out['organizationid'];
+ }
+ }
+ }
+ if (!isset($response['organizationId']) && isset($_SERVER[CONFIG_SCOPED_AFFILIATION])) {
+ if (preg_match('/(^|;)[^@]+@([^;]+)/', $_SERVER[CONFIG_SCOPED_AFFILIATION], $out)) {
+ $out = Database::queryFirst("SELECT organizationid FROM organization_suffix WHERE suffix = :suffix", [
+ 'suffix' => $out[2]
+ ]);
+ if ($out !== false) {
+ $response['organizationId'] = $out['organizationid'];
+ }
+ }
+ }
+ // This one we send to the running master server handler
+ $rpc = $response;
+ $rpc['role'] = $role;
+ if (isset($response['organizationId']) && $accessCode === null) {
+ $response['satellites2'] = self::getSatelliteList($response['organizationId']);
+ }
+ } else {
+ $response['status'] = 'unregistered';
+ $response['error'] = 'Sie müssen sich erst für die Nutzung von ' . CONFIG_SUITE . ' registrieren';
+ }
+ $response['id'] = $shibId;
+ $response['url'] = CONFIG_MASTERWEBIF;
+ file_put_contents('/tmp/shib-unreg-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
+ } else {
+ /*
+ if (in_array($shibId, unserialize(CONFIG_ADMINS), true) || $shibId === '2fa5c3e020a5aca0cbf9a562268d5173-') {
+ $role = 'STUDENT';
+ }
+ */
+ // Found, see if we got personal information, either temporarily through metadata, or from database
+ $firstName = $user['firstname'];
+ $lastName = $user['lastname'];
+ $mail = $user['email'];
+ if (empty($firstName) && isset($_SERVER['givenName']))
+ $firstName = trim($_SERVER['givenName']);
+ if (empty($lastName) && isset($_SERVER[CONFIG_SURNAME]))
+ $lastName = trim($_SERVER[CONFIG_SURNAME]);
+ if (empty($mail) && isset($_SERVER['mail']))
+ $mail = trim($_SERVER['mail']);
+ //
+ $login = (empty($user['userid']) ? $shibId : $user['userid'] );
+ if (empty($firstName) || empty($lastName) || empty($login)) {
+ // This means the user did not provide personal information on signup, nor does the IdP send them
+ $response['status'] = 'anonymous';
+ $response['error'] = "Ihr IdP liefert nicht die erforderlichen Attribute\n"
+ . CONFIG_SURNAME . ', ' . 'givenName' . ', ' . 'email';
+ } else {
+ // Seems ok!
+ //
+ $response['status'] = 'ok';
+ $response['firstName'] = $firstName;
+ $response['lastName'] = $lastName;
+ $response['mail'] = $mail;
+ $response['userId'] = $user['userid'];
+ $response['organizationId'] = $user['organizationid'];
+ // This one we send to the running master server handler
+ $rpc = $response;
+ $rpc['userId'] = $login;
+ $rpc['role'] = $role;
+ // This one we only send to the user
+ $response['satellites2'] = self::getSatelliteList($user['organizationid']);
+ }
+ }
+
+ if (isset($rpc)) {
+ if ($accessCode !== null) {
+ $rpc['accessCode'] = $accessCode;
+ }
+ $reply = RPC::submit($rpc);
+ if (preg_match('/^TOKEN:(\w+) SESSIONID:(\w+)$/', $reply, $out)) {
+ // For talking to the sat server, also referred to as userToken in Java
+ $response['token'] = $out[1];
+ // For talking to the master server - not known by satellite server, referred to as masterToken in Java
+ $response['sessionId'] = $out[2];
+ } else {
+ if (empty($rpc['mail'])) {
+ $reply .= ' (No email given)';
+ }
+ if (empty($rpc['firstName'])) {
+ $reply .= ' (No first name given)';
+ }
+ if (empty($rpc['lastName'])) {
+ $reply .= ' (No last name given)';
+ }
+ if (empty($rpc['organizationId'])) {
+ $reply .= ' (No organization id found)';
+ }
+ $response['error'] = $reply;
+ $response['status'] = 'error';
+ }
+ }
+ return $response;
+ }
+
+ public static function login(?string $accessCode = null): array
+ {
+ $res = self::loginInternal($accessCode);
+ if ($res['status'] !== 'ok' && isset($res['error']) && $accessCode !== null) {
+ RPC::submit(['status' => 'error', 'error' => $res['error'], 'accessCode' => $accessCode]);
+ }
+ return $res;
+ }
+
+ private static function getSatelliteList($orgId)
+ {
+ // Determine satellite(s)
+ $res = Database::simpleQuery("SELECT satellitename, addresses, certsha256 FROM satellite"
+ . " WHERE organizationid = :organizationid AND userid IS NULL", array('organizationid' => $orgId));
+ $sat2 = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $addrs = json_decode($row['addresses'], true);
+ if (!is_array($addrs) || empty($addrs))
+ continue;
+ $sat2[$row['satellitename']] = array(
+ 'addresses' => $addrs,
+ 'certHash' => $row['certsha256']
+ );
+ }
+ return $sat2;
+ }
+
+} \ No newline at end of file
diff --git a/inc/user.inc.php b/inc/user.inc.php
index 6e3c06d..bc07f5d 100644
--- a/inc/user.inc.php
+++ b/inc/user.inc.php
@@ -3,15 +3,15 @@
class User
{
- private static $user = false;
- private static $organization = NULL;
- private static $isShib = false;
- private static $isInDb = false;
- private static $isAnonymous = false;
+ private static ?array $user = null;
+ private static ?array $organization = NULL;
+ private static bool $isShib = false;
+ private static bool $isInDb = false;
+ private static bool $isAnonymous = false;
- public static function isLoggedIn()
+ public static function isLoggedIn(): bool
{
- return self::$user !== false;
+ return self::$user !== null;
}
public static function isShibbolethAuth()
@@ -26,7 +26,7 @@ class User
public static function isLocalOnly()
{
- return self::$user !== false && self::$isShib === false;
+ return self::$user !== null && self::$isShib === false;
}
public static function isAnonymous()
@@ -39,44 +39,44 @@ class User
return self::$user;
}
- public static function getId()
+ public static function getId(): ?string
{
if (!isset(self::$user['userid']))
- return false;
+ return null;
return self::$user['userid'];
}
- public static function getMail()
+ public static function getMail(): ?string
{
if (!isset(self::$user['email']))
- return false;
+ return null;
return self::$user['email'];
}
- public static function getName()
+ public static function getName(): ?string
{
if (!self::isLoggedIn())
- return false;
+ return null;
return self::$user['firstname'] . ' ' . self::$user['lastname'];
}
- public static function getFirstName()
+ public static function getFirstName(): ?string
{
if (!self::isLoggedIn())
- return false;
+ return null;
return self::$user['firstname'];
}
- public static function getLastName()
+ public static function getLastName(): ?string
{
if (!self::isLoggedIn())
- return false;
+ return null;
return self::$user['lastname'];
}
- public static function hasFullName()
+ public static function hasFullName(): bool
{
- return self::$user !== false && !empty(self::$user['firstname']) && !empty(self::$user['lastname']);
+ return self::$user !== null && !empty(self::$user['firstname']) && !empty(self::$user['lastname']);
}
public static function isTutor()
@@ -84,7 +84,7 @@ class User
return isset(self::$user['role']) && self::$user['role'] === 'TUTOR';
}
- public static function isAdmin()
+ public static function isAdmin(): bool
{
// TODO: per Institution...
return in_array(self::getShibId(), unserialize(CONFIG_ADMINS), true);
@@ -95,19 +95,19 @@ class User
*
* @return string
*/
- public static function getOrganizationId()
+ public static function getOrganizationId(): ?string
{
$org = self::getOrganization();
if (!isset($org['organizationid']))
- return false;
+ return null;
return $org['organizationid'];
}
- public static function getOrganizationName()
+ public static function getOrganizationName(): ?string
{
$org = self::getOrganization();
if (!isset($org['name']))
- return false;
+ return null;
return $org['name'];
}
@@ -116,21 +116,26 @@ class User
*
* @return string
*/
- public static function getRemoteOrganizationId()
+ public static function getRemoteOrganizationId(): ?string
{
if (empty(self::$user['organization']))
- return false;
+ return null;
return self::$user['organization'];
}
- public static function getOrganization()
+ /**
+ * Return user's organization, or null if not known in our DB.
+ * @return ?array{organizationid: string, name: string}
+ */
+ public static function getOrganization(): ?array
{
if (!self::isLoggedIn())
- return false;
+ return null;
if (is_null(self::$organization)) {
- self::$organization = Database::queryFirst('SELECT organizationid, name FROM organization_suffix '
+ $org = Database::queryFirst('SELECT organizationid, name FROM organization_suffix '
. ' INNER JOIN organization USING (organizationid) '
. ' WHERE suffix = :org LIMIT 1', array('org' => self::$user['organization']));
+ self::$organization = $org !== false ? $org : null;
}
return self::$organization;
}
@@ -159,8 +164,10 @@ class User
return false;
}
// Try user from local DB
- self::$user = Database::queryFirst('SELECT userid, shibid, organizationid AS organization, firstname, lastname, email FROM user WHERE userid = :uid LIMIT 1', array('uid' => Session::getUid()));
- self::$isInDb = self::$user !== false;
+ $usr = Database::queryFirst('SELECT userid, shibid, organizationid AS organization, firstname, lastname, email
+ FROM user WHERE userid = :uid LIMIT 1', ['uid' => Session::getUid()]);
+ self::$user = $usr !== false ? $usr : null;
+ self::$isInDb = self::$user !== null;
if (!self::$isInDb) {
Session::delete();
}
@@ -181,38 +188,45 @@ class User
Util::redirect('?do=Main&force-cookie=true.dat');
}
self::$isShib = true;
- if (!isset($_SERVER['sn']))
- $_SERVER['sn'] = '';
+ if (!isset($_SERVER[CONFIG_SURNAME]))
+ $_SERVER[CONFIG_SURNAME] = '';
if (!isset($_SERVER['givenName']))
$_SERVER['givenName'] = '';
if (!isset($_SERVER['mail']))
$_SERVER['mail'] = '';
- $shibId = md5($_SERVER['persistent-id']);
+ $shibId = [];
+ if (strpos($_SERVER['persistent-id'], ';') !== false) {
+ foreach (explode(';', $_SERVER['persistent-id']) as $s) {
+ $shibId[] = md5($s);
+ }
+ }
+ $shibId[] = md5($_SERVER['persistent-id']);
self::$user = array(
'userid' => NULL,
- 'shibid' => $shibId,
+ 'shibid' => $shibId[0],
'firstname' => $_SERVER['givenName'],
- 'lastname' => $_SERVER['sn'],
+ 'lastname' => $_SERVER[CONFIG_SURNAME],
'email' => $_SERVER['mail'],
);
// Figure out whether the user should be considered a tutor
- if (isset($_SERVER['affiliation']) && (strpos(";{$_SERVER['affiliation']}", ';employee@') !== false
- || strpos(";{$_SERVER['affiliation']}", ';staff@') !== false
- || strpos(";{$_SERVER['affiliation']}", ';faculty@') !== false))
+ if (isset($_SERVER[CONFIG_SCOPED_AFFILIATION]) && (strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]}", ';employee@') !== false
+ || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]}", ';staff@') !== false
+ || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]}", ';faculty@') !== false))
self::$user['role'] = 'TUTOR';
elseif (isset($_SERVER['entitlement']) && strpos(";{$_SERVER['entitlement']};", CONFIG_ENTITLEMENT) !== false)
self::$user['role'] = 'TUTOR';
else
self::$user['role'] = 'STUDENT';
// Try to figure out organization
- if (isset($_SERVER['eppn']) && preg_match('/@([0-9a-zA-Z\-\._]+)$/', $_SERVER['eppn'], $out)) {
+ if (isset($_SERVER[CONFIG_EPPN]) && preg_match('/@([0-9a-zA-Z\-._]+)$/', $_SERVER[CONFIG_EPPN], $out)) {
self::$user['organization'] = $out[1];
}
- if (!isset(self::$user['organization']) && isset($_SERVER['affiliation']) && preg_match('/@([0-9a-zA-Z\-\._]+)(;|$)/', $_SERVER['affiliation'], $out)) {
+ if (!isset(self::$user['organization']) && isset($_SERVER[CONFIG_SCOPED_AFFILIATION]) && preg_match('/@([0-9a-zA-Z\-._]+)(;|$)/', $_SERVER[CONFIG_SCOPED_AFFILIATION], $out)) {
self::$user['organization'] = $out[1];
}
// Get matching db entry if any
- $user = Database::queryFirst('SELECT userid, firstname, lastname, email, fixedname FROM user WHERE shibid = :shibid LIMIT 1', array('shibid' => $shibId));
+ $user = Database::queryFirst('SELECT userid, firstname, lastname, email, fixedname FROM user
+ WHERE shibid IN (:shibid) LIMIT 1', ['shibid' => $shibId]);
if ($user === false) {
// No match in database, user is not signed up
return true;
@@ -232,11 +246,16 @@ class User
return true;
}
- public static function deploy($anonymous, $existingLogin = false)
+ public static function deploy(bool $anonymous, $existingLogin = false): bool
{
if (empty(self::$user['shibid']))
Util::traceError('NO SHIBID');
+ if (self::getOrganizationId() === null) {
+ Message::addError('Your home organization ID {{0}} is not known to this server', self::getRemoteOrganizationId());
+ Util::redirect('?do=Main');
+ }
+
// Merging with test-account:
if (!empty($existingLogin)) {
if ($anonymous) {
@@ -300,7 +319,7 @@ class User
'mail' => $mail,
'user' => self::getId()
));
- return $ret == 1 || $mail === self::get('email');
+ return $ret == 1 || $mail === self::$user['email'];
}
public static function login($user, $pass)
diff --git a/inc/util.inc.php b/inc/util.inc.php
index aaf46c6..0e06e6a 100644
--- a/inc/util.inc.php
+++ b/inc/util.inc.php
@@ -59,7 +59,7 @@ SADFACE;
public static function redirect($location = false)
{
if ($location === false) {
- $location = preg_replace('/(&|\?)message\[\]\=[^&]*/', '\1', $_SERVER['REQUEST_URI']);
+ $location = preg_replace('/([&?])message\[]=[^&]*/', '\1', $_SERVER['REQUEST_URI']);
}
Session::save();
$messages = Message::toRequest();
@@ -113,9 +113,9 @@ SADFACE;
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);
+ $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);
}
@@ -123,11 +123,11 @@ SADFACE;
* 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
+ * @param int $bytes numeric value of the filesize to make readable
+ * @param int $decimals number of decimals to show, -1 for automatic
+ * @return string human-readable string representing the given filesize
*/
- public static function readableFileSize($bytes, $decimals = -1)
+ public static function readableFileSize(int $bytes, int $decimals = -1): string
{
static $sz = array('Byte', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB');
$factor = floor((strlen($bytes) - 1) / 3);
@@ -139,20 +139,20 @@ SADFACE;
return sprintf("%.{$decimals}f ", $bytes / pow(1024, $factor)) . $sz[$factor];
}
- public static function sanitizeFilename($name)
+ public static function sanitizeFilename(string $name): string
{
return preg_replace('/[^a-zA-Z0-9_\-]+/', '_', $name);
}
- public static function safePath($path, $prefix = '')
+ public static function safePath(string $path, string $prefix = ''): ?string
{
if (empty($path))
- return false;
+ return null;
$path = trim($path);
- if ($path{0} == '/' || preg_match('/[\x00-\x19\?\*]/', $path))
- return false;
+ if ($path[0] == '/' || preg_match('/[\x00-\x19?*]/', $path))
+ return null;
if (strpos($path, '..') !== false)
- return false;
+ return null;
if (substr($path, 0, 2) !== './')
$path = "./$path";
if (empty($prefix))
@@ -160,7 +160,7 @@ SADFACE;
if (substr($prefix, 0, 2) !== './')
$prefix = "./$prefix";
if (substr($path, 0, strlen($prefix)) !== $prefix)
- return false;
+ return null;
return $path;
}
@@ -249,14 +249,14 @@ SADFACE;
/**
* 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.
+ * @param string $file path of local file
+ * @param string $name name of file to send to user agent
+ * @param bool $delete delete the file when done?
+ * @return bool 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)
+ public static function sendFile(string $file, string $name, bool $delete = false): bool
{
while ((@ob_get_level()) > 0)
@ob_end_clean();