Merge ../BusinessAppTemplate
# Conflicts: # README.md # api.php # composer.lock # index.php # lang/en_us.php # lib/login.php # required.php # settings.template.phpmaster
commit
47540e57d2
@ -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>
|
@ -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();
|
@ -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" => [
|
||||
]
|
||||
]
|
||||
];
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"];
|
@ -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);
|
||||
}
|
||||
|
||||
/* 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!");
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
} 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>
|
||||
/**
|
||||
* 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">
|
||||
|
||||
<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/index.css" rel="stylesheet">
|
||||
<?php if (CAPTCHA_ENABLED) { ?>
|
||||
<script src="<?php echo CAPTCHA_SERVER ?>/captcheck.dist.js"></script>
|
||||
<?php } ?>
|
||||
</head>
|
||||
<body>
|
||||
<style nonce="<?php echo $SECURE_NONCE; ?>">
|
||||
.display-5 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
max-height: 100px;
|
||||
margin: 2em auto;
|
||||
border: 1px solid grey;
|
||||
border-radius: 15%;
|
||||
}
|
||||
</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>
|
||||
<div class="col-12 text-center">
|
||||
<img class="banner-image" src="./static/img/logo.svg" />
|
||||
</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 class="col-12 text-center">
|
||||
<h1 class="display-5 mb-4"><?php $Strings->get($title); ?></h1>
|
||||
</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 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 class="footer">
|
||||
<?php echo FOOTER_TEXT; ?><br />
|
||||
Copyright © <?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());
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
]);
|
@ -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}"
|
||||
}
|
@ -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"
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Home": "Home",
|
||||
"Users": "Users",
|
||||
"Groups": "Groups",
|
||||
"Security": "Security",
|
||||
"Security Log": "Security Log",
|
||||
"Managers": "Managers",
|
||||
"Permissions": "Permissions"
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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,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;
|
||||
}
|
||||
|
||||
}
|
@ -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,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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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,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);
|
||||
}
|
||||
|
||||
}
|
@ -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,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') {
|
||||
$sortby = "ASC";
|
||||
}
|
||||
switch ($VARS['order'][0]['column']) {
|
||||
case 2:
|
||||
$order = ["realname" => $sortby];
|
||||
break;
|
||||
case 3:
|
||||
$order = ["username" => $sortby];
|
||||
break;
|
||||
case 4:
|
||||
$order = ["email" => $sortby];
|
||||
break;
|
||||
case 5:
|
||||
$order = ["authsecret" => $sortby];
|
||||
break;
|
||||
case 6:
|
||||
$order = ["statuscode" => $sortby];
|
||||
break;
|
||||
case 7:
|
||||
$order = ["typecode" => $sortby];
|
||||
break;
|
||||
}
|
||||
|
||||
// search
|
||||
if (!is_empty($VARS['search']['value'])) {
|
||||
$filter = true;
|
||||
if ($show_deleted) {
|
||||
$wherenolimit = [
|
||||
"OR" => [
|
||||
"username[~]" => $VARS['search']['value'],
|
||||
"realname[~]" => $VARS['search']['value'],
|
||||
"email[~]" => $VARS['search']['value'],
|
||||
"statuscode[~]" => $VARS['search']['value'],
|
||||
"typecode[~]" => $VARS['search']['value']
|
||||
]
|
||||
];
|
||||
} else {
|
||||
$wherenolimit = [
|
||||
"AND" => [
|
||||
"OR" => [
|
||||
"username[~]" => $VARS['search']['value'],
|
||||
"realname[~]" => $VARS['search']['value'],
|
||||
"email[~]" => $VARS['search']['value'],
|
||||
"statuscode[~]" => $VARS['search']['value'],
|
||||
"typecode[~]" => $VARS['search']['value']
|
||||
],
|
||||
"deleted" => 0
|
||||
]
|
||||
];
|
||||
}
|
||||
$where = $wherenolimit;
|
||||
$where["LIMIT"] = [$VARS['start'], $VARS['length']];
|
||||
} else {
|
||||
$where = ["LIMIT" => [$VARS['start'], $VARS['length']]];
|
||||
if (!$show_deleted) {
|
||||
$where["deleted"] = 0;
|
||||
}
|
||||
}
|
||||
if (!is_null($order)) {
|
||||
$where["ORDER"] = $order;
|
||||
}
|
||||
|
||||
|
||||
$users = $database->select('accounts', [
|
||||
"[>]acctstatus" => ['acctstatus' => 'statusid'],
|
||||
"[>]accttypes" => ['accttype' => 'typeid']
|
||||
], [
|
||||
'uid',
|
||||
'username',
|
||||
'realname',
|
||||
'email',
|
||||
'authsecret (2fa)',
|
||||
'acctstatus',
|
||||
'statuscode',
|
||||
'accttype',
|
||||
'typecode',
|
||||
'deleted'
|
||||
], $where);
|
||||
|
||||
|
||||
$out['status'] = "OK";
|
||||
if ($filter) {
|
||||
$recordsFiltered = $database->count('accounts', [
|
||||
"[>]acctstatus" => ['acctstatus' => 'statusid'],
|
||||
"[>]accttypes" => ['accttype' => 'typecode']
|
||||
], 'uid', $wherenolimit);
|
||||
} else {
|
||||
$recordsFiltered = $out['recordsTotal'];
|
||||
}
|
||||
$out['recordsFiltered'] = $recordsFiltered;
|
||||
for ($i = 0; $i < count($users); $i++) {
|
||||
$users[$i]["2fa"] = (is_empty($users[$i]["2fa"]) ? false : true);
|
||||
$users[$i]["editbtn"] = '<a class="btn btn-blue btn-sm" href="app.php?page=edituser&id=' . $users[$i]['uid'] . '"><i class="far fa-edit"></i> ' . lang("edit", false) . '</a>';
|
||||
}
|
||||
$out['users'] = $users;
|
||||
|
||||
echo json_encode($out);
|
@ -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,402 +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 a Portal 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 [];
|
||||
}
|
||||
}
|
@ -1,55 +1,65 @@
|
||||
<?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/.
|
||||
*/
|
||||
|
||||
// Whether to show debugging data in output.
|
||||
// DO NOT SET TO TRUE IN PRODUCTION!!!
|
||||
define("DEBUG", false);
|
||||
// Settings for the app.
|
||||
// Copy to settings.php and customize.
|
||||
|
||||
// AccountHub database connection settings
|
||||
// See http://medoo.in/api/new for info
|
||||
define("DB_TYPE", "mysql");
|
||||
define("DB_NAME", "accounthub");
|
||||
define("DB_SERVER", "localhost");
|
||||
define("DB_USER", "accounthub");
|
||||
define("DB_PASS", "");
|
||||
define("DB_CHARSET", "utf8");
|
||||
|
||||
// ManagePanel DB connection
|
||||
define("DB2_TYPE", "mysql");
|
||||
define("DB2_NAME", "managepanel");
|
||||
define("DB2_SERVER", "localhost");
|
||||
define("DB2_USER", "managepanel");
|
||||
define("DB2_PASS", "");
|
||||
define("DB2_CHARSET", "utf8");
|
||||
|
||||
// Name of the app.
|
||||
define("SITE_TITLE", "ManagePanel");
|
||||
|
||||
|
||||
// URL of the AccountHub API endpoint
|
||||
define("PORTAL_API", "http://localhost/accounthub/api.php");
|
||||
// URL of the AccountHub home page
|
||||
define("PORTAL_URL", "http://localhost/accounthub/home.php");
|
||||
// AccountHub API Key
|
||||
define("PORTAL_KEY", "123");
|
||||
|
||||
// For supported values, see http://php.net/manual/en/timezones.php
|
||||
define("TIMEZONE", "America/Denver");
|
||||
|
||||
// Base URL for site links.
|
||||
define('URL', '.');
|
||||
|
||||
// Use Captcheck on login screen
|
||||
// https://captcheck.netsyms.com
|
||||
define("CAPTCHA_ENABLED", FALSE);
|
||||
define('CAPTCHA_SERVER', 'https://captcheck.netsyms.com');
|
||||
|
||||
// See lang folder for language options
|
||||
define('LANGUAGE', "en_us");
|
||||
|
||||
|
||||
define("FOOTER_TEXT", "");
|
||||
define("COPYRIGHT_NAME", "Netsyms Technologies");
|
||||
$SETTINGS = [
|
||||
// Whether to output debugging info like PHP notices, warnings,
|
||||
// and stacktraces.
|
||||
// Turning this on in production is a security risk and can sometimes break
|
||||
// things, such as JSON output where extra content is not expected.
|
||||
"debug" => false,
|
||||
// Database connection settings
|
||||
// See http://medoo.in/api/new for info
|
||||
// AccountHub database
|
||||
"database" => [
|
||||
"type" => "mysql",
|
||||
"name" => "accounthub",
|
||||
"server" => "localhost",
|
||||
"user" => "app",
|
||||
"password" => "",
|
||||
"charset" => "utf8"
|
||||
],
|
||||
// ManagePanel database
|
||||
"database2" => [
|
||||
"type" => "mysql",
|
||||
"name" => "managepanel",
|
||||
"server" => "localhost",
|
||||
"user" => "app",
|
||||
"password" => "",
|
||||
"charset" => "utf8"
|
||||
],
|
||||
// Name of the app.
|
||||
"site_title" => "ManagePanel",
|
||||
// Settings for connecting to the AccountHub server.
|
||||
"accounthub" => [
|
||||
// URL for the API endpoint
|
||||
"api" => "http://localhost/accounthub/api/",
|
||||
// URL of the home page
|
||||
"home" => "http://localhost/accounthub/home.php",
|
||||
// API key
|
||||
"key" => "123"
|
||||
],
|
||||
// List of required user permissions to access this app.
|
||||
"permissions" => [
|
||||
"ADMIN"
|
||||
],
|
||||
// For supported values, see http://php.net/manual/en/timezones.php
|
||||
"timezone" => "America/Denver",
|
||||
// Language to use for localization. See langs folder to add a language.
|
||||
"language" => "en",
|
||||
// Shown in the footer of all the pages.
|
||||
"footer_text" => "",
|
||||
// Also shown in the footer, but with "Copyright <current_year>" in front.
|
||||
"copyright" => "Netsyms Technologies",
|
||||
// Base URL for building links relative to the location of the app.
|
||||
// Only used when there's no good context for the path.
|
||||
// The default is almost definitely fine.
|
||||
"url" => "."
|
||||
];
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,15 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
.banner-image {
|
||||
max-height: 100px;
|
||||
margin: 2em auto;
|
||||
border: 1px solid grey;
|
||||
border-radius: 15%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 10em;
|
||||
text-align: center;
|
||||
}
|
@ -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.6.0 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{filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2.5em}.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:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.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
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue