Merge BusinessAppTemplate

# Conflicts:
#	api.php
#	composer.lock
#	lang/en_us.php
#	lib/login.php
#	mobile/index.php
master
Skylar Ittner 6 years ago
commit 6ad9d1c716

@ -8,8 +8,6 @@
* Make things happen when buttons are pressed and forms submitted.
*/
require_once __DIR__ . "/required.php";
require_once __DIR__ . "/lib/login.php";
require_once __DIR__ . "/lib/userinfo.php";
if ($VARS['action'] !== "signout") {
@ -32,7 +30,7 @@ function returnToSender($msg, $arg = "") {
die();
}
if ($VARS['action'] != "signout" && !account_has_permission($_SESSION['username'], "TASKFLOOR")) {
if ($VARS['action'] != "signout" && !(new User($_SESSION['uid']))->hasPermission("TASKFLOOR")) {
returnToSender("no_permission");
}
@ -47,8 +45,8 @@ switch ($VARS['action']) {
if (is_empty($VARS['to'])) {
$to = null;
die(); // TODO: add some kind of permission thing to allow this
} else if (user_exists($VARS['to'])) {
$to = getUserByUsername($VARS['to'])['uid'];
} else if (User::byUsername($VARS['to'])->exists()) {
$to = User::byUsername($VARS['to'])->getUID();
} else {
die();
}
@ -56,6 +54,10 @@ switch ($VARS['action']) {
die();
}
$database->insert('messages', ['messagetext' => $msg, 'messagedate' => date("Y-m-d H:i:s"), 'from' => $_SESSION['uid'], 'to' => $to]);
if (!is_null($to)) {
$touser = new User($to);
Notifications::add($touser, $Strings->get("TaskFloor: New message"), "$msg\nFrom: " . (new User($_SESSION['uid']))->getName());
}
break;
case "delmsg":
header('HTTP/1.0 204 No Content');
@ -66,10 +68,11 @@ switch ($VARS['action']) {
die();
}
$msg = $database->select('messages', ['to', 'from'], ['messageid' => $VARS['msgid']])[0];
$me = new User($_SESSION['uid']);
if ($msg['to'] == $_SESSION['uid'] ||
$msg['from'] == $_SESSION['uid'] ||
isManagerOf($_SESSION['uid'], $msg['to']) ||
isManagerOf($_SESSION['uid'], $msg['from'])) {
$me->isManagerOf(new User($msg['to'])) ||
$me->isManagerOf(new User($msg['from']))) {
$database->update('messages', ['deleted' => 1], ['messageid' => $VARS['msgid']]);
}
break;
@ -85,6 +88,11 @@ switch ($VARS['action']) {
die('Invalid operation.');
}
header('HTTP/1.0 204 No Content');
if ($database->get('assigned_tasks', 'statusid', ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $_SESSION['uid']]]) == 4) {
$owneruid = $database->get('tasks', 'taskcreatoruid', ['taskid' => $VARS['taskid']]);
$tasktitle = $database->get('tasks', 'tasktitle', ['taskid' => $VARS['taskid']]);
Notifications::add(new User($owneruid), $Strings->get("TaskFloor: Problem resolved"), (new User($_SESSION['uid']))->getName() . " has resolved their problem with task $tasktitle.");
}
$database->update('assigned_tasks', ['statusid' => 1], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $_SESSION['uid']]]);
break;
case "finish":
@ -93,6 +101,9 @@ switch ($VARS['action']) {
die('You are not assigned to this task!');
}
$database->update('assigned_tasks', ['endtime' => date("Y-m-d H:i:s"), 'statusid' => 2], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $_SESSION['uid']]]);
$owneruid = $database->get('tasks', 'taskcreatoruid', ['taskid' => $VARS['taskid']]);
$tasktitle = $database->get('tasks', 'tasktitle', ['taskid' => $VARS['taskid']]);
Notifications::add(new User($owneruid), $Strings->get("TaskFloor: Task finished"), (new User($_SESSION['uid']))->getName() . " has finished task $tasktitle.");
break;
case "pause":
if (!$database->has('assigned_tasks', ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $_SESSION['uid']]])) {
@ -107,6 +118,9 @@ switch ($VARS['action']) {
}
header('HTTP/1.0 204 No Content');
$database->update('assigned_tasks', ['statusid' => 4], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $_SESSION['uid']]]);
$owneruid = $database->get('tasks', 'taskcreatoruid', ['taskid' => $VARS['taskid']]);
$tasktitle = $database->get('tasks', 'tasktitle', ['taskid' => $VARS['taskid']]);
Notifications::add(new User($owneruid), $Strings->get("TaskFloor: Problem reported"), (new User($_SESSION['uid']))->getName() . " has reported a problem with task $tasktitle.");
break;
case "edittask":
if (is_empty($VARS['tasktitle'])) {
@ -139,8 +153,12 @@ switch ($VARS['action']) {
$database->update('tasks', ['taskdueby' => null], ['taskid' => $VARS['taskid']]);
}
if (!is_empty($VARS['assignedto']) && user_exists($VARS['assignedto'])) {
$uid = getUserByUsername($VARS['assignedto'])['uid'];
$managed_uids = getManagedUIDs($_SESSION['uid']);
$uid = User::byUsername($VARS['assignedto'])->getUID();
$managed_users = (new User($_SESSION['uid']))->getManagedUsers();
$managed_uids = [];
foreach ($managed_users as $m) {
$managed_uids[] = $m->getUID();
}
// allow self-assignment
if (!in_array($uid, $managed_uids) && $uid != $_SESSION['uid']) {
header('Location: app.php?page=edittask&taskid=' . $VARS['taskid'] . '&msg=user_not_managed');
@ -160,7 +178,11 @@ switch ($VARS['action']) {
die('Missing taskid.');
}
$managed_uids = getManagedUIDs($_SESSION['uid']);
$managed_users = (new User($_SESSION['uid']))->getManagedUsers();
$managed_uids = [];
foreach ($managed_users as $m) {
$managed_uids[] = $m->getUID();
}
// There needs to be at least one entry otherwise the SQL query craps itself
if (count($managed_uids) < 1) {
$managed_uids = [-1];

@ -12,33 +12,57 @@
* user passwords.
*/
require __DIR__ . '/required.php';
require_once __DIR__ . '/lib/login.php';
require_once __DIR__ . '/lib/userinfo.php';
header("Content-Type: application/json");
/**
* Checks if the given AccountHub API key is valid by attempting to
* access the API with it.
* @param String $key The API key to check
* @return boolean TRUE if the key is valid, FALSE if invalid or something went wrong
*/
function checkAPIKey($key) {
try {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => $key,
'action' => "ping"
]
]);
if ($response->getStatusCode() === 200) {
return true;
}
return false;
} catch (Exception $e) {
return false;
}
}
$username = $VARS['username'];
$password = $VARS['password'];
if (user_exists($username) !== true || (authenticate_user($username, $password, $errmsg) !== true && checkAPIKey($password) !== true)) {
$user = User::byUsername($username);
if ($user->exists() !== true || ((Login::auth($username, $password) !== Login::LOGIN_OK) && !checkAPIKey($password))) {
header("HTTP/1.1 403 Unauthorized");
die("\"403 Unauthorized\"");
}
if (!account_has_permission($username, "TASKFLOOR")) {
if (!$user->hasPermission("TASKFLOOR")) {
header("HTTP/1.1 403 Unauthorized");
die("\"403 Unauthorized\"");
}
$userinfo = getUserByUsername($username);
// query max results
$max = 20;
if (preg_match("/^[0-9]+$/", $VARS['max']) === 1 && $VARS['max'] <= 1000) {
if (isset($VARS['max']) && preg_match("/^[0-9]+$/", $VARS['max']) === 1 && $VARS['max'] <= 1000) {
$max = (int) $VARS['max'];
}
switch ($VARS['action']) {
case "gettasks":
$tasks = $database->query("SELECT * FROM assigned_tasks LEFT JOIN tasks ON assigned_tasks.taskid = tasks.taskid WHERE assigned_tasks.userid = '" . $userinfo['uid'] . "' AND assigned_tasks.statusid IN (0,1,3,4) AND taskassignedon <= NOW() AND tasks.deleted = 0 ORDER BY 0 - taskdueby DESC LIMIT $max")->fetchAll();
$tasks = $database->query("SELECT * FROM assigned_tasks LEFT JOIN tasks ON assigned_tasks.taskid = tasks.taskid WHERE assigned_tasks.userid = '" . $user->getUID() . "' AND assigned_tasks.statusid IN (0,1,3,4) AND taskassignedon <= NOW() AND tasks.deleted = 0 ORDER BY 0 - taskdueby DESC LIMIT $max")->fetchAll();
$out = ["status" => "OK", "maxresults" => $max, "tasks" => []];
foreach ($tasks as $task) {
$icon = "ellipsis-h";
@ -78,9 +102,9 @@ switch ($VARS['action']) {
], [
"AND" => [
"OR" => [
"to" => $userinfo['uid'],
"to" => $user->getUID(),
"to #null" => null,
"from" => $userinfo['uid']
"from" => $user->getUID()
],
"deleted" => 0
],
@ -95,14 +119,14 @@ switch ($VARS['action']) {
foreach ($messages as $msg) {
$to = null;
if (!isset($usercache[$msg['from']])) {
$usercache[$msg['from']] = getUserByID($msg['from']);
$usercache[$msg['from']] = new User($msg['from']);
}
if (is_null($msg['to'])) {
$to['name'] = lang("all users", false);
$to['username'] = lang("all users", false);
} else {
if (!isset($usercache[$msg['to']])) {
$usercache[$msg['to']] = getUserByID($msg['to']);
$usercache[$msg['to']] = new User($msg['to']);
}
$to = $usercache[$msg['to']];
}
@ -110,40 +134,40 @@ switch ($VARS['action']) {
$out['messages'][$msg['id']] = [
"text" => $msg['text'],
"from" => [
"username" => $usercache[$msg['from']]['username'],
"name" => $usercache[$msg['from']]['name']
"username" => $usercache[$msg['from']]->getUsername(),
"name" => $usercache[$msg['from']]->getName()
],
"to" => [
"username" => $to['username'],
"name" => $to['name']
"username" => $to->getUsername(),
"name" => $to->getName()
],
"sent" => date("F j, Y, g:i a", strtotime($msg['date']))
];
}
exit(json_encode($out));
case "updatetask":
if (!$database->has('assigned_tasks', ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $userinfo['uid']]])) {
if (!$database->has('assigned_tasks', ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $user->getUID()]])) {
die('{"status": "ERROR", "msg": "You are not assigned to this task!"}');
}
switch ($VARS['status']) {
case "start":
$database->update('assigned_tasks', ['starttime' => date("Y-m-d H:i:s"), 'statusid' => 1], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $userinfo['uid']]]);
$database->update('assigned_tasks', ['starttime' => date("Y-m-d H:i:s"), 'statusid' => 1], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $user->getUID()]]);
break;
case "resume":
if (!$database->has('assigned_tasks', ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $userinfo['uid'], 'starttime[!]' => null]])) {
if (!$database->has('assigned_tasks', ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $user->getUID(), 'starttime[!]' => null]])) {
die('{"status": "ERROR", "msg": "Cannot resume non-started task."}');
}
$database->update('assigned_tasks', ['statusid' => 1], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $userinfo['uid']]]);
$database->update('assigned_tasks', ['statusid' => 1], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $user->getUID()]]);
break;
case "finish":
$database->update('assigned_tasks', ['endtime' => date("Y-m-d H:i:s"), 'statusid' => 2], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $userinfo['uid']]]);
$database->update('assigned_tasks', ['endtime' => date("Y-m-d H:i:s"), 'statusid' => 2], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $user->getUID()]]);
break;
case "pause":
$database->update('assigned_tasks', ['statusid' => 3], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $userinfo['uid']]]);
$database->update('assigned_tasks', ['statusid' => 3], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $user->getUID()]]);
break;
case "problem":
$database->update('assigned_tasks', ['statusid' => 4], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $userinfo['uid']]]);
$database->update('assigned_tasks', ['statusid' => 4], ["AND" => ['taskid' => $VARS['taskid'], 'userid' => $user->getUID()]]);
break;
default:
die('{"status": "ERROR", "msg": "Invalid status requested."}');
@ -152,14 +176,14 @@ switch ($VARS['action']) {
case "sendmsg":
$msg = strip_tags($VARS['msg']);
if (user_exists($VARS['to'])) {
$to = getUserByUsername($VARS['to'])['uid'];
$to = User::byUsername($VARS['to'])->getUID();
} else {
die('{"status": "ERROR", "msg": "Invalid user."}');
}
if (is_empty($msg)) {
die('{"status": "ERROR", "msg": "Missing message."}');
}
$database->insert('messages', ['messagetext' => $msg, 'messagedate' => date("Y-m-d H:i:s"), 'from' => $userinfo['uid'], 'to' => $to]);
$database->insert('messages', ['messagetext' => $msg, 'messagedate' => date("Y-m-d H:i:s"), 'from' => $user->getUID(), 'to' => $to]);
die('{"status": "OK"}');
default:
header("HTTP/1.1 400 Bad Request");

@ -69,9 +69,9 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (!isset($_GET['arg']) || is_empty($_GET['arg'])) {
$alertmsg = lang(MESSAGES[$_GET['msg']]['string'], false);
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
} else {
$alertmsg = lang2(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
@ -146,7 +146,7 @@ END;
if (isset($pg['icon'])) {
?><i class="<?php echo $pg['icon']; ?> fa-fw"></i> <?php
}
lang($pg['title']);
$Strings->get($pg['title']);
?>
</a>
</span>
@ -163,7 +163,7 @@ END;
</span>
<span class="nav-item mr-auto py-<?php echo $navbar_breakpoint; ?>-0">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="action.php?action=signout">
<i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php lang("sign out") ?></span>
<i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php $Strings->get("sign out") ?></span>
</a>
</span>
</div>

34
composer.lock generated

@ -9,16 +9,16 @@
"packages": [
{
"name": "catfan/medoo",
"version": "v1.5.3",
"version": "v1.5.7",
"source": {
"type": "git",
"url": "https://github.com/catfan/Medoo.git",
"reference": "1aa25a4001e0cfb739ba2996f00f4a3d2a7fdf07"
"reference": "8d90cba0e8ff176028847527d0ea76fe41a06ecf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/catfan/Medoo/zipball/1aa25a4001e0cfb739ba2996f00f4a3d2a7fdf07",
"reference": "1aa25a4001e0cfb739ba2996f00f4a3d2a7fdf07",
"url": "https://api.github.com/repos/catfan/Medoo/zipball/8d90cba0e8ff176028847527d0ea76fe41a06ecf",
"reference": "8d90cba0e8ff176028847527d0ea76fe41a06ecf",
"shasum": ""
},
"require": {
@ -64,20 +64,20 @@
"sql",
"sqlite"
],
"time": "2017-12-25 17:02:41"
"time": "2018-06-14 18:59:08"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.9.3",
"version": "v4.10.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "95e1bae3182efc0f3422896a3236e991049dac69"
"reference": "d85d39da4576a6934b72480be6978fb10c860021"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/95e1bae3182efc0f3422896a3236e991049dac69",
"reference": "95e1bae3182efc0f3422896a3236e991049dac69",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/d85d39da4576a6934b72480be6978fb10c860021",
"reference": "d85d39da4576a6934b72480be6978fb10c860021",
"shasum": ""
},
"require": {
@ -111,20 +111,20 @@
"keywords": [
"html"
],
"time": "2017-06-03 02:28:16"
"time": "2018-02-23 01:58:20"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.3.0",
"version": "6.3.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699"
"reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699",
"reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
"reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
"shasum": ""
},
"require": {
@ -134,7 +134,7 @@
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^4.0 || ^5.0",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
"psr/log": "^1.0"
},
"suggest": {
@ -143,7 +143,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.2-dev"
"dev-master": "6.3-dev"
}
},
"autoload": {
@ -176,7 +176,7 @@
"rest",
"web service"
],
"time": "2017-06-22 18:50:49"
"time": "2018-04-22 15:46:56"
},
{
"name": "guzzlehttp/promises",

@ -5,80 +5,91 @@
require_once __DIR__ . "/required.php";
require_once __DIR__ . "/lib/login.php";
// if we're logged in, we don't need to be here.
if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && !isset($_GET['permissionerror'])) {
header('Location: app.php');
}
if (isset($_GET['permissionerror'])) {
$alert = lang("no access permission", false);
$alert = $Strings->get("no access permission", false);
}
/* Authenticate user */
$userpass_ok = false;
$multiauth = false;
if (checkLoginServer()) {
if (!empty($VARS['progress']) && $VARS['progress'] == "1") {
if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && verifyCaptcheck($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
$errmsg = "";
if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) {
switch (get_account_status($VARS['username'])) {
if (Login::checkLoginServer()) {
if (empty($VARS['progress'])) {
// Easy way to remove "undefined" warnings.
} else if ($VARS['progress'] == "1") {
if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
$autherror = "";
$user = User::byUsername($VARS['username']);
if ($user->exists()) {
$status = $user->getStatus()->getString();
switch ($status) {
case "LOCKED_OR_DISABLED":
$alert = lang("account locked", false);
$alert = $Strings->get("account locked", false);
break;
case "TERMINATED":
$alert = lang("account terminated", false);
$alert = $Strings->get("account terminated", false);
break;
case "CHANGE_PASSWORD":
$alert = lang("password expired", false);
$alert = $Strings->get("password expired", false);
break;
case "NORMAL":
$userpass_ok = true;
$username_ok = true;
break;
case "ALERT_ON_ACCESS":
sendLoginAlertEmail($VARS['username']);
$userpass_ok = true;
$mail_resp = $user->sendAlertEmail();
if (DEBUG) {
var_dump($mail_resp);
}
$username_ok = true;
break;
default:
if (!is_empty($error)) {
$alert = $error;
} else {
$alert = $Strings->get("login error", false);
}
break;
}
if ($userpass_ok) {
if ($username_ok) {
if ($user->checkPassword($VARS['password'])) {
$_SESSION['passok'] = true; // stop logins using only username and authcode
if (userHasTOTP($VARS['username'])) {
if ($user->has2fa()) {
$multiauth = true;
} else {
doLoginUser($VARS['username'], $VARS['password']);
Session::start($user);
header('Location: app.php');
die("Logged in, go to app.php");
}
}
} else {
if (!is_empty($errmsg)) {
$alert = lang2("login server error", ['arg' => $errmsg], false);
} else {
$alert = lang("login incorrect", false);
$alert = $Strings->get("login incorrect", false);
}
}
} else { // User does not exist anywhere
$alert = $Strings->get("login incorrect", false);
}
} else {
$alert = lang("captcha error", false);
$alert = $Strings->get("captcha error", false);
}
} else if (!empty($VARS['progress']) && $VARS['progress'] == "2") {
} else if ($VARS['progress'] == "2") {
$user = User::byUsername($VARS['username']);
if ($_SESSION['passok'] !== true) {
// stop logins using only username and authcode
sendError("Password integrity check failed!");
}
if (verifyTOTP($VARS['username'], $VARS['authcode'])) {
if (doLoginUser($VARS['username'])) {
if ($user->check2fa($VARS['authcode'])) {
Session::start($user);
header('Location: app.php');
die("Logged in, go to app.php");
} else {
$alert = lang("login server user data error", false);
}
} else {
$alert = lang("2fa incorrect", false);
$alert = $Strings->get("2fa incorrect", false);
}
}
} else {
$alert = lang("login server unavailable", false);
$alert = $Strings->get("login server unavailable", false);
}
header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
@ -114,7 +125,7 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
<div class="row justify-content-center">
<div class="card col-11 col-xs-11 col-sm-8 col-md-6 col-lg-4">
<div class="card-body">
<h5 class="card-title"><?php lang("sign in"); ?></h5>
<h5 class="card-title"><?php $Strings->get("sign in"); ?></h5>
<form action="" method="POST">
<?php
if (!empty($alert)) {
@ -127,8 +138,8 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
if ($multiauth != true) {
?>
<input type="text" class="form-control" name="username" placeholder="<?php lang("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php lang("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<input type="text" class="form-control" name="username" placeholder="<?php $Strings->get("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<?php if (CAPTCHA_ENABLED) { ?>
<div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
<br />
@ -138,16 +149,16 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
} else if ($multiauth) {
?>
<div class="alert alert-info">
<?php lang("2fa prompt"); ?>
<?php $Strings->get("2fa prompt"); ?>
</div>
<input type="text" class="form-control" name="authcode" placeholder="<?php lang("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="hidden" name="progress" value="2" />
<input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
<?php
}
?>
<button type="submit" class="btn btn-primary">
<?php lang("continue"); ?>
<?php $Strings->get("continue"); ?>
</button>
</form>
</div>

@ -1,86 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
define("STRINGS", [
"sign in" => "Sign In",
"username" => "Username",
"password" => "Password",
"continue" => "Continue",
"authcode" => "Authentication code",
"2fa prompt" => "Enter the six-digit code from your mobile authenticator app.",
"2fa incorrect" => "Authentication code incorrect.",
"login incorrect" => "Login incorrect.",
"login server unavailable" => "Login server unavailable. Try again later or contact technical support.",
"account locked" => "This account has been disabled. Contact technical support.",
"password expired" => "You must change your password before continuing.",
"account terminated" => "Account terminated. Access denied.",
"account state error" => "Your account state is not stable. Log out, restart your browser, and try again.",
"captcha error" => "There was a problem with the CAPTCHA (robot test). Try again.",
"welcome user" => "Welcome, {user}!",
"sign out" => "Sign out",
"settings" => "Settings",
"options" => "Options",
"404 error" => "404 Error",
"no permission" => "You do not have permission to access this system.",
"page not found" => "Page not found.",
"invalid parameters" => "Invalid request parameters.",
"login server error" => "The login server returned an error: {arg}",
"login server user data error" => "The login server refused to provide account information. Try again or contact technical support.",
"captcha error" => "There was a problem with the CAPTCHA (robot test). Try again.",
"no access permission" => "You do not have permission to access this system.",
"no messages" => "No messages found.",
"all caught up" => "You're all caught up!",
"home" => "Home",
"more" => "More",
"my tasks" => "My Tasks",
"message board" => "Message Board",
"messages" => "Messages",
"all users" => "All Users",
"delete message" => "Delete Message",
"delete task" => "Delete Task",
"send" => "Send",
"from user" => "From {user}",
"to user" => "To {user}",
"pending" => "Pending",
"started" => "Started",
"finished" => "Finished",
"paused" => "Paused",
"problem" => "Problem",
"assigned on" => "Assigned: {date}",
"due by" => "Due by: {date}",
"actions" => "Actions",
"start" => "Start",
"finish" => "Finish",
"pause" => "Pause",
"resume" => "Continue",
"no description" => "No description.",
"no assigned date" => "No assigned date",
"no due date" => "No due date",
"no tasks" => "There aren't any tasks to show.",
"new task" => "New Task",
"edit task" => "Edit Task",
"task manager" => "Task Manager",
"task title" => "Task Title",
"task description" => "Task Description",
"assigned to" => "Assigned to",
"nobody" => "Nobody",
"save task" => "Save Task",
"exit" => "Exit",
"use now tip" => "Tip: You can type \"now\" to use the current date and time. Also try \"tomorrow\" or \"next friday\".",
"assigned on 2" => "Assigned on",
"due by 2" => "Due by",
"task saved" => "Task saved successfully.",
"message" => "Message",
"send message" => "Send message",
"to" => "To",
"user not managed" => "You are not a manager of the selected user, and cannot assign a task to them.",
"task edit not allowed" => "You are not allowed to edit that task.",
"task delete not allowed" => "You are not allowed to delete that task.",
"task deleted" => "Task deleted.",
"finished on" => "Finished on: {date}",
"started on" => "Started on: {date}",
"add task" => "Add Task"
]);

@ -0,0 +1,26 @@
{
"sign in": "Sign In",
"username": "Username",
"password": "Password",
"continue": "Continue",
"authcode": "Authentication code",
"2fa prompt": "Enter the six-digit code from your mobile authenticator app.",
"2fa incorrect": "Authentication code incorrect.",
"login incorrect": "Login incorrect.",
"login server unavailable": "Login server unavailable. Try again later or contact technical support.",
"account locked": "This account has been disabled. Contact technical support.",
"password expired": "You must change your password before continuing.",
"account terminated": "Account terminated. Access denied.",
"account state error": "Your account state is not stable. Log out, restart your browser, and try again.",
"welcome user": "Welcome, {user}!",
"sign out": "Sign out",
"settings": "Settings",
"options": "Options",
"404 error": "404 Error",
"page not found": "Page not found.",
"invalid parameters": "Invalid request parameters.",
"login server error": "The login server returned an error: {arg}",
"login server user data error": "The login server refused to provide account information. Try again or contact technical support.",
"captcha error": "There was a problem with the CAPTCHA (robot test). Try again.",
"no access permission": "You do not have permission to access this system."
}

@ -0,0 +1,13 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
class IncorrectPasswordException extends Exception {
public function __construct(string $message = "Incorrect password.", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}

@ -0,0 +1,135 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
class IPUtils {
/**
* Check if a given ipv4 address is in a given cidr
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
* @author Thorsten Ott <https://gist.github.com/tott/7684443>
*/
public static function ip4_in_cidr($ip, $cidr) {
if (strpos($cidr, '/') == false) {
$cidr .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $cidr, $netmask ) = explode('/', $cidr, 2);
$range_decimal = ip2long($cidr);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, ( 32 - $netmask)) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
/**
* Check if a given ipv6 address is in a given cidr
* @param string $ip IP to check in IPV6 format
* @param string $cidr CIDR netmask
* @return boolean true if the IP is in this range, false otherwise.
* @author MW. <https://stackoverflow.com/a/7952169>
*/
public static function ip6_in_cidr($ip, $cidr) {
$address = inet_pton($ip);
$subnetAddress = inet_pton(explode("/", $cidr)[0]);
$subnetMask = explode("/", $cidr)[1];
$addr = str_repeat("f", $subnetMask / 4);
switch ($subnetMask % 4) {
case 0:
break;
case 1:
$addr .= "8";
break;
case 2:
$addr .= "c";
break;
case 3:
$addr .= "e";
break;
}
$addr = str_pad($addr, 32, '0');
$addr = pack("H*", $addr);
$binMask = $addr;
return ($address & $binMask) == $subnetAddress;
}
/**
* Check if the REMOTE_ADDR is on Cloudflare's network.
* @return boolean true if it is, otherwise false
*/
public static function validateCloudflare() {
if (filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Using IPv6
$cloudflare_ips_v6 = [
"2400:cb00::/32",
"2405:8100::/32",
"2405:b500::/32",
"2606:4700::/32",
"2803:f800::/32",
"2c0f:f248::/32",
"2a06:98c0::/29"
];
$valid = false;
foreach ($cloudflare_ips_v6 as $cidr) {
if (ip6_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
} else {
// Using IPv4
$cloudflare_ips_v4 = [
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"104.16.0.0/12",
"108.162.192.0/18",
"131.0.72.0/22",
"141.101.64.0/18",
"162.158.0.0/15",
"172.64.0.0/13",
"173.245.48.0/20",
"188.114.96.0/20",
"190.93.240.0/20",
"197.234.240.0/22",
"198.41.128.0/17"
];
$valid = false;
foreach ($cloudflare_ips_v4 as $cidr) {
if (ip4_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
}
return $valid;
}
/**
* Makes a good guess at the client's real IP address.
*
* @return string Client IP or `0.0.0.0` if we can't find anything
*/
public static function getClientIP() {
// If CloudFlare is in the mix, we should use it.
// Check if the request is actually from CloudFlare before trusting it.
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
if (validateCloudflare()) {
return $_SERVER["HTTP_CF_CONNECTING_IP"];
}
}
if (isset($_SERVER["REMOTE_ADDR"])) {
return $_SERVER["REMOTE_ADDR"];
}
return "0.0.0.0"; // This will not happen unless we aren't a web server
}
}

@ -0,0 +1,129 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
class Login {
const BAD_USERPASS = 1;
const BAD_2FA = 2;
const ACCOUNT_DISABLED = 3;
const LOGIN_OK = 4;
public static function auth(string $username, string $password, string $twofa = ""): int {
global $database;
$username = strtolower($username);
$user = User::byUsername($username);
if (!$user->exists()) {
return Login::BAD_USERPASS;
}
if (!$user->checkPassword($password)) {
return Login::BAD_USERPASS;
}
if ($user->has2fa()) {
if (!$user->check2fa($twofa)) {
return Login::BAD_2FA;
}
}
switch ($user->getStatus()->get()) {
case AccountStatus::TERMINATED:
return Login::BAD_USERPASS;
case AccountStatus::LOCKED_OR_DISABLED:
return Login::ACCOUNT_DISABLED;
case AccountStatus::NORMAL:
default:
return Login::LOGIN_OK;
}
return Login::LOGIN_OK;
}
public static function verifyCaptcha(string $session, string $answer, string $url): bool {
$data = [
'session_id' => $session,
'answer_id' => $answer,
'action' => "verify"
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data)
]
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$resp = json_decode($result, TRUE);
if (!$resp['result']) {
return false;
} else {
return true;
}
}
/**
* Check the login server API for sanity
* @return boolean true if OK, else false
*/
public static function checkLoginServer() {
try {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "ping"
]
]);
if ($response->getStatusCode() != 200) {
return false;
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {
return false;
}
} catch (Exception $e) {
return false;
}
}
/**
* Checks if the given AccountHub API key is valid by attempting to
* access the API with it.
* @param String $key The API key to check
* @return boolean TRUE if the key is valid, FALSE if invalid or something went wrong
*/
function checkAPIKey($key) {
try {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => $key,
'action' => "ping"
]
]);
if ($response->getStatusCode() === 200) {
return true;
}
return false;
} catch (Exception $e) {
return false;
}
}
}

@ -0,0 +1,65 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
class Notifications {
/**
* Add a new notification.
* @global $database
* @param User $user
* @param string $title
* @param string $content
* @param string $timestamp If left empty, the current date and time will be used.
* @param string $url
* @param bool $sensitive If true, the notification is marked as containing sensitive content, and the $content might be hidden on lockscreens and other non-secure places.
* @return int The newly-created notification ID.
* @throws Exception
*/
public static function add(User $user, string $title, string $content, string $timestamp = "", string $url = "", bool $sensitive = false): int {
global $Strings;
if ($user->exists()) {
if (empty($title) || empty($content)) {
throw new Exception($Strings->get("invalid parameters", false));
}
$timestamp = date("Y-m-d H:i:s");
if (!empty($timestamp)) {
$timestamp = date("Y-m-d H:i:s", strtotime($timestamp));
}
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "addnotification",
'uid' => $user->getUID(),
'title' => $title,
'content' => $content,
'timestamp' => $timestamp,
'url' => $url,
'sensitive' => $sensitive
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['id'] * 1;
} else {
return false;
}
}
throw new Exception($Strings->get("user does not exist", false));
}
}

@ -0,0 +1,19 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
class Session {
public static function start(User $user) {
$_SESSION['username'] = $user->getUsername();
$_SESSION['uid'] = $user->getUID();
$_SESSION['email'] = $user->getEmail();
$_SESSION['realname'] = $user->getName();
$_SESSION['loggedin'] = true;
}
}

@ -0,0 +1,118 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/**
* Provides translated language strings.
*/
class Strings {
private $language = "en";
private $strings = [];
public function __construct($language = "en") {
if (!preg_match("/[a-zA-Z\_\-]+/", $language)) {
throw new Exception("Invalid language code $language");
}
$this->load("en");
if (file_exists(__DIR__ . "/../langs/$language/")) {
$this->language = $language;
$this->load($language);
} else {
trigger_error("Language $language could not be found.", E_USER_WARNING);
}
}
/**
* Load all JSON files for the specified language.
* @param string $language
*/
private function load(string $language) {
$files = glob(__DIR__ . "/../langs/$language/*.json");
foreach ($files as $file) {
$strings = json_decode(file_get_contents($file), true);
foreach ($strings as $key => $val) {
if (array_key_exists($key, $this->strings)) {
trigger_error("Language key \"$key\" is defined more than once.", E_USER_WARNING);
}
$this->strings[$key] = $val;
}
}
}
/**
* Add language strings dynamically.
* @param array $strings ["key" => "value", ...]
*/
public function addStrings(array $strings) {
foreach ($strings as $key => $val) {
$this->strings[$key] = $val;
}
}
/**
* I18N string getter. If the key isn't found, it outputs the key itself.
* @param string $key
* @param bool $echo True to echo the result, false to return it. Default is true.
* @return string
*/
public function get(string $key, bool $echo = true): string {
$str = $key;
if (array_key_exists($key, $this->strings)) {
$str = $this->strings[$key];
} else {
trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING);
}
if ($echo) {
echo $str;
}
return $str;
}
/**
* I18N string getter (with builder). If the key doesn't exist, outputs the key itself.
* @param string $key
* @param array $replace key-value array of replacements.
* If the string value is "hello {abc}" and you give ["abc" => "123"], the
* result will be "hello 123".
* @param bool $echo True to echo the result, false to return it. Default is true.
* @return string
*/
public function build(string $key, array $replace, bool $echo = true): string {
$str = $key;
if (array_key_exists($key, $this->strings)) {
$str = $this->strings[$key];
} else {
trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING);
}
foreach ($replace as $find => $repl) {
$str = str_replace("{" . $find . "}", $repl, $str);
}
if ($echo) {
echo $str;
}
return $str;
}
/**
* Builds and returns a JSON key:value string for the supplied array of keys.
* @param array $keys ["key1", "key2", ...]
*/
public function getJSON(array $keys): string {
$strings = [];
foreach ($keys as $k) {
$strings[$k] = $this->get($k, false);
}
return json_encode($strings);
}
}

@ -0,0 +1,441 @@
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
class User {
private $uid = null;
private $username;
private $email;
private $realname;
private $has2fa = false;
private $exists = false;
public function __construct(int $uid, string $username = "") {
// Check if user exists
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userexists",
'uid' => $uid
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK" && $resp['exists'] === true) {
$this->exists = true;
} else {
$this->uid = $uid;
$this->username = $username;
$this->exists = false;
}
if ($this->exists) {
// Get user info
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userinfo",
'uid' => $uid
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
$this->uid = $resp['data']['uid'] * 1;
$this->username = $resp['data']['username'];
$this->email = $resp['data']['email'];
$this->realname = $resp['data']['name'];
} else {
sendError("Login server error: " . $resp['msg']);
}
}
}
public static function byUsername(string $username): User {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'username' => $username,
'action' => "userinfo"
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if (!isset($resp['status'])) {
sendError("Login server error: " . $resp);
}
if ($resp['status'] == "OK") {
return new self($resp['data']['uid'] * 1);
} else {
return new self(-1, $username);
}
}
public function exists(): bool {
return $this->exists;
}
public function has2fa(): bool {
if (!$this->exists) {
return false;
}
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "hastotp",
'username' => $this->username
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['otp'] == true;
} else {
return false;
}
}
function getUsername() {
return $this->username;
}
function getUID() {
return $this->uid;
}
function getEmail() {
return $this->email;
}
function getName() {
return $this->realname;
}
/**
* Check the given plaintext password against the stored hash.
* @param string $password
* @return bool
*/
function checkPassword(string $password): bool {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "auth",
'username' => $this->username,
'password' => $password
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {
return false;
}
}
function check2fa(string $code): bool {
if (!$this->has2fa) {
return true;
}
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "verifytotp",
'username' => $this->username,
'code' => $code
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['valid'];
} else {
return false;
}
}
/**
* Check if the given username has the given permission (or admin access)
* @global $database $database
* @param string $code
* @return boolean TRUE if the user has the permission (or admin access), else FALSE
*/
function hasPermission(string $code): bool {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "permission",
'username' => $this->username,
'code' => $code
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['has_permission'];
} else {
return false;
}
}
/**
* Get the account status.
* @return \AccountStatus
*/
function getStatus(): AccountStatus {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "acctstatus",
'username' => $this->username
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return AccountStatus::fromString($resp['account']);
} else {
return null;
}
}
function sendAlertEmail(string $appname = SITE_TITLE) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "alertemail",
'username' => $this->username,
'appname' => SITE_TITLE
]
]);
if ($response->getStatusCode() > 299) {
return "An unknown error occurred.";
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {
return $resp['msg'];
}
}
/**
* Check if this user is the manager of the supplied user.
* @param \User $user Employee UID
* @return boolean
*/
function isManagerOf($user): boolean {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "ismanagerof",
'manager' => $this->uid,
'employee' => $user->getUID(),
'uid' => 1
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['managerof'] === true;
} else {
// this shouldn't happen, but in case it does just fake it.
return false;
}
}
/**
* Get an array of Users this user is a manager of.
* @return array
*/
function getManagedUsers(): array {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "getmanaged",
'uid' => $this->uid
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
$users = [];
if ($resp['status'] == "OK") {
foreach ($resp['employees'] as $e) {
$users[] = new User($e);
}
}
return $users;
}
/**
* Get a list of the groups the user is a member of, as {['id':1,'name':"abc"],...}
*/
function getGroups($uid) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "getgroupsbyuser",
'uid' => $this->uid
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['groups'];
} else {
return [];
}
}
}
class AccountStatus {
const NORMAL = 1;
const LOCKED_OR_DISABLED = 2;
const CHANGE_PASSWORD = 3;
const TERMINATED = 4;
const ALERT_ON_ACCESS = 5;
private $status;
public function __construct(int $status) {
$this->status = $status;
}
public static function fromString(string $status): AccountStatus {
switch ($status) {
case "NORMAL":
return new self(self::NORMAL);
case "LOCKED_OR_DISABLED":
return new self(self::LOCKED_OR_DISABLED);
case "CHANGE_PASSWORD":
return new self(self::CHANGE_PASSWORD);
case "TERMINATED":
return new self(self::TERMINATED);
case "ALERT_ON_ACCESS":
return new self(self::ALERT_ON_ACCESS);
default:
return new self(0);
}
}
/**
* Get the account status/state as an integer.
* @return int
*/
public function get(): int {
return $this->status;
}
/**
* Get the account status/state as a string representation.
* @return string
*/
public function getString(): string {
switch ($this->status) {
case self::NORMAL:
return "NORMAL";
case self::LOCKED_OR_DISABLED:
return "LOCKED_OR_DISABLED";
case self::CHANGE_PASSWORD:
return "CHANGE_PASSWORD";
case self::TERMINATED:
return "TERMINATED";
case self::ALERT_ON_ACCESS:
return "ALERT_ON_ACCESS";
default:
return "OTHER_" . $this->status;
}
}
}

@ -7,8 +7,6 @@ require_once __DIR__ . "/../required.php";
redirectifnotloggedin();
require_once __DIR__ . "/userinfo.php";
$messages = $database->select(
'messages', [
'messageid (id)',
@ -37,14 +35,14 @@ if (count($messages) > 0) {
foreach ($messages as $msg) {
$to = null;
if (!isset($usercache[$msg['from']])) {
$usercache[$msg['from']] = getUserByID($msg['from']);
$usercache[$msg['from']] = new User($msg['from']);
}
if (is_null($msg['to'])) {
$to['name'] = lang("all users", false);
$to['username'] = lang("all users", false);
$to['name'] = $Strings->get("all users", false);
$to['username'] = $Strings->get("all users", false);
} else {
if (!isset($usercache[$msg['to']])) {
$usercache[$msg['to']] = getUserByID($msg['to']);
$usercache[$msg['to']] = new User($msg['to']);
}
$to = $usercache[$msg['to']];
}
@ -52,13 +50,13 @@ if (count($messages) > 0) {
<div class="list-group-item">
<div class="text-muted">
<i class="fas fa-user fa-fw"></i>
<span data-toggle="tooltip" title="<?php lang2("from user", ["user" => $usercache[$msg['from']]['username']]) ?>">
<?php echo $usercache[$msg['from']]['name']; ?>
<span data-toggle="tooltip" title="<?php $Strings->build("from user", ["user" => $usercache[$msg['from']]->getUsername()]) ?>">
<?php echo $usercache[$msg['from']]->getName(); ?>
</span>
<div class="float-right float-md-none d-inline">
<i class="fas fa-caret-right fa-fw"></i>
<span data-toggle="tooltip" title="<?php lang2("to user", ["user" => $to['username']]) ?>">
<?php echo $to['name']; ?>
<span data-toggle="tooltip" title="<?php $Strings->build("to user", ["user" => $to->getUsername()]) ?>">
<?php echo $to->getName(); ?>
</span>
</div>
</div>
@ -77,6 +75,6 @@ if (count($messages) > 0) {
<?php
}
} else {
echo "<div class='alert alert-info'><i class='far fa-comment-alt'></i> " . lang("no messages", false) . "</div>";
echo "<div class='alert alert-info'><i class='far fa-comment-alt'></i> " . $Strings->get("no messages", false) . "</div>";
}
?>

@ -7,9 +7,12 @@ require_once __DIR__ . "/../required.php";
redirectifnotloggedin();
require_once __DIR__ . "/userinfo.php";
$managed_users = (new User($_SESSION['uid']))->getManagedUsers();
$managed_uids = getManagedUIDs($_SESSION['uid']);
$managed_uids = [];
foreach ($managed_users as $u) {
$managed_uids[] = $u->getUID();
}
// There needs to be at least one entry otherwise the SQL query craps itself
if (count($managed_uids) < 1) {
@ -83,9 +86,9 @@ if (count($tasks) > 0) {
if (!is_null($task['userid'])) {
echo "<span class='float-right text-muted small ml-1 mt-1'>";
if (!isset($usercache[$task['userid']])) {
$usercache[$task['userid']] = getUserByID($task['userid']);
$usercache[$task['userid']] = (new User($task['userid']));
}
echo "<i class='fas fa-user fa-fw'></i> " . $usercache[$task['userid']]['name'];
echo "<i class='fas fa-user fa-fw'></i> " . $usercache[$task['userid']]->getName();
echo "</span>";
}
?>
@ -96,16 +99,16 @@ if (count($tasks) > 0) {
<div class='row'>
<div class='col-12 col-sm-8 list-group'>
<div class="list-group-item">
<i class='fas fa-hourglass-start fa-fw'></i> <?php lang2("assigned on", ["date" => ($task['assigned'] > 0 ? date("M j, g:i a", strtotime($task['assigned'])) : lang("no assigned date", false))]) ?>
<i class='fas fa-hourglass-start fa-fw'></i> <?php $Strings->build("assigned on", ["date" => ($task['assigned'] > 0 ? date("M j, g:i a", strtotime($task['assigned'])) : $Strings->get("no assigned date", false))]) ?>
</div>
<div class="list-group-item">
<i class='fas fa-hourglass-end fa-fw'></i> <?php lang2("due by", ["date" => ($task['due'] > 0 ? date("M j, g:i a", strtotime($task['due'])) : lang("no due date", false))]) ?>
<i class='fas fa-hourglass-end fa-fw'></i> <?php $Strings->build("due by", ["date" => ($task['due'] > 0 ? date("M j, g:i a", strtotime($task['due'])) : $Strings->get("no due date", false))]) ?>
</div>
<?php
if ($task['statusid'] > 0) {
?>
<div class="list-group-item">
<i class='fas fa-play fa-fw'></i> <?php lang2("started on", ["date" => date("M j, g:i a", strtotime($task['starttime']))]) ?>
<i class='fas fa-play fa-fw'></i> <?php $Strings->build("started on", ["date" => date("M j, g:i a", strtotime($task['starttime']))]) ?>
</div>
<?php
}
@ -114,7 +117,7 @@ if (count($tasks) > 0) {
if ($task['statusid'] == 2) {
?>
<div class="list-group-item">
<i class='fas fa-stop fa-fw'></i> <?php lang2("finished on", ["date" => date("M j, g:i a", strtotime($task['endtime']))]) ?>
<i class='fas fa-stop fa-fw'></i> <?php $Strings->build("finished on", ["date" => date("M j, g:i a", strtotime($task['endtime']))]) ?>
</div>
<?php
}
@ -125,7 +128,7 @@ if (count($tasks) > 0) {
<form action='app.php?page=edittask' method='GET' class='bin-btn'>
<input type='hidden' name='page' value='edittask' />
<input type='hidden' name='taskid' value='<?php echo $task['taskid'] ?>' />
<button type='submit' class='btn btn-sm btn-primary' data-toggle="tooltip" data-placement="auto left" title="<?php lang("edit task") ?>"><i class='fas fa-edit fa-fw'></i> <?php lang("edit"); ?></button>
<button type='submit' class='btn btn-sm btn-primary' data-toggle="tooltip" data-placement="auto left" title="<?php $Strings->get("edit task") ?>"><i class='fas fa-edit fa-fw'></i> <?php $Strings->get("edit"); ?></button>
</form>
<form
action='action.php'
@ -141,7 +144,7 @@ if (count($tasks) > 0) {
<?php
}
?>
<button type='submit' id='deltaskbtn<?php echo $task['taskid'] ?>' class='btn btn-sm btn-danger' data-toggle="tooltip" data-placement="auto left" title="<?php lang("delete task") ?>"><i class='fas fa-trash fa-fw'></i> <?php lang("delete"); ?></button>
<button type='submit' id='deltaskbtn<?php echo $task['taskid'] ?>' class='btn btn-sm btn-danger' data-toggle="tooltip" data-placement="auto left" title="<?php $Strings->get("delete task") ?>"><i class='fas fa-trash fa-fw'></i> <?php $Strings->get("delete"); ?></button>
</form>
</div>
</div>
@ -157,7 +160,7 @@ if (count($tasks) > 0) {
if (isset($_GET['alone']) || (isset($pageid) && $pageid != "home")) {
echo "<div class=\"col-12 col-md-6\">";
}
echo "<div class='alert alert-info'><i class='fas fa-info-circle'></i> " . lang("no tasks", false) . "</div>";
echo "<div class='alert alert-info'><i class='fas fa-info-circle'></i> " . $Strings->get("no tasks", false) . "</div>";
if (isset($_GET['alone']) || (isset($pageid) && $pageid != "home")) {
echo "</div>";
}

@ -48,7 +48,7 @@ if (count($tasks) > 0) {
class='bin-btn'>
<input type='hidden' name='taskid' value='" . $task['taskid'] . "' />
<input type='hidden' name='action' value='start' />
<button type='submit' id='starttaskbtn" . $task['taskid'] . "' class='btn btn-primary'><i class='fas fa-fw fa-play'></i> " . lang("start", false) . "</button>
<button type='submit' id='starttaskbtn" . $task['taskid'] . "' class='btn btn-primary'><i class='fas fa-fw fa-play'></i> " . $Strings->get("start", false) . "</button>
</form>";
} else if ($task['statusid'] == 1) {
$btns = "<form
@ -59,7 +59,7 @@ if (count($tasks) > 0) {
class='bin-btn'>
<input type='hidden' name='taskid' value='" . $task['taskid'] . "' />
<input type='hidden' name='action' value='finish' />
<button type='submit' id='finishtaskbtn" . $task['taskid'] . "' class='btn btn-success'><i class='fas fa-fw fa-stop'></i> " . lang("finish", false) . "</button>
<button type='submit' id='finishtaskbtn" . $task['taskid'] . "' class='btn btn-success'><i class='fas fa-fw fa-stop'></i> " . $Strings->get("finish", false) . "</button>
</form>";
$btns .= "<form
action='action.php'
@ -69,7 +69,7 @@ if (count($tasks) > 0) {
class='bin-btn'>
<input type='hidden' name='taskid' value='" . $task['taskid'] . "' />
<input type='hidden' name='action' value='pause' />
<button type='submit' id='pausetaskbtn" . $task['taskid'] . "' class='btn btn-warning'><i class='fas fa-fw fa-pause'></i> " . lang("pause", false) . "</button>
<button type='submit' id='pausetaskbtn" . $task['taskid'] . "' class='btn btn-warning'><i class='fas fa-fw fa-pause'></i> " . $Strings->get("pause", false) . "</button>
</form>";
$btns .= "<form
action='action.php'
@ -79,7 +79,7 @@ if (count($tasks) > 0) {
class='bin-btn'>
<input type='hidden' name='taskid' value='" . $task['taskid'] . "' />
<input type='hidden' name='action' value='problem' />
<button type='submit' id='problemtaskbtn" . $task['taskid'] . "' class='btn btn-danger'><i class='fas fa-fw fa-exclamation'></i> " . lang("problem", false) . "</button>
<button type='submit' id='problemtaskbtn" . $task['taskid'] . "' class='btn btn-danger'><i class='fas fa-fw fa-exclamation'></i> " . $Strings->get("problem", false) . "</button>
</form>";
} else if ($task['statusid'] == 3 || $task['statusid'] == 4) {
$btns = "<form
@ -90,19 +90,19 @@ if (count($tasks) > 0) {
class='bin-btn'>
<input type='hidden' name='taskid' value='" . $task['taskid'] . "' />
<input type='hidden' name='action' value='resume' />
<button type='submit' id='resumetaskbtn" . $task['taskid'] . "' class='btn btn-primary'><i class='fas fa-fw fa-play'></i> " . lang("resume", false) . "</button>
<button type='submit' id='resumetaskbtn" . $task['taskid'] . "' class='btn btn-primary'><i class='fas fa-fw fa-play'></i> " . $Strings->get("resume", false) . "</button>
</form>";
}
$assignedon = "<i class='fas fa-hourglass-start fa-fw'></i> " . lang2("assigned on", ["date" => date("F j, Y, g:i a", strtotime($task['taskassignedon']))], false);
$dueby = "<i class='fas fa-hourglass-end fa-fw'></i> " . lang2("due by", ["date" => ($task['taskdueby'] > 0 ? date("F j, Y, g:i a", strtotime($task['taskdueby'])) : lang("no due date", false))], false);
$assignedon = "<i class='fas fa-hourglass-start fa-fw'></i> " . $Strings->build("assigned on", ["date" => date("F j, Y, g:i a", strtotime($task['taskassignedon']))], false);
$dueby = "<i class='fas fa-hourglass-end fa-fw'></i> " . $Strings->build("due by", ["date" => ($task['taskdueby'] > 0 ? date("F j, Y, g:i a", strtotime($task['taskdueby'])) : $Strings->get("no due date", false))], false);
$startedon = "";
if ($task['statusid'] > 0) {
$startedon = "<i class='fas fa-play fa-fw'></i> " . lang2("started on", ["date" => date("F j, Y, g:i a", strtotime($task['starttime']))], false);
$startedon = "<i class='fas fa-play fa-fw'></i> " . $Strings->build("started on", ["date" => date("F j, Y, g:i a", strtotime($task['starttime']))], false);
}
$finishedon = "";
if ($task['statusid'] == 2) {
$finishedon = "<i class='fas fa-stop fa-fw'></i> " . lang2("finished on", ["date" => date("F j, Y, g:i a", strtotime($task['endtime']))], false);
$finishedon = "<i class='fas fa-stop fa-fw'></i> " . $Strings->build("finished on", ["date" => date("F j, Y, g:i a", strtotime($task['endtime']))], false);
}
if ($home) {
?>
@ -179,7 +179,7 @@ if (count($tasks) > 0) {
if (!$home) {
echo "<div class=\"col-12 col-md-6\">";
}
echo "<div class='alert alert-success'><i class='fas fa-check'></i> " . lang("all caught up", false) . "</div>";
echo "<div class='alert alert-success'><i class='fas fa-check'></i> " . $Strings->get("all caught up", false) . "</div>";
if (!$home) {
echo "</div>";
}

@ -1,131 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Check if a given ipv4 address is in a given cidr
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
* @author Thorsten Ott <https://gist.github.com/tott/7684443>
*/
function ip4_in_cidr($ip, $cidr) {
if (strpos($cidr, '/') == false) {
$cidr .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $cidr, $netmask ) = explode('/', $cidr, 2);
$range_decimal = ip2long($cidr);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, ( 32 - $netmask)) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
/**
* Check if a given ipv6 address is in a given cidr
* @param string $ip IP to check in IPV6 format
* @param string $cidr CIDR netmask
* @return boolean true if the IP is in this range, false otherwise.
* @author MW. <https://stackoverflow.com/a/7952169>
*/
function ip6_in_cidr($ip, $cidr) {
$address = inet_pton($ip);
$subnetAddress = inet_pton(explode("/", $cidr)[0]);
$subnetMask = explode("/", $cidr)[1];
$addr = str_repeat("f", $subnetMask / 4);
switch ($subnetMask % 4) {
case 0:
break;
case 1:
$addr .= "8";
break;
case 2:
$addr .= "c";
break;
case 3:
$addr .= "e";
break;
}
$addr = str_pad($addr, 32, '0');
$addr = pack("H*", $addr);
$binMask = $addr;
return ($address & $binMask) == $subnetAddress;
}
/**
* Check if the REMOTE_ADDR is on Cloudflare's network.
* @return boolean true if it is, otherwise false
*/
function validateCloudflare() {
if (filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Using IPv6
$cloudflare_ips_v6 = [
"2400:cb00::/32",
"2405:8100::/32",
"2405:b500::/32",
"2606:4700::/32",
"2803:f800::/32",
"2c0f:f248::/32",
"2a06:98c0::/29"
];
$valid = false;
foreach ($cloudflare_ips_v6 as $cidr) {
if (ip6_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
} else {
// Using IPv4
$cloudflare_ips_v4 = [
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"104.16.0.0/12",
"108.162.192.0/18",
"131.0.72.0/22",
"141.101.64.0/18",
"162.158.0.0/15",
"172.64.0.0/13",
"173.245.48.0/20",
"188.114.96.0/20",
"190.93.240.0/20",
"197.234.240.0/22",
"198.41.128.0/17"
];
$valid = false;
foreach ($cloudflare_ips_v4 as $cidr) {
if (ip4_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
}
return $valid;
}
/**
* Makes a good guess at the client's real IP address.
*
* @return string Client IP or `0.0.0.0` if we can't find anything
*/
function getClientIP() {
// If CloudFlare is in the mix, we should use it.
// Check if the request is actually from CloudFlare before trusting it.
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
if (validateCloudflare()) {
return $_SERVER["HTTP_CF_CONNECTING_IP"];
}
}
if (isset($_SERVER["REMOTE_ADDR"])) {
return $_SERVER["REMOTE_ADDR"];
}
return "0.0.0.0"; // This will not happen unless we aren't a web server
}

@ -1,403 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Authentication and account functions. Connects to an AccountHub instance.
*/
/**
* Check the login server API for sanity
* @return boolean true if OK, else false
*/
function checkLoginServer() {
try {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "ping"
]
]);
if ($response->getStatusCode() != 200) {
return false;
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {
return false;
}
} catch (Exception $e) {
return false;
}
}
/**
* Checks if the given AccountHub API key is valid by attempting to
* access the API with it.
* @param String $key The API key to check
* @return boolean TRUE if the key is valid, FALSE if invalid or something went wrong
*/
function checkAPIKey($key) {
try {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => $key,
'action' => "ping"
]
]);
if ($response->getStatusCode() === 200) {
return true;
}
return false;
} catch (Exception $e) {
return false;
}
}
////////////////////////////////////////////////////////////////////////////////
// Account handling //
////////////////////////////////////////////////////////////////////////////////
/**
* Checks the given credentials against the API.
* @param string $username
* @param string $password
* @return boolean True if OK, else false
*/
function authenticate_user($username, $password, &$errmsg) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "auth",
'username' => $username,
'password' => $password
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {
$errmsg = $resp['msg'];
return false;
}
}
/**
* Check if a username exists.
* @param String $username
*/
function user_exists($username) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userexists",
'username' => $username
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK" && $resp['exists'] === true) {
return true;
} else {
return false;
}
}
/**
* Check if a UID exists.
* @param String $uid
*/
function uid_exists($uid) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userexists",
'uid' => $uid
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK" && $resp['exists'] === true) {
return true;
} else {
return false;
}
}
/**
* Get the account status: NORMAL, TERMINATED, LOCKED_OR_DISABLED,
* CHANGE_PASSWORD, or ALERT_ON_ACCESS
* @param string $username
* @return string
*/
function get_account_status($username) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "acctstatus",
'username' => $username
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['account'];
} else {
return false;
}
}
/**
* Check if the given username has the given permission (or admin access)
* @param string $username
* @param string $permcode
* @return boolean TRUE if the user has the permission (or admin access), else FALSE
*/
function account_has_permission($username, $permcode) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "permission",
'username' => $username,
'code' => $permcode
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['has_permission'];
} else {
return false;
}
}
////////////////////////////////////////////////////////////////////////////////
// Login handling //
////////////////////////////////////////////////////////////////////////////////
/**
* Setup $_SESSION values with user data and set loggedin flag to true
* @param string $username
*/
function doLoginUser($username) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userinfo",
'username' => $username
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
$userinfo = $resp['data'];
session_regenerate_id(true);
$newSession = session_id();
session_write_close();
session_id($newSession);
session_start();
$_SESSION['username'] = $username;
$_SESSION['uid'] = $userinfo['uid'];
$_SESSION['email'] = $userinfo['email'];
$_SESSION['realname'] = $userinfo['name'];
$_SESSION['loggedin'] = true;
return true;
} else {
return false;
}
}
function sendLoginAlertEmail($username) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "alertemail",
'username' => $username,
'appname' => SITE_TITLE
]
]);
if ($response->getStatusCode() > 299) {
return "An unknown error occurred.";
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {
return $resp['msg'];
}
}
function simLogin($username, $password) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "login",
'username' => $username,
'password' => $password
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {
return $resp['msg'];
}
}
function verifyCaptcheck($session, $answer, $url) {
$data = [
'session_id' => $session,
'answer_id' => $answer,
'action' => "verify"
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data)
]
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$resp = json_decode($result, TRUE);
if (!$resp['result']) {
return false;
} else {
return true;
}
}
////////////////////////////////////////////////////////////////////////////////
// 2-factor authentication //
////////////////////////////////////////////////////////////////////////////////
/**
* Check if a user has TOTP setup
* @param string $username
* @return boolean true if TOTP secret exists, else false
*/
function userHasTOTP($username) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "hastotp",
'username' => $username
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['otp'];
} else {
return false;
}
}
/**
* Verify a TOTP multiauth code
* @global $database
* @param string $username
* @param int $code
* @return boolean true if it's legit, else false
*/
function verifyTOTP($username, $code) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "verifytotp",
'username' => $username,
'code' => $code
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['valid'];
} else {
return false;
}
}

@ -1,127 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Get user info for the given username.
* @param int $u username
* @return [string] Array of [uid, username, name]
*/
function getUserByUsername($u) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userinfo",
'username' => $u
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['data'];
} else {
// this shouldn't happen, but in case it does just fake it.
return ["name" => $u, "username" => $u, "uid" => $u];
}
}
/**
* Get user info for the given UID.
* @param int $u user ID
* @return [string] Array of [uid, username, name]
*/
function getUserByID($u) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "userinfo",
'uid' => $u
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['data'];
} else {
// this shouldn't happen, but in case it does just fake it.
return ["name" => $u, "username" => $u, "uid" => $u];
}
}
/**
* Check if the first UID is a manager of the second UID.
* @param int $m Manager UID
* @param int $e Employee UID
* @return boolean
*/
function isManagerOf($m, $e) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "ismanagerof",
'manager' => $m,
'employee' => $e,
'uid' => 1
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['managerof'] === true;
} else {
// this shouldn't happen, but in case it does just fake it.
return false;
}
}
/**
* Get an array of UIDs the given UID is a manager of.
* @param int $manageruid The UID of the manager to find employees for.
* @return [int]
*/
function getManagedUIDs($manageruid) {
$client = new GuzzleHttp\Client();
$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "getmanaged",
'uid' => $manageruid
]
]);
if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}
$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['employees'];
} else {
return [];
}
}

@ -14,8 +14,6 @@ $access_permission = "TASKFLOOR";
require __DIR__ . "/../required.php";
require __DIR__ . "/../lib/login.php";
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
@ -73,7 +71,7 @@ function mobile_valid($username, $code) {
}
if (mobile_enabled() !== TRUE) {
exit(json_encode(["status" => "ERROR", "msg" => lang("mobile login disabled", false)]));
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("mobile login disabled", false)]));
}
// Make sure we have a username and access key
@ -93,11 +91,12 @@ if (!mobile_valid($VARS['username'], $VARS['key'])) {
switch ($VARS['action']) {
case "start_session":
// Do a web login.
if (user_exists($VARS['username'])) {
if (get_account_status($VARS['username']) == "NORMAL") {
if (authenticate_user($VARS['username'], $VARS['password'], $autherror)) {
if (is_null($access_permission) || account_has_permission($VARS['username'], $access_permission)) {
doLoginUser($VARS['username'], $VARS['password']);
$user = User::byUsername($VARS['username']);
if ($user->exists()) {
if ($user->getStatus()->getString() == "NORMAL") {
if ($user->checkPassword($VARS['password'])) {
if (is_null($access_permission) || $user->hasPermission($access_permission)) {
Session::start($user);
$_SESSION['mobile'] = true;
exit(json_encode(["status" => "OK"]));
} else {
@ -106,7 +105,7 @@ switch ($VARS['action']) {
}
}
}
exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)]));
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
default:
http_response_code(404);
die(json_encode(["status" => "ERROR", "msg" => "The requested action is not available."]));

@ -5,6 +5,6 @@
?>
<div class="row justify-content-center">
<div class="col-12 col-sm-10 col-md-8 col-lg-6">
<div class="alert alert-warning"><b><?php lang("404 error");?></b><br /> <?php lang("page not found"); ?></div>
<div class="alert alert-warning"><b><?php $Strings->get("404 error");?></b><br /> <?php $Strings->get("page not found"); ?></div>
</div>
</div>

@ -11,21 +11,22 @@ redirectifnotloggedin();
<h3 class="card-header text-blue-grey">
<i class="fas fa-edit"></i> <?php
if (is_empty($VARS['taskid'])) {
lang("add task");
$Strings->get("add task");
} else {
lang("edit task");
$Strings->get("edit task");
}
?>
</h3>
<?php
include_once __DIR__ . "/../lib/userinfo.php";
include_once __DIR__ . "/../lib/manage.php";
if (!is_empty($VARS['taskid'])) {
$taskid = $VARS['taskid'];
$managed_uids = getManagedUIDs($_SESSION['uid']);
$managed_users = (new User($_SESSION['uid']))->getManagedUsers();
$managed_uids = [];
foreach ($managed_users as $u) {
$managed_uids[] = $u->getUID();
}
// There needs to be at least one entry otherwise the SQL query craps itself
if (count($managed_uids) < 1) {
$managed_uids = [-1];
@ -45,7 +46,7 @@ redirectifnotloggedin();
if (!$allowed) {
header("Location: app.php?page=edittask&msg=task_edit_not_allowed");
die(lang("task edit not allowed", false));
die($Strings->get("task edit not allowed", false));
}
}
@ -62,21 +63,21 @@ redirectifnotloggedin();
<div class="card-body">
<div class="row">
<div class="col-12 mb-3">
<?php lang("task title") ?>: <input type="text" name="tasktitle" placeholder="<?php lang("task title") ?>" required="required" class="form-control" value="<?php echo $task['tasktitle']; ?>"/>
<?php $Strings->get("task title") ?>: <input type="text" name="tasktitle" placeholder="<?php $Strings->get("task title") ?>" required="required" class="form-control" value="<?php echo $task['tasktitle']; ?>"/>
</div>
<div class="col-12 mb-3">
<?php lang("task description") ?>:<br />
<?php $Strings->get("task description") ?>:<br />
<textarea name="taskdesc" id="taskdesc" class="form-control"><?php echo $task['taskdesc']; ?></textarea>
</div>
<div class="col-12 col-lg-4 mb-3">
<?php lang("assigned to") ?>:
<input type="text" id="assigned-to-box" name="assignedto" class="form-control" autocomplete="off" value="<?php echo (is_null($tass['userid']) ? "" : getUserByID($tass['userid'])['username'] ); ?>" placeholder="<?php lang("nobody") ?>" />
<?php $Strings->get("assigned to") ?>:
<input type="text" id="assigned-to-box" name="assignedto" class="form-control" autocomplete="off" value="<?php echo (is_null($tass['userid']) ? "" : (new User($tass['userid']))->getUsername() ); ?>" placeholder="<?php $Strings->get("nobody") ?>" />
</div>
<div class="col-12 col-md-6 col-lg-4 mb-3">
<?php lang("assigned on 2") ?>: <input type="text" class="form-control" id="assigned-on-box" name="taskassignedon" data-toggle="datetimepicker" data-target="#assigned-on-box" value="<?php echo (is_empty($task['taskassignedon']) ? "" : date("D F j, Y g:i a"/* 'Y-m-d\TH:i' */, strtotime($task['taskassignedon']))); ?>" />
<?php $Strings->get("assigned on 2") ?>: <input type="text" class="form-control" id="assigned-on-box" name="taskassignedon" data-toggle="datetimepicker" data-target="#assigned-on-box" value="<?php echo (is_empty($task['taskassignedon']) ? "" : date("D F j, Y g:i a"/* 'Y-m-d\TH:i' */, strtotime($task['taskassignedon']))); ?>" />
</div>
<div class="col-12 col-md-6 col-lg-4">
<?php lang("due by 2") ?>: <input type="text" class="form-control" id="due-by-box" name="taskdueby" data-toggle="datetimepicker" data-target="#due-by-box" value="<?php echo (is_empty($task['taskdueby']) ? "" : date("D F j, Y g:i a", strtotime($task['taskdueby']))); ?>"/>
<?php $Strings->get("due by 2") ?>: <input type="text" class="form-control" id="due-by-box" name="taskdueby" data-toggle="datetimepicker" data-target="#due-by-box" value="<?php echo (is_empty($task['taskdueby']) ? "" : date("D F j, Y g:i a", strtotime($task['taskdueby']))); ?>"/>
</div>
</div>
</div>
@ -85,8 +86,8 @@ redirectifnotloggedin();
<input type="hidden" name="taskid" value="<?php echo $taskid; ?>" />
<?php } ?>
<div class="card-footer d-flex">
<button id="savebtn" type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php lang("save task") ?></button>
<a class="btn btn-warning" href="app.php?page=taskman"><i class="fas fa-times"></i> <?php lang("exit") ?></a>
<button id="savebtn" type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php $Strings->get("save task") ?></button>
<a class="btn btn-warning" href="app.php?page=taskman"><i class="fas fa-times"></i> <?php $Strings->get("exit") ?></a>
</div>
</form>
</div>

@ -12,7 +12,7 @@ redirectifnotloggedin();
<div class="card">
<div class="card-body p-1">
<h4 class="card-title p-3">
<i class="fas fa-th-list"></i> <?php lang("my tasks") ?>
<i class="fas fa-th-list"></i> <?php $Strings->get("my tasks") ?>
</h4>
<?php
include __DIR__ . '/mytasks.php';
@ -24,7 +24,7 @@ redirectifnotloggedin();
<div class="card">
<div class="card-body p-1">
<h4 class="card-title p-3">
<i class="fas fa-comments"></i> <?php lang("messages") ?>
<i class="fas fa-comments"></i> <?php $Strings->get("messages") ?>
</h4>
<?php
include __DIR__ . '/messages.php';

@ -10,10 +10,10 @@ redirectifnotloggedin();
<form action="action.php" method="POST" class="form-horizontal mb-1" id="msgsendform">
<input type="hidden" name="action" value="sendmsg" />
<div class="input-group" id="msgsenddiv"> <!--col-12 col-md-5 col-lg-4-->
<input type="text" id="msgsendbox" name="msg" class="form-control" placeholder="<?php lang("send message") ?>" autocomplete="off" />
<input type="text" id="msgtobox" name="to" class="form-control" placeholder="<?php lang("to") ?>" autocomplete="off" />
<input type="text" id="msgsendbox" name="msg" class="form-control" placeholder="<?php $Strings->get("send message") ?>" autocomplete="off" />
<input type="text" id="msgtobox" name="to" class="form-control" placeholder="<?php $Strings->get("to") ?>" autocomplete="off" />
<div class="input-group-append">
<button id="msgsendbtn" class="btn btn-primary btn-sm" type="submit"><i class="fas fa-paper-plane"></i> <?php lang("send") ?></button>
<button id="msgsendbtn" class="btn btn-primary btn-sm" type="submit"><i class="fas fa-paper-plane"></i> <?php $Strings->get("send") ?></button>
</div>
</div>
</form>

@ -9,7 +9,7 @@ redirectifnotloggedin();
?>
<div class="btn-toolbar mb-4">
<div class="btn-group mr-2 mb-2">
<a href="app.php?page=edittask" class="btn btn-success"><i class="fas fa-plus"></i> <?php lang("new task") ?></a>
<a href="app.php?page=edittask" class="btn btn-success"><i class="fas fa-plus"></i> <?php $Strings->get("new task") ?></a>
</div>
</div>
<div id="tasksdispdiv" class="row justify-content-center">

@ -61,9 +61,14 @@ if ($_SESSION['mobile'] === TRUE) {
require __DIR__ . '/vendor/autoload.php';
// List of alert messages
require __DIR__ . '/lang/messages.php';
// text strings (i18n)
require __DIR__ . '/lang/' . LANGUAGE . ".php";
require __DIR__ . '/langs/messages.php';
$libs = glob(__DIR__ . "/lib/*.lib.php");
foreach ($libs as $lib) {
require_once $lib;
}
$Strings = new Strings(LANGUAGE);
/**
* Kill off the running process and spit out an error message
@ -135,59 +140,13 @@ function is_empty($str) {
return (!isset($str) || is_null($str) || $str == '');
}
/**
* I18N string getter. If the key doesn't exist, outputs the key itself.
* @param string $key I18N string key
* @param boolean $echo whether to echo the result or return it (default echo)
*/
function lang($key, $echo = true) {
if (array_key_exists($key, STRINGS)) {
$str = STRINGS[$key];
} else {
trigger_error("Language key \"$key\" does not exist in " . LANGUAGE, E_USER_WARNING);
$str = $key;
}
if ($echo) {
echo $str;
} else {
return $str;
}
}
/**
* I18N string getter (with builder). If the key doesn't exist, outputs the key itself.
* @param string $key I18N string key
* @param array $replace key-value array of replacements.
* If the string value is "hello {abc}" and you give ["abc" => "123"], the
* result will be "hello 123".
* @param boolean $echo whether to echo the result or return it (default echo)
*/
function lang2($key, $replace, $echo = true) {
if (array_key_exists($key, STRINGS)) {
$str = STRINGS[$key];
} else {
trigger_error("Language key \"$key\" does not exist in " . LANGUAGE, E_USER_WARNING);
$str = $key;
}
foreach ($replace as $find => $repl) {
$str = str_replace("{" . $find . "}", $repl, $str);
}
if ($echo) {
echo $str;
} else {
return $str;
}
}
function dieifnotloggedin() {
if ($_SESSION['loggedin'] != true) {
sendError("Session expired. Please log out and log in again.");
}
require_once __DIR__ . "/lib/login.php";
if (account_has_permission($_SESSION['username'], "TASKFLOOR") == FALSE) {
if (!(new User($_SESSION['uid']))->hasPermission("TASKFLOOR")) {
die("You don't have permission to be here.");
}
}
@ -243,12 +202,11 @@ if (!function_exists('base_url')) {
function redirectIfNotLoggedIn() {
if ($_SESSION['loggedin'] !== TRUE) {
header('Location: ' . URL . '/index.php');
header('Location: ./index.php');
die();
}
require_once __DIR__ . "/lib/login.php";
if (account_has_permission($_SESSION['username'], "TASKFLOOR") == FALSE) {
header('Location: ./index.php');
if ((new User($_SESSION['uid']))->hasPermission("TASKFLOOR") == FALSE) {
header('Location: ./index.php?permissionerror');
die("You don't have permission to be here.");
}
}

File diff suppressed because one or more lines are too long

@ -1,5 +1,5 @@
/*!
* Font Awesome Free 5.1.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Font Awesome Free 5.3.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
.svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;transform:translate(-50%,-50%);transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;transform:scale(.25);transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;transform:scale(.25);transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;transform:scale(.25);transform-origin:top left}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:a 2s infinite linear}.fa-pulse{animation:a 1s infinite steps(8)}@keyframes a{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1em}.svg-inline--fa.fa-stack-2x{height:2em;width:2em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}
.svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;transform:translate(-50%,-50%);transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;transform:scale(.25);transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;transform:scale(.25);transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;transform:scale(.25);transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;transform:scale(.25);transform-origin:top left}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{animation:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1em}.svg-inline--fa.fa-stack-2x{height:2em;width:2em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save