<?php
class User
{
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(): bool
{
return self::$user !== null;
}
public static function isShibbolethAuth(): bool
{
return self::$isShib;
}
public static function isInDatabase(): bool
{
return self::$isInDb;
}
public static function isLocalOnly(): bool
{
return self::$user !== null && self::$isShib === false;
}
public static function isAnonymous(): bool
{
return self::$isAnonymous;
}
public static function getData(): ?array
{
return self::$user;
}
public static function getId(): ?string
{
if (!isset(self::$user['userid']))
return null;
return self::$user['userid'];
}
public static function getMail(): ?string
{
if (!isset(self::$user['email']))
return null;
return self::$user['email'];
}
public static function getName(): ?string
{
if (!self::isLoggedIn())
return null;
return self::$user['firstname'] . ' ' . self::$user['lastname'];
}
public static function getFirstName(): ?string
{
if (!self::isLoggedIn())
return null;
return self::$user['firstname'];
}
public static function getLastName(): ?string
{
if (!self::isLoggedIn())
return null;
return self::$user['lastname'];
}
public static function hasFullName(): bool
{
return self::$user !== null && !empty(self::$user['firstname']) && !empty(self::$user['lastname']);
}
public static function isTutor(): bool
{
return isset(self::$user['role']) && self::$user['role'] === 'TUTOR';
}
public static function isAdmin(): bool
{
// TODO: per Institution...
return in_array(self::getShibId(), unserialize(CONFIG_ADMINS), true);
}
/**
* Organization ID used locally in our DB
*
* @return string
*/
public static function getOrganizationId(): ?string
{
$org = self::getOrganization();
if (!isset($org['organizationid']))
return null;
return $org['organizationid'];
}
public static function getOrganizationName(): ?string
{
$org = self::getOrganization();
if (!isset($org['name']))
return null;
return $org['name'];
}
/**
* Organization ID as supplied by shibboleth
*
* @return string
*/
public static function getRemoteOrganizationId(): ?string
{
if (empty(self::$user['organization']))
return null;
return self::$user['organization'];
}
/**
* 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 null;
if (is_null(self::$organization)) {
$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;
}
public static function getShibId()
{
if (empty(self::$user['shibid']))
return false;
return self::$user['shibid'];
}
public static function load(): bool
{
//file_put_contents('/tmp/test-' . time(), print_r($_SERVER, true));
if (self::isLoggedIn())
return true;
$hasSession = Session::load();
if (empty($_SERVER['persistent-id'])) {
if (Session::getUid() === false) {
if (!empty($_SERVER['Shib-Session-ID'])) {
Message::addError('Sie haben sich erfolgreich mittels {{0}} authentifiziert,'
. ' aber der IdP Ihrer Einrichtung scheint die benötigten Metadaten nicht'
. ' an den {{1}}-SP zu übermitteln. Bitte wenden Sie sich an den Support.', CONFIG_IDM, CONFIG_SUITE);
}
Session::delete();
file_put_contents('/tmp/shib-load-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
return false;
}
// Try user from local DB
$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();
}
return self::$isInDb;
}
// Try bwIDM etc.
if (!$hasSession) {
// Make sure cookies are enabled
if (!empty($_SERVER['Shib-Session-ID'])) {
if (isset($_GET['force-cookie']))
die('Bitte aktivieren Sie Cookies und Javascript!');
}
Session::create();
Session::set('token', md5(mt_rand() . $_SERVER['REMOTE_ADDR'] . microtime(true) . $_SERVER['persistent-id'] . mt_rand()));
Session::save();
if (!empty($_SERVER['Shib-Session-ID']))
Util::redirect('?do=Main&force-cookie=true.dat');
}
self::$isShib = true;
if (!isset($_SERVER[CONFIG_SURNAME]))
$_SERVER[CONFIG_SURNAME] = '';
if (!isset($_SERVER['givenName']))
$_SERVER['givenName'] = '';
if (!isset($_SERVER['mail']))
$_SERVER['mail'] = '';
$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[0],
'firstname' => $_SERVER['givenName'],
'lastname' => $_SERVER[CONFIG_SURNAME],
'email' => $_SERVER['mail'],
);
// Figure out whether the user should be considered a tutor
self::$user['role'] = Util::getRole();
// Try to figure out organization
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[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 IN (:shibid) LIMIT 1', ['shibid' => $shibId]);
if ($user === false) {
// No match in database, user is not signed up
return true;
}
self::$user['userid'] = $user['userid'];
if (Session::getUid() === false) {
Session::setUid($user['userid']);
Session::save();
}
// Already signed up, see if we can fetch missing fields from DB
self::$isInDb = true;
self::$isAnonymous = (empty($user['firstname']) && empty($user['lastname']));
foreach (array('firstname', 'lastname', 'email') as $key) {
if (empty(self::$user[$key]))
self::$user[$key] = $user[$key];
}
return true;
}
public static function deploy(bool $anonymous, ?string $existingLogin = null): 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) {
$ret = Database::exec("UPDATE user SET shibid = :shibid, firstname = '', lastname = '', email = '', password = '' "
. " WHERE userid = :userid LIMIT 1", array(
'shibid' => self::$user['shibid'],
'userid' => $existingLogin
));
} else {
$ret = Database::exec("UPDATE user SET shibid = :shibid, password = '', firstname = :firstname, lastname = :lastname, email = :email "
. " WHERE userid = :userid LIMIT 1", array(
'shibid' => self::$user['shibid'],
'userid' => $existingLogin,
'firstname' => self::$user['firstname'],
'lastname' => self::$user['lastname'],
'email' => self::$user['email']
));
}
return $ret > 0;
}
// New account
if ($anonymous) {
Database::exec("INSERT INTO user (shibid, userid, organizationid, firstname, lastname, email) "
. " VALUES (:shibid, :shibid, :org, '', '', '') "
. " ON DUPLICATE KEY UPDATE firstname = '', lastname = '', email = '', password = ''", array(
'shibid' => self::$user['shibid'],
'org' => self::getOrganizationId()
));
} else {
Database::exec("INSERT INTO user (shibid, userid, organizationid, firstname, lastname, email) "
. " VALUES (:shibid, :shibid, :org, :firstname, :lastname, :email) "
. " ON DUPLICATE KEY UPDATE firstname = VALUES(firstname), lastname = VALUES(lastname), email = VALUES(email), password = ''", array(
'shibid' => self::$user['shibid'],
'firstname' => self::$user['firstname'],
'lastname' => self::$user['lastname'],
'email' => self::$user['email'],
'org' => self::getOrganizationId()
));
}
return true;
}
public static function updatePassword(string $pass): bool
{
if (!self::isLoggedIn() || self::$isShib || !self::$isInDb)
return false;
$pw = Crypto::hash6($pass);
$ret = Database::exec('UPDATE user SET password = :pass WHERE userid = :user LIMIT 1', array(
'pass' => $pw,
'user' => self::getId()
));
return $ret == 1;
}
public static function updateMail(string $mail): bool
{
if (!self::isLoggedIn() || self::$isShib || !self::$isInDb)
return false;
$ret = Database::exec('UPDATE user SET email = :mail WHERE userid = :user LIMIT 1', array(
'mail' => $mail,
'user' => self::getId()
));
return $ret == 1 || $mail === self::$user['email'];
}
public static function login(string $user, string $pass): bool
{
$ret = Database::queryFirst('SELECT userid, password FROM user WHERE userid = :user LIMIT 1', array(':user' => $user));
if ($ret === false)
return false;
if (!Crypto::verify($pass, $ret['password']))
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(): never
{
foreach ($_COOKIE as $name => $value) {
if (substr($name, 0, 5) !== '_shib')
continue;
@setcookie($name, '', time() - CONFIG_SESSION_TIMEOUT, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
}
Session::delete();
if (self::$isShib) {
Header('Location: /Shibboleth.sso/Logout');
} else {
Header('Location: ?do=Main');
}
exit(0);
}
public static function delete(): bool
{
if (!User::isLoggedIn() || !User::isInDatabase())
return true;
return Database::exec("DELETE FROM user WHERE userid = :userid LIMIT 1", array('userid' => User::getId()), true) > 0;
}
}