Browse Source

Merge ../BusinessAppTemplate

# Conflicts:
#	README.md
#	api.php
#	composer.lock
#	index.php
#	lang/en_us.php
#	lib/login.php
#	required.php
#	settings.template.php
master
Skylar Ittner 11 months ago
parent
commit
47540e57d2
59 changed files with 1639 additions and 2079 deletions
  1. 1
    1
      LICENSE.md
  2. 4
    5
      README.md
  3. 23
    27
      action.php
  4. 2
    33
      api.php
  5. 5
    0
      api/.htaccess
  6. 9
    0
      api/actions/ping.php
  7. 15
    0
      api/apisettings.php
  8. 144
    0
      api/functions.php
  9. 79
    0
      api/index.php
  10. 40
    34
      app.php
  11. 12
    12
      composer.lock
  12. 112
    145
      index.php
  13. 0
    140
      lang/en_us.php
  14. 7
    0
      langs/en/core.json
  15. 8
    0
      langs/en/index.json
  16. 9
    0
      langs/en/titles.json
  17. 0
    0
      langs/messages.php
  18. 56
    0
      lib/AccountHubApi.lib.php
  19. 13
    0
      lib/Exceptions.lib.php
  20. 275
    0
      lib/FormBuilder.lib.php
  21. 135
    0
      lib/IPUtils.lib.php
  22. 80
    0
      lib/Login.lib.php
  23. 53
    0
      lib/Notifications.lib.php
  24. 19
    0
      lib/Session.lib.php
  25. 122
    0
      lib/Strings.lib.php
  26. 0
    18
      lib/authlog.php
  27. 1
    1
      lib/getlogtable.php
  28. 2
    2
      lib/getmanagetable.php
  29. 2
    2
      lib/getpermtable.php
  30. 0
    130
      lib/getusertable.php
  31. 0
    131
      lib/iputils.php
  32. 0
    402
      lib/login.php
  33. 44
    136
      lib/reports.php
  34. 0
    127
      lib/userinfo.php
  35. 19
    45
      mobile/index.php
  36. 6
    6
      pages.php
  37. 1
    1
      pages/404.php
  38. 12
    12
      pages/authlog.php
  39. 4
    4
      pages/clearlog.php
  40. 11
    19
      pages/deluser.php
  41. 34
    119
      pages/edituser.php
  42. 11
    11
      pages/export.php
  43. 25
    16
      pages/groups.php
  44. 6
    6
      pages/home.php
  45. 39
    14
      pages/managers.php
  46. 28
    39
      pages/permissions.php
  47. 59
    23
      pages/users.php
  48. 27
    119
      required.php
  49. 60
    50
      settings.template.php
  50. 3
    3
      static/css/bootstrap.min.css
  51. 0
    15
      static/css/index.css
  52. 3
    3
      static/css/svg-with-js.min.css
  53. 7
    0
      static/js/bootstrap.bundle.min.js
  54. 0
    7
      static/js/bootstrap.min.js
  55. 3
    3
      static/js/fontawesome-all.min.js
  56. 2
    47
      static/js/groups.js
  57. 2
    86
      static/js/managers.js
  58. 4
    52
      static/js/permissions.js
  59. 1
    33
      static/js/users.js

+ 1
- 1
LICENSE.md View File

@@ -1,4 +1,4 @@
Copyright (c) 2018 Netsyms Technologies.
Copyright (c) 2018-2019 Netsyms Technologies.

If you modify and redistribute this project, you must replace the branding
assets with your own.

+ 4
- 5
README.md View File

@@ -1,7 +1,7 @@
ManagePanel
===========

System administration tool. Manage user accounts, permissions, and other data
System administration tool. Manage user accounts, permissions, and other data
shared between the apps.

https://netsyms.biz/apps/managepanel
@@ -12,8 +12,7 @@ Installing
0. Follow the installation directions for [AccountHub](https://source.netsyms.com/Business/AccountHub), then download this app somewhere.
1. Copy `settings.template.php` to `settings.php`
2. Import `database.sql` into your database server
3. Edit `settings.php` and fill in your DB info ("DB_*" for the AccountHub database, "DB2_*" for the ManagePanel one you just installed)
4. Set the location of the AccountHub API in `settings.php` (see "PORTAL_API") and enter an API key ("PORTAL_KEY")
5. Set the location of the AccountHub home page ("PORTAL_URL")
6. Set the URL of this app ("URL")
3. Edit `settings.php` and fill in your DB info ("database" for the AccountHub database, "database2" for the ManagePanel one you just installed)
4. Set the location of the AccountHub API in `settings.php`, enter an API key, and set the home page
6. Set the URL of this app
7. Run `composer install` (or `composer.phar install`) to install dependency libraries.

+ 23
- 27
action.php View File

@@ -8,14 +8,12 @@
* 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/authlog.php";

if ($VARS['action'] !== "signout") {
dieifnotloggedin();
}

if (account_has_permission($_SESSION['username'], "ADMIN") == FALSE) {
if ((new User($_SESSION['uid']))->hasPermission("ADMIN") == FALSE) {
die("You don't have permission to be here.");
}

@@ -44,7 +42,7 @@ function returnToSender($msg, $arg = "", $additional = []) {

switch ($VARS['action']) {
case "edituser":
if (is_empty($VARS['id'])) {
if (empty($VARS['id'])) {
$insert = true;
} else {
if ($database->has('accounts', ['uid' => $VARS['id']])) {
@@ -53,7 +51,7 @@ switch ($VARS['action']) {
returnToSender("invalid_userid");
}
}
if (is_empty($VARS['name']) || is_empty($VARS['username']) || is_empty($VARS['status'])) {
if (empty($VARS['name']) || empty($VARS['username']) || empty($VARS['status'])) {
returnToSender('invalid_parameters');
}

@@ -69,7 +67,7 @@ switch ($VARS['action']) {
'deleted' => 0
];

if (!is_empty($VARS['pass'])) {
if (!empty($VARS['pass'])) {
$data['password'] = password_hash($VARS['pass'], PASSWORD_BCRYPT);
}

@@ -78,11 +76,11 @@ switch ($VARS['action']) {
$data['phone2'] = "";
$data['accttype'] = 1;
$database->insert('accounts', $data);
insertAuthLog(17, $_SESSION['uid'], $data['username'] . ", " . $data['realname'] . ", " . $data['email'] . ", " . $data['acctstatus']);
Log::insert(LogType::USER_ADDED, $_SESSION['uid'], $data['username'] . ", " . $data['realname'] . ", " . $data['email'] . ", " . $data['acctstatus']);
} else {
$olddata = $database->select('accounts', '*', ['uid' => $VARS['id']])[0];
$database->update('accounts', $data, ['uid' => $VARS['id']]);
insertAuthLog(18, $_SESSION['uid'], "OLD: " . $olddata['username'] . ", " . $olddata['realname'] . ", " . $olddata['email'] . ", " . $olddata['acctstatus'] . "; NEW: " . $data['username'] . ", " . $data['realname'] . ", " . $data['email'] . ", " . $data['acctstatus']);
Log::insert(LogType::USER_EDITED, $_SESSION['uid'], "OLD: " . $olddata['username'] . ", " . $olddata['realname'] . ", " . $olddata['email'] . ", " . $olddata['acctstatus'] . "; NEW: " . $data['username'] . ", " . $data['realname'] . ", " . $data['email'] . ", " . $data['acctstatus']);
}

returnToSender("user_saved");
@@ -97,7 +95,7 @@ switch ($VARS['action']) {
// we will flag it as deleted and set the status to LOCKED_OR_DISABLED.
$database->update('accounts', ['acctstatus' => 2, 'deleted' => 1], ['uid' => $VARS['id']]);
}
insertAuthLog(16, $_SESSION['uid'], $olddata['username'] . ", " . $olddata['realname'] . ", " . $olddata['email'] . ", " . $olddata['acctstatus']);
Log::insert(LogType::USER_REMOVED, $_SESSION['uid'], $olddata['username'] . ", " . $olddata['realname'] . ", " . $olddata['email'] . ", " . $olddata['acctstatus']);
returnToSender("user_deleted");
case "rmtotp":
if ($database->has('accounts', ['uid' => $VARS['id']]) !== TRUE) {
@@ -105,28 +103,27 @@ switch ($VARS['action']) {
}
$u = $database->get('accounts', 'username', ['uid' => $VARS['id']]);
$database->update('accounts', ["authsecret" => null], ['uid' => $VARS['id']]);
insertAuthLog(10, $_SESSION['uid'], $u);
Log::insert(LogType::REMOVED_2FA, $_SESSION['uid'], $u);
returnToSender("2fa_removed");
case "clearlog":
$rows = $database->count('authlog');
$database->delete('authlog', []);
insertAuthLog(15, $_SESSION['uid'], lang2("removed n entries", ['n' => $rows], false));
Log::insert(LogType::LOG_CLEARED, $_SESSION['uid'], $Strings->build("removed n entries", ['n' => $rows], false));
returnToSender("log_cleared");
case "editmanager":
require_once __DIR__ . "/lib/userinfo.php";
if (!$database->has('accounts', ['username' => $VARS['manager']])) {
returnToSender("invalid_manager");
}
$manager = getUserByUsername($VARS['manager'])['uid'];
$manager = User::byUsername($VARS['manager'])->getUID();
$already_assigned = $database->select('managers', 'employeeid', ['managerid' => $manager]);

foreach ($VARS['employees'] as $u) {
if (!user_exists($u)) {
returnToSender("user_not_exists", htmlentities($u));
$emp = User::byUsername($u);
if (!$emp->exists()) {
returnToSender("user_not_exists", htmlentities($emp->getUsername()));
}
$uid = getUserByUsername($u)['uid'];
$database->insert('managers', ['employeeid' => $uid, 'managerid' => $manager]);
$already_assigned = array_diff($already_assigned, [$uid]); // Remove user from old list
$database->insert('managers', ['employeeid' => $emp->getUID(), 'managerid' => $manager]);
$already_assigned = array_diff($already_assigned, [$emp->getUID()]); // Remove user from old list
}
foreach ($already_assigned as $uid) {
$database->delete('managers', ["AND" => ['employeeid' => $uid, 'managerid' => $manager]]);
@@ -198,14 +195,14 @@ switch ($VARS['action']) {
returnToSender("permission_deleted");
case "autocomplete_user":
header("Content-Type: application/json");
if (is_empty($VARS['q']) || strlen($VARS['q']) < 3) {
if (empty($VARS['q']) || strlen($VARS['q']) < 3) {
exit(json_encode([]));
}
$data = $database->select('accounts', ['uid', 'username', 'realname (name)'], ["OR" => ['username[~]' => $VARS['q'], 'realname[~]' => $VARS['q']], "LIMIT" => 10]);
exit(json_encode($data));
case "autocomplete_permission":
header("Content-Type: application/json");
if (is_empty($VARS['q'])) {
if (empty($VARS['q'])) {
exit(json_encode([]));
}
$data = $database->select('permissions', ['permcode (name)', 'perminfo (info)'], ["OR" => ['permcode[~]' => $VARS['q'], 'perminfo[~]' => $VARS['q']], "LIMIT" => 10]);
@@ -217,14 +214,13 @@ switch ($VARS['action']) {
$gid = $VARS['gid'];
$already_assigned = $database->select('assigned_groups', 'uid', ['groupid' => $gid]);

require_once __DIR__ . "/lib/userinfo.php";
foreach ($VARS['users'] as $u) {
if (!user_exists($u)) {
returnToSender("user_not_exists", htmlentities($u));
$user = User::byUsername($u);
if (!$user->exists()) {
returnToSender("user_not_exists", htmlentities($user->getUsername()));
}
$uid = getUserByUsername($u)['uid'];
$database->insert('assigned_groups', ['groupid' => $gid, 'uid' => $uid]);
$already_assigned = array_diff($already_assigned, [$uid]); // Remove user from old list
$database->insert('assigned_groups', ['groupid' => $gid, 'uid' => $user->getUID()]);
$already_assigned = array_diff($already_assigned, [$user->getUID()]); // Remove user from old list
}
foreach ($already_assigned as $uid) {
$database->delete('assigned_groups', ["AND" => ['uid' => $uid, 'groupid' => $gid]]);
@@ -251,7 +247,7 @@ switch ($VARS['action']) {
break;
case "signout":
session_destroy();
header('Location: index.php');
header('Location: index.php?logout=1');
die("Logged out.");
default:
die("Invalid action");

+ 2
- 33
api.php View File

@@ -4,37 +4,6 @@
* 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/. */

/**
* Simple JSON API to allow other apps to access data from this app.
*
* Requests can be sent via either GET or POST requests. POST is recommended
* as it has a lower chance of being logged on the server, exposing unencrypted
* user passwords.
*/
require __DIR__ . '/required.php';
require_once __DIR__ . '/lib/login.php';
require_once __DIR__ . '/lib/userinfo.php';
header("Content-Type: application/json");

$username = $VARS['username'];
$password = $VARS['password'];
if (user_exists($username) !== true || authenticate_user($username, $password, $errmsg) !== true || account_has_permission($username, "ADMIN") !== true) {
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) {
$max = (int) $VARS['max'];
}

switch ($VARS['action']) {
case "ping":
$out = ["status" => "OK", "maxresults" => $max, "pong" => true];
exit(json_encode($out));
default:
header("HTTP/1.1 400 Bad Request");
die("\"400 Bad Request\"");
}
// Load in new API from legacy location (a.k.a. here)
require __DIR__ . "/api/index.php";

+ 5
- 0
api/.htaccess View File

@@ -0,0 +1,5 @@
# Rewrite for Nextcloud Notes API
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteRule ([a-zA-Z0-9]+) index.php?action=$1 [PT]
</IfModule>

+ 9
- 0
api/actions/ping.php View File

@@ -0,0 +1,9 @@
<?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/.
*/

sendJsonResp();

+ 15
- 0
api/apisettings.php View File

@@ -0,0 +1,15 @@
<?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/.
*/

$APIS = [
"ping" => [
"load" => "ping.php",
"vars" => [
]
]
];

+ 144
- 0
api/functions.php View File

@@ -0,0 +1,144 @@
<?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/.
*/

/**
* Build and send a simple JSON response.
* @param string $msg A message
* @param string $status "OK" or "ERROR"
* @param array $data More JSON data
*/
function sendJsonResp(string $msg = null, string $status = "OK", array $data = null) {
$resp = [];
if (!is_null($data)) {
$resp = $data;
}
if (!is_null($msg)) {
$resp["msg"] = $msg;
}
$resp["status"] = $status;
header("Content-Type: application/json");
exit(json_encode($resp));
}

function exitWithJson(array $json) {
header("Content-Type: application/json");
exit(json_encode($json));
}

/**
* Get the API key with most of the characters replaced with *s.
* @global string $key
* @return string
*/
function getCensoredKey() {
global $key;
$resp = $key;
if (strlen($key) > 5) {
for ($i = 2; $i < strlen($key) - 2; $i++) {
$resp[$i] = "*";
}
}
return $resp;
}

/**
* Check if the request is allowed
* @global array $VARS
* @return bool true if the request should continue, false if the request is bad
*/
function authenticate(): bool {
global $VARS;
// HTTP basic auth
if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
$user = User::byUsername($_SERVER['PHP_AUTH_USER']);
if (!$user->checkPassword($_SERVER['PHP_AUTH_PW'])) {
return false;
}
return true;
}
// Form auth
if (empty($VARS['username']) || empty($VARS['password'])) {
return false;
} else {
$username = $VARS['username'];
$password = $VARS['password'];
$user = User::byUsername($username);
if ($user->exists() !== true || Login::auth($username, $password) !== Login::LOGIN_OK) {
return false;
}
}
return true;
}

/**
* Get the User whose credentials were used to make the request.
*/
function getRequestUser(): User {
global $VARS;
if (!empty($_SERVER['PHP_AUTH_USER'])) {
return User::byUsername($_SERVER['PHP_AUTH_USER']);
} else {
return User::byUsername($VARS['username']);
}
}

function checkVars($vars, $or = false) {
global $VARS;
$ok = [];
foreach ($vars as $key => $val) {
if (strpos($key, "OR") === 0) {
checkVars($vars[$key], true);
continue;
}

// Only check type of optional variables if they're set, and don't
// mark them as bad if they're not set
if (strpos($key, " (optional)") !== false) {
$key = str_replace(" (optional)", "", $key);
if (empty($VARS[$key])) {
continue;
}
} else {
if (empty($VARS[$key])) {
$ok[$key] = false;
continue;
}
}

if (strpos($val, "/") === 0) {
// regex
$ok[$key] = preg_match($val, $VARS[$key]) === 1;
} else {
$checkmethod = "is_$val";
$ok[$key] = !($checkmethod($VARS[$key]) !== true);
}
}
if ($or) {
$success = false;
$bad = "";
foreach ($ok as $k => $v) {
if ($v) {
$success = true;
break;
} else {
$bad = $k;
}
}
if (!$success) {
http_response_code(400);
die("400 Bad request: variable $bad is missing or invalid");
}
} else {
foreach ($ok as $key => $bool) {
if (!$bool) {
http_response_code(400);
die("400 Bad request: variable $key is missing or invalid");
}
}
}
}

+ 79
- 0
api/index.php View File

@@ -0,0 +1,79 @@
<?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/.
*/

require __DIR__ . '/../required.php';
require __DIR__ . '/functions.php';
require __DIR__ . '/apisettings.php';

$VARS = $_GET;
if ($_SERVER['REQUEST_METHOD'] != "GET") {
$VARS = array_merge($VARS, $_POST);
}

$requestbody = file_get_contents('php://input');
$requestjson = json_decode($requestbody, TRUE);
if (json_last_error() == JSON_ERROR_NONE) {
$VARS = array_merge($VARS, $requestjson);
}

// If we're not using the old api.php file, allow more flexible requests
if (strpos($_SERVER['REQUEST_URI'], "/api.php") === FALSE) {
$route = explode("/", substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], "api/") + 4));

if (count($route) >= 1) {
$VARS["action"] = $route[0];
}
if (count($route) >= 2 && strpos($route[1], "?") !== 0) {
for ($i = 1; $i < count($route); $i++) {
if (empty($route[$i]) || strpos($route[$i], "=") === false) {
continue;
}
$key = explode("=", $route[$i], 2)[0];
$val = explode("=", $route[$i], 2)[1];
$VARS[$key] = $val;
}
}

if (strpos($route[count($route) - 1], "?") === 0) {
$morevars = explode("&", substr($route[count($route) - 1], 1));
foreach ($morevars as $var) {
$key = explode("=", $var, 2)[0];
$val = explode("=", $var, 2)[1];
$VARS[$key] = $val;
}
}
}

if (!authenticate()) {
header('WWW-Authenticate: Basic realm="' . $SETTINGS['site_title'] . '"');
header('HTTP/1.1 401 Unauthorized');
die("401 Unauthorized: you need to supply valid credentials.");
}

if (empty($VARS['action'])) {
http_response_code(404);
die("404 No action specified");
}

if (!isset($APIS[$VARS['action']])) {
http_response_code(404);
die("404 Action not defined");
}

$APIACTION = $APIS[$VARS["action"]];

if (!file_exists(__DIR__ . "/actions/" . $APIACTION["load"])) {
http_response_code(404);
die("404 Action not found");
}

if (!empty($APIACTION["vars"])) {
checkVars($APIACTION["vars"]);
}

require_once __DIR__ . "/actions/" . $APIACTION["load"];

+ 40
- 34
app.php View File

@@ -1,5 +1,4 @@
<?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/. */
@@ -14,7 +13,7 @@ if ($_SESSION['loggedin'] != true) {
require_once __DIR__ . "/pages.php";

$pageid = "home";
if (isset($_GET['page']) && !is_empty($_GET['page'])) {
if (!empty($_GET['page'])) {
$pg = strtolower($_GET['page']);
$pg = preg_replace('/[^0-9a-z_]/', "", $pg);
if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) {
@@ -28,10 +27,10 @@ header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
header("Link: <static/css/app.css>; rel=preload; as=style", false);
header("Link: <static/css/fa-svg-with-js.css>; rel=preload; as=style", false);
header("Link: <static/css/svg-with-js.min.css>; rel=preload; as=style", false);
header("Link: <static/js/fontawesome-all.min.js>; rel=preload; as=script", false);
header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
@@ -40,14 +39,14 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title><?php echo SITE_TITLE; ?></title>
<title><?php echo $SETTINGS['site_title']; ?></title>

<link rel="icon" href="static/img/logo.svg">

<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
<link href="static/css/fa-svg-with-js.css" rel="stylesheet">
<link href="static/css/svg-with-js.min.css" rel="stylesheet">
<script nonce="<?php echo $SECURE_NONCE; ?>">
FontAwesomeConfig = {autoAddCss: false}
</script>
@@ -66,28 +65,35 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);

<?php
// Alert messages
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);
if (!empty($_GET['msg'])) {
if (array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (empty($_GET['arg'])) {
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
} else {
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
switch (MESSAGES[$_GET['msg']]['type']) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
}
} else {
$alertmsg = lang2(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
switch (MESSAGES[$_GET['msg']]['type']) {
case "danger":
$alerticon = "times";
break;
case "warning":
$alerticon = "exclamation-triangle";
break;
case "info":
$alerticon = "info-circle";
break;
case "success":
$alerticon = "check";
break;
// We don't have a message for this, so just assume an error and escape stuff.
$alertmsg = htmlentities($Strings->get($_GET['msg'], false));
$alerticon = "times";
$alerttype = "danger";
}
echo <<<END
<div class="row justify-content-center" id="msg-alert-box">
@@ -121,7 +127,7 @@ END;
</button>
<a class="navbar-brand py-0 mr-auto" href="app.php">
<img src="static/img/logo.svg" alt="" class="d-none d-<?php echo $navbar_breakpoint; ?>-inline brand-img py-0" />
<?php echo SITE_TITLE; ?>
<?php echo $SETTINGS['site_title']; ?>
</a>

<div class="collapse navbar-collapse py-0" id="navbar-collapse">
@@ -146,7 +152,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>
@@ -157,13 +163,13 @@ END;
</div>
<div class="navbar-nav ml-auto py-0" id="navbar-right">
<span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="<?php echo PORTAL_URL; ?>">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="<?php echo $SETTINGS['accounthub']['home']; ?>">
<i class="fas fa-user fa-fw"></i><span>&nbsp;<?php echo $_SESSION['realname'] ?></span>
</a>
</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>
@@ -177,12 +183,12 @@ END;
?>
</div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
<?php echo $SETTINGS['footer_text']; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo $SETTINGS['copyright']; ?>
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/app.js"></script>
<?php
// custom page scripts

+ 12
- 12
composer.lock View File

@@ -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": "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": {
@@ -87,7 +87,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": {
@@ -96,7 +96,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.2-dev"
"dev-master": "6.3-dev"
}
},
"autoload": {
@@ -129,7 +129,7 @@
"rest",
"web service"
],
"time": "2017-06-22 18:50:49"
"time": "2018-04-22 15:46:56"
},
{
"name": "guzzlehttp/promises",

+ 112
- 145
index.php View File

@@ -1,164 +1,131 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* 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/. */
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

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');
die();
}

if (isset($_GET['permissionerror'])) {
$alert = lang("no access permission", false);
}
/**
* Show a simple HTML page with a line of text and a button. Matches the UI of
* the AccountHub login flow.
*
* @global type $SETTINGS
* @global type $SECURE_NONCE
* @global type $Strings
* @param string $title Text to show, passed through i18n
* @param string $button Button text, passed through i18n
* @param string $url URL for the button
*/
function showHTML(string $title, string $button, string $url) {
global $SETTINGS, $SECURE_NONCE, $Strings;
?>
<!DOCTYPE html>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

/* 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'])) {
case "LOCKED_OR_DISABLED":
$alert = lang("account locked", false);
break;
case "TERMINATED":
$alert = lang("account terminated", false);
break;
case "CHANGE_PASSWORD":
$alert = lang("password expired", false);
case "NORMAL":
$userpass_ok = true;
break;
case "ALERT_ON_ACCESS":
sendLoginAlertEmail($VARS['username']);
$userpass_ok = true;
break;
}
if ($userpass_ok) {
$_SESSION['passok'] = true; // stop logins using only username and authcode
if (userHasTOTP($VARS['username'])) {
$multiauth = true;
} else {
doLoginUser($VARS['username'], $VARS['password']);
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);
}
}
} else {
$alert = lang("captcha error", false);
}
} else if (!empty($VARS['progress']) && $VARS['progress'] == "2") {
if ($_SESSION['passok'] !== true) {
// stop logins using only username and authcode
sendError("Password integrity check failed!");
<title><?php echo $SETTINGS['site_title']; ?></title>

<link rel="icon" href="static/img/logo.svg">

<link href="static/css/bootstrap.min.css" rel="stylesheet">
<style nonce="<?php echo $SECURE_NONCE; ?>">
.display-5 {
font-size: 2.5rem;
font-weight: 300;
line-height: 1.2;
}
if (verifyTOTP($VARS['username'], $VARS['authcode'])) {
if (doLoginUser($VARS['username'])) {
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);

.banner-image {
max-height: 100px;
margin: 2em auto;
border: 1px solid grey;
border-radius: 15%;
}
}
} else {
$alert = lang("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);
header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
header("Link: <static/css/index.css>; rel=preload; as=style", false);
header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title><?php echo SITE_TITLE; ?></title>

<link rel="icon" href="static/img/logo.svg">

<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/index.css" rel="stylesheet">
<?php if (CAPTCHA_ENABLED) { ?>
<script src="<?php echo CAPTCHA_SERVER ?>/captcheck.dist.js"></script>
<?php } ?>
</head>
<body>
</style>

<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-auto">
<img class="banner-image" src="static/img/logo.svg" />
<div class="col-12 text-center">
<img class="banner-image" src="./static/img/logo.svg" />
</div>
</div>
<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>
<form action="" method="POST">
<?php
if (!empty($alert)) {
?>
<div class="alert alert-danger">
<i class="fa fa-fw fa-exclamation-triangle"></i> <?php echo $alert; ?>
</div>
<?php
}

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 />
<?php if (CAPTCHA_ENABLED) { ?>
<div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
<br />
<?php } ?>
<input type="hidden" name="progress" value="1" />
<?php
} else if ($multiauth) {
?>
<div class="alert alert-info">
<?php lang("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="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"); ?>
</button>
</form>

<div class="col-12 text-center">
<h1 class="display-5 mb-4"><?php $Strings->get($title); ?></h1>
</div>

<div class="col-12 col-sm-8 col-lg-6">
<div class="card mt-4">
<div class="card-body">
<a href="<?php echo $url; ?>" class="btn btn-primary btn-block"><?php $Strings->get($button); ?></a>
</div>
</div>
</div>
</div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
</body>
</html>
<?php
}

if (!empty($_GET['logout'])) {
showHTML("You have been logged out.", "Log in again", "./index.php");
die();
}
if (empty($_SESSION["login_code"])) {
$redirecttologin = true;
} else {
try {
$uidinfo = AccountHubApi::get("checkloginkey", ["code" => $_SESSION["login_code"]]);
if ($uidinfo["status"] == "ERROR") {
throw new Exception();
}
if (is_numeric($uidinfo['uid'])) {
$user = new User($uidinfo['uid'] * 1);
foreach ($SETTINGS['permissions'] as $perm) {
if (!$user->hasPermission($perm)) {
showHTML("no access permission", "sign out", "./action.php?action=signout");
die();
}
}
Session::start($user);
$_SESSION["login_code"] = null;
header('Location: app.php');
showHTML("Logged in", "Continue", "./app.php");
die();
} else {
throw new Exception();
}
} catch (Exception $ex) {
$redirecttologin = true;
}
}

if ($redirecttologin) {
try {
$urlbase = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "");
$iconurl = $urlbase . str_replace("index.php", "", $_SERVER["REQUEST_URI"]) . "static/img/logo.svg";
$codedata = AccountHubApi::get("getloginkey", ["appname" => $SETTINGS["site_title"], "appicon" => $iconurl]);

if ($codedata['status'] != "OK") {
throw new Exception($Strings->get("login server unavailable", false));
}

$redirecturl = $urlbase . $_SERVER['REQUEST_URI'];

$_SESSION["login_code"] = $codedata["code"];

$locationurl = $codedata["loginurl"] . "?code=" . htmlentities($codedata["code"]) . "&redirect=" . htmlentities($redirecturl);
header("Location: $locationurl");
showHTML("Continue", "Continue", $locationurl);
die();
} catch (Exception $ex) {
sendError($ex->getMessage());
}
}

+ 0
- 140
lang/en_us.php View File

@@ -1,140 +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.",
"no admin permission" => "You do not have permission to access this system.",
"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.",
"home" => "Home",
"users" => "Users",
"more" => "More",
"actions" => "Actions",
"name" => "Name",
"email" => "Email",
"status" => "Status",
"type" => "Type",
"new user" => "New User",
"total users" => "Total Users",
"view users" => "View Users",
"normal accounts" => "Normal Accounts",
"locked accounts" => "Locked Accounts",
"editing user" => "Editing {user}",
"invalid userid" => "Invalid user ID.",
"user saved" => "User saved.",
"adding user" => "Adding new user",
"placeholder name" => "John Doe",
"placeholder username" => "jdoe",
"placeholder email address" => "jdoe@example.com",
"placeholder password" => "swordfish",
"new password" => "New Password",
"non-local account warning" => "This account is not locally managed. Changes made here will not synchronize to the directory server and some attributes cannot be edited.",
"delete user" => "Delete User",
"really delete user" => "Are you sure you want to delete this user? This action cannot be reversed.",
"user deleted" => "User account deleted.",
"user does not exist" => "User does not exist.",
"logtime" => "Date/Time",
"logtype" => "Event Type",
"ip address" => "IP Address",
"other data" => "Other",
"security log" => "Security Log",
"event type reference" => "Event Type Reference",
"clear log" => "Clear Log",
"really clear log" => "Are you sure you want to purge the security log? This action cannot be reversed.",
"log cleared" => "Security log cleared.",
"removed n entries" => "Removed {n} entries",
"security log entries" => "Security Log Entries",
"view security log" => "View Security Log",
"managers" => "Managers",
"manager" => "Manager",
"employee" => "Employee",
"delete relationship" => "Delete Relationship",
"really delete relationship" => "Are you sure you want to remove this manager-employee relationship? This action cannot be reversed.",
"relationship deleted" => "Relationship deleted.",
"edit relationship" => "Edit Relationship",
"adding relationship" => "Adding Relationship",
"relationship added" => "Relationship added.",
"permissions" => "Permissions",
"permission" => "Permission",
"new permission" => "New Permission",
"delete permission" => "Delete Permission",
"adding permission" => "Adding Permission",
"user" => "User",
"permission does not exist" => "Permission does not exist: {arg}",
"really delete permission" => "Are you sure you want to revoke this permission?",
"permission added" => "Permission assigned.",
"permission deleted" => "Permission deleted.",
"remove 2fa" => "Reset 2FA",
"action performed by" => "Action performed by {user}",
"2fa removed" => "2-factor authentication removed.",
"2fa" => "2FA",
"show deleted" => "Show deleted",
"editing deleted account" => "You are editing an account marked as deleted. The account will be undeleted if you press Save.",
"manager assigned" => "Manager relationships saved.",
"manager does not exist" => "The selected manager username does not exist.",
"type to add a person" => "Type to add a person",
"employees" => "Employees",
"type to select a manager" => "Type to select a manager",
"select a manager to view or edit employees" => "Select a manager to view or edit the assigned employees.",
"report export" => "Reports/Export",
"report type" => "Report type",
"format" => "Format",
"generate report" => "Generate report",
"choose an option" => "Choose an option",
"csv file" => "CSV text file",
"ods file" => "ODS spreadsheet",
"html file" => "HTML web page",
"uid" => "User ID",
"manager name" => "Manager",
"manager username" => "Mgr. Username",
"employee name" => "Employee",
"employee username" => "Emp. Username",
"permission id" => "Perm. ID",
"permissions assigned" => "Permissions assigned.",
"type to select a user" => "Type to select a user",
"type to add a permission" => "Type to add a permission",
"Choose a permission" => "Choose a permission",
"select a user to view or edit permissions" => "Select a user to view or edit the assigned permissions.",
"group" => "Group",
"groups" => "Groups",
"group does not exist" => "That group does not exist.",
"group members updated" => "Group members updated.",
"group added" => "Group added.",
"group deleted" => "Group deleted.",
"group already exists" => "A group with that name already exists.",
"save" => "Save",
"next" => "Next",
"add" => "Add",
"delete" => "Delete",
"new group" => "New group",
"delete group" => "Delete group",
"enter group name" => "Group name",
"group management" => "Group Management",
"group assignments" => "Group Assignments",
"group id" => "Group ID",
"group name" => "Group Name"
]);

+ 7
- 0
langs/en/core.json View File

@@ -0,0 +1,7 @@
{
"sign out": "Sign out",
"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}"
}

+ 8
- 0
langs/en/index.json View File

@@ -0,0 +1,8 @@
{
"You have been logged out.": "You have been logged out.",
"Log in again": "Log in again",
"login server unavailable": "Login server unavailable. Try again later or contact technical support.",
"no access permission": "You do not have permission to access this system.",
"Logged in": "Logged in",
"Continue": "Continue"
}

+ 9
- 0
langs/en/titles.json View File

@@ -0,0 +1,9 @@
{
"Home": "Home",
"Users": "Users",
"Groups": "Groups",
"Security": "Security",
"Security Log": "Security Log",
"Managers": "Managers",
"Permissions": "Permissions"
}

lang/messages.php → langs/messages.php View File


+ 56
- 0
lib/AccountHubApi.lib.php View File

@@ -0,0 +1,56 @@
<?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 AccountHubApi {

public static function get(string $action, array $data = null, bool $throwex = false) {
global $SETTINGS;

$content = [
"action" => $action,
"key" => $SETTINGS['accounthub']['key']
];
if (!is_null($data)) {
$content = array_merge($content, $data);
}
$options = [
'http' => [
'method' => 'POST',
'content' => json_encode($content),
'header' => "Content-Type: application/json\r\n" .
"Accept: application/json\r\n",
"ignore_errors" => true
]
];

$context = stream_context_create($options);
$result = file_get_contents($SETTINGS['accounthub']['api'], false, $context);
$response = json_decode($result, true);
if ($result === false || !AccountHubApi::checkHttpRespCode($http_response_header) || json_last_error() != JSON_ERROR_NONE) {
if ($throwex) {
throw new Exception($result);
} else {
sendError($result);
}
}
return $response;
}

private static function checkHttpRespCode(array $headers): bool {
foreach ($headers as $header) {
if (preg_match("/HTTP\/[0-9]\.[0-9] [0-9]{3}.*/", $header)) {
$respcode = explode(" ", $header)[1] * 1;
if ($respcode >= 200 && $respcode < 300) {
return true;
}
}
}
return false;
}

}

+ 13
- 0
lib/Exceptions.lib.php View File

@@ -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);
}
}

+ 275
- 0
lib/FormBuilder.lib.php View File

@@ -0,0 +1,275 @@
<?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 FormBuilder {

private $items = [];
private $hiddenitems = [];
private $title = "";
private $icon = "";
private $buttons = [];
private $action = "action.php";
private $method = "POST";
private $id = "editform";

/**
* Create a form with autogenerated HTML.
*
* @param string $title Form title/heading
* @param string $icon FontAwesone icon next to the title.
* @param string $action URL to submit the form to.
* @param string $method Form submission method (POST, GET, etc.)
*/
public function __construct(string $title = "Untitled Form", string $icon = "fas fa-file-alt", string $action = "action.php", string $method = "POST") {
$this->title = $title;
$this->icon = $icon;
$this->action = $action;
$this->method = $method;
}

/**
* Set the title of the form.
* @param string $title
*/
public function setTitle(string $title) {
$this->title = $title;
}

/**
* Set the icon for the form.
* @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
*/
public function setIcon(string $icon) {
$this->icon = $icon;
}

/**
* Set the URL the form will submit to.
* @param string $action
*/
public function setAction(string $action) {
$this->action = $action;
}

/**
* Set the form submission method (GET, POST, etc)
* @param string $method
*/
public function setMethod(string $method = "POST") {
$this->method = $method;
}

/**
* Set the form ID.
* @param string $id
*/
public function setID(string $id = "editform") {
$this->id = $id;
}

/**
* Add an input to the form.
*
* @param string $name Element name
* @param string $value Element value
* @param string $type Input type (text, number, date, select, tel...)
* @param bool $required If the element is required for form submission.
* @param string $id Element ID
* @param array $options Array of [value => text] pairs for a select element
* @param string $label Text label to display near the input
* @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
* @param int $width Bootstrap column width for the input, out of 12.
* @param int $minlength Minimum number of characters for the input.
* @param int $maxlength Maximum number of characters for the input.
* @param string $pattern Regex pattern for custom client-side validation.
* @param string $error Message to show if the input doesn't validate.
*/
public function addInput(string $name, string $value = "", string $type = "text", bool $required = true, string $id = null, array $options = null, string $label = "", string $icon = "", int $width = 4, int $minlength = 1, int $maxlength = 100, string $pattern = "", string $error = "") {
$item = [
"name" => $name,
"value" => $value,
"type" => $type,
"required" => $required,
"label" => $label,
"icon" => $icon,
"width" => $width,
"minlength" => $minlength,
"maxlength" => $maxlength
];
if (!empty($id)) {
$item["id"] = $id;
}
if (!empty($options) && $type == "select") {
$item["options"] = $options;
}
if (!empty($pattern)) {
$item["pattern"] = $pattern;
}
if (!empty($error)) {
$item["error"] = $error;
}
$this->items[] = $item;
}

/**
* Add a button to the form.
*
* @param string $text Text string to show on the button.
* @param string $icon FontAwesome icon to show next to the text.
* @param string $href If not null, the button will actually be a hyperlink.
* @param string $type Usually "button" or "submit". Ignored if $href is set.
* @param string $id The element ID.
* @param string $name The element name for the button.
* @param string $value The form value for the button. Ignored if $name is null.
* @param string $class The CSS classes for the button, if a standard success-colored one isn't right.
*/
public function addButton(string $text, string $icon = "", string $href = null, string $type = "button", string $id = null, string $name = null, string $value = "", string $class = "btn btn-success") {
$button = [
"text" => $text,
"icon" => $icon,
"class" => $class,
"type" => $type,
"id" => $id,
"href" => $href,
"name" => $name,
"value" => $value
];
$this->buttons[] = $button;
}

/**
* Add a hidden input.
* @param string $name
* @param string $value
*/
public function addHiddenInput(string $name, string $value) {
$this->hiddenitems[$name] = $value;
}

/**
* Generate the form HTML.
* @param bool $echo If false, returns HTML string instead of outputting it.
*/
public function generate(bool $echo = true) {
$html = <<<HTMLTOP
<form action="$this->action" method="$this->method" id="$this->id">
<div class="card">
<h3 class="card-header d-flex">
<div>
<i class="$this->icon"></i> $this->title
</div>
</h3>

<div class="card-body">
<div class="row">
HTMLTOP;

foreach ($this->items as $item) {
$required = $item["required"] ? "required" : "";
$id = empty($item["id"]) ? "" : "id=\"$item[id]\"";
$pattern = empty($item["pattern"]) ? "" : "pattern=\"$item[pattern]\"";
if (empty($item['type'])) {
$item['type'] = "text";
}
$itemhtml = "";
$itemlabel = "";
if ($item['type'] != "checkbox") {
$itemlabel = "<label class=\"mb-0\">$item[label]:</label>";
}
$strippedlabel = strip_tags($item['label']);
$itemhtml .= <<<ITEMTOP
\n\n <div class="col-12 col-md-$item[width]">
<div class="form-group mb-3">
$itemlabel
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="$item[icon]"></i></span>
</div>
ITEMTOP;
switch ($item['type']) {
case "select":
$itemhtml .= <<<SELECT
\n <select class="form-control" name="$item[name]" aria-label="$strippedlabel" $required>
SELECT;
foreach ($item['options'] as $value => $label) {
$selected = "";
if (!empty($item['value']) && $value == $item['value']) {
$selected = " selected";
}
$itemhtml .= "\n <option value=\"$value\"$selected>$label</option>";
}
$itemhtml .= "\n </select>";
break;
case "checkbox":
$itemhtml .= <<<CHECKBOX
\n <div class="form-group form-check">
<input type="checkbox" name="$item[name]" $id class="form-check-input" value="$item[value]" $required aria-label="$strippedlabel">
<label class="form-check-label">$item[label]</label>
</div>
CHECKBOX;
break;
default:
$itemhtml .= <<<INPUT
\n <input type="$item[type]" name="$item[name]" $id class="form-control" aria-label="$strippedlabel" minlength="$item[minlength]" maxlength="$item[maxlength]" $pattern value="$item[value]" $required />
INPUT;
break;
}

if (!empty($item["error"])) {
$itemhtml .= <<<ERROR
\n <div class="invalid-feedback">
$item[error]
</div>
ERROR;
}
$itemhtml .= <<<ITEMBOTTOM
\n </div>
</div>
</div>\n
ITEMBOTTOM;
$html .= $itemhtml;
}

$html .= <<<HTMLBOTTOM

</div>
</div>
HTMLBOTTOM;

if (!empty($this->buttons)) {
$html .= "\n <div class=\"card-footer\">";
foreach ($this->buttons as $btn) {
$btnhtml = "";
$inner = "<i class=\"$btn[icon]\"></i> $btn[text]";
$id = empty($btn['id']) ? "" : "id=\"$btn[id]\"";
if (!empty($btn['href'])) {
$btnhtml = "<a href=\"$btn[href]\" class=\"$btn[class]\" $id>$inner</a>";
} else {
$name = empty($btn['name']) ? "" : "name=\"$btn[name]\"";
$value = (!empty($btn['name']) && !empty($btn['value'])) ? "value=\"$btn[value]\"" : "";
$btnhtml = "<button type=\"$btn[type]\" class=\"$btn[class]\" $id $name $value>$inner</button>";
}
$html .= "\n $btnhtml";
}
$html .= "\n </div>";
}

$html .= "\n </div>";
foreach ($this->hiddenitems as $name => $value) {
$value = htmlentities($value);
$html .= "\n <input type=\"hidden\" name=\"$name\" value=\"$value\" />";
}
$html .= "\n</form>\n";

if ($echo) {
echo $html;
}
return $html;
}

}

+ 135
- 0
lib/IPUtils.lib.php View File

@@ -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
}

}

+ 80
- 0
lib/Login.lib.php View File

@@ -0,0 +1,80 @@
<?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;
}

/**
* Check the login server API for sanity
* @return boolean true if OK, else false
*/
public static function checkLoginServer() {
try {
$resp = AccountHubApi::get("ping");
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 {
$resp = AccountHubApi::get("ping", null, true);
return false;
} catch (Exception $e) {
return false;
}
}

}

+ 53
- 0
lib/Notifications.lib.php View File

@@ -0,0 +1,53 @@
<?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));
}

$resp = AccountHubApi::get("addnotification", [
'uid' => $user->getUID(),
'title' => $title,
'content' => $content,
'timestamp' => $timestamp,
'url' => $url,
'sensitive' => $sensitive
]
);
if ($resp['status'] == "OK") {
return $resp['id'] * 1;
} else {
return false;
}
}
throw new Exception($Strings->get("user does not exist", false));
}

}

+ 19
- 0
lib/Session.lib.php View File

@@ -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;
}

}

+ 122
- 0
lib/Strings.lib.php View File

@@ -0,0 +1,122 @@
<?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 ($language == "en") {
return;
}

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
- 18
lib/authlog.php View File

@@ -1,18 +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/. */


require_once __DIR__ . "/../required.php";
require_once __DIR__ . "/iputils.php";

dieifnotloggedin();

function insertAuthLog($type, $uid = null, $data = "") {
global $database;
// find IP address
$ip = getClientIP();
$database->insert("authlog", ['logtime' => date("Y-m-d H:i:s"), 'logtype' => $type, 'uid' => $uid, 'ip' => $ip, 'otherdata' => $data]);
}

+ 1
- 1
lib/getlogtable.php View File

@@ -43,7 +43,7 @@ switch ($VARS['order'][0]['column']) {
}

// search
if (!is_empty($VARS['search']['value'])) {
if (!empty($VARS['search']['value'])) {
$filter = true;
$wherenolimit = [
"OR" => [

+ 2
- 2
lib/getmanagetable.php View File

@@ -34,7 +34,7 @@ switch ($VARS['order'][0]['column']) {
}

// search
if (!is_empty($VARS['search']['value'])) {
if (!empty($VARS['search']['value'])) {
$filter = true;
$wherenolimit = [
"OR" => [
@@ -78,7 +78,7 @@ if ($filter) {
}
$out['recordsFiltered'] = $recordsFiltered;
for ($i = 0; $i < count($managers); $i++) {
$managers[$i]["delbtn"] = '<a class="btn btn-danger btn-xs" href="app.php?page=delmanager&mid=' . $managers[$i]['managerid'] . '&eid=' . $managers[$i]['employeeid'] . '"><i class="fa fa-trash"></i> ' . lang("delete", false) . '</a>';
$managers[$i]["delbtn"] = '<a class="btn btn-danger btn-xs" href="app.php?page=delmanager&mid=' . $managers[$i]['managerid'] . '&eid=' . $managers[$i]['employeeid'] . '"><i class="fa fa-trash"></i> ' . $Strings->get("delete", false) . '</a>';
}
$out['managers'] = $managers;


+ 2
- 2
lib/getpermtable.php View File

@@ -34,7 +34,7 @@ switch ($VARS['order'][0]['column']) {
}

// search
if (!is_empty($VARS['search']['value'])) {
if (!empty($VARS['search']['value'])) {
$filter = true;
$wherenolimit = [
"OR" => [
@@ -76,7 +76,7 @@ if ($filter) {
}
$out['recordsFiltered'] = $recordsFiltered;
for ($i = 0; $i < count($data); $i++) {
$data[$i]["delbtn"] = '<a class="btn btn-danger btn-xs" href="app.php?page=delpermission&uid=' . $data[$i]['uid'] . '&pid=' . $data[$i]['permid'] . '"><i class="fa fa-trash"></i> ' . lang("delete", false) . '</a>';
$data[$i]["delbtn"] = '<a class="btn btn-danger btn-xs" href="app.php?page=delpermission&uid=' . $data[$i]['uid'] . '&pid=' . $data[$i]['permid'] . '"><i class="fa fa-trash"></i> ' . $Strings->get("delete", false) . '</a>';
}
$out['perms'] = $data;


+ 0
- 130
lib/getusertable.php View File

@@ -1,130 +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/. */


require __DIR__ . '/../required.php';

dieifnotloggedin();

header("Content-Type: application/json");

$show_deleted = false;
if ($VARS['show_deleted'] == 1) {
$show_deleted = true;
}

$out = [];

$out['draw'] = intval($VARS['draw']);

if ($show_deleted) {
$out['recordsTotal'] = $database->count('accounts');
} else {
$out['recordsTotal'] = $database->count('accounts', ['deleted' => 0]);
}
$filter = false;

// sort
$order = null;
$sortby = "DESC";
if ($VARS['order'][0]['dir'] == 'asc') {