Merge BusinessAppTemplate (new settings.php format)

# Conflicts:
#	api.php
#	api/apisettings.php
#	api/index.php
#	app.php
#	index.php
#	langs/en/titles.json
#	lib/Login.lib.php
#	lib/Notifications.lib.php
#	lib/User.lib.php
#	mobile/index.php
#	required.php
#	settings.template.php
master
Skylar Ittner 5 years ago
commit bb5639c447

@ -4,4 +4,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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 __DIR__ . "/api/index.php";
// Load in new API from legacy location (a.k.a. here)
require __DIR__ . "/api/index.php";

@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
$apps = EXTERNAL_APPS; $apps = $SETTINGS['apps'];
// Format paths as absolute URLs // Format paths as absolute URLs
foreach ($apps as $k => $v) { foreach ($apps as $k => $v) {
if (strpos($apps[$k]['url'], "http") === FALSE) { if (strpos($apps[$k]['url'], "http") === FALSE) {

@ -6,4 +6,4 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
exitWithJson(["status" => "OK", "mobile" => MOBILE_ENABLED]); exitWithJson(["status" => "OK", "mobile" => $SETTINGS['mobile_enabled']]);

@ -1,5 +1,4 @@
<?php <?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 * 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/. */
@ -14,7 +13,7 @@ if ($_SESSION['loggedin'] != true) {
require_once __DIR__ . "/pages.php"; require_once __DIR__ . "/pages.php";
$pageid = "home"; $pageid = "home";
if (isset($_GET['page']) && !empty($_GET['page'])) { if (!empty($_GET['page'])) {
$pg = strtolower($_GET['page']); $pg = strtolower($_GET['page']);
$pg = preg_replace('/[^0-9a-z_]/', "", $pg); $pg = preg_replace('/[^0-9a-z_]/', "", $pg);
if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) { if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) {
@ -40,7 +39,7 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <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 rel="icon" href="static/img/logo.svg">
@ -66,28 +65,35 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
<?php <?php
// Alert messages // Alert messages
if (isset($_GET['msg']) && !empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) { if (!empty($_GET['msg'])) {
// optional string generation argument if (array_key_exists($_GET['msg'], MESSAGES)) {
if (!isset($_GET['arg']) || empty($_GET['arg'])) { // optional string generation argument
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false); 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 { } else {
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false); // We don't have a message for this, so just assume an error and escape stuff.
} $alertmsg = htmlentities($Strings->get($_GET['msg'], false));
$alerttype = MESSAGES[$_GET['msg']]['type']; $alerticon = "times";
$alerticon = "square-o"; $alerttype = "danger";
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;
} }
echo <<<END echo <<<END
<div class="row justify-content-center" id="msg-alert-box"> <div class="row justify-content-center" id="msg-alert-box">
@ -121,7 +127,7 @@ END;
</button> </button>
<a class="navbar-brand py-0 mr-auto" href="app.php"> <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" /> <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> </a>
<div class="collapse navbar-collapse py-0" id="navbar-collapse"> <div class="collapse navbar-collapse py-0" id="navbar-collapse">
@ -177,8 +183,8 @@ END;
?> ?>
</div> </div>
<div class="footer"> <div class="footer">
<?php echo FOOTER_TEXT; ?><br /> <?php echo $SETTINGS['footer_text']; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?> Copyright &copy; <?php echo date('Y'); ?> <?php echo $SETTINGS['copyright']; ?>
</div> </div>
</div> </div>
<script src="static/js/jquery-3.3.1.min.js"></script> <script src="static/js/jquery-3.3.1.min.js"></script>
@ -194,4 +200,4 @@ END;
} }
?> ?>
</body> </body>
</html> </html>

@ -54,12 +54,12 @@ switch ($_GET['type']) {
die("400 Bad Request: feed parameter must have a value of \"rss\", \"rss1\", \"rss2\" or \"atom\"."); die("400 Bad Request: feed parameter must have a value of \"rss\", \"rss1\", \"rss2\" or \"atom\".");
} }
$feed->setTitle($Strings->build("Notifications from server for user", ['server' => SITE_TITLE, 'user' => $user->getName()], false)); $feed->setTitle($Strings->build("Notifications from server for user", ['server' => $SETTINGS['site_title'], 'user' => $user->getName()], false));
if (strpos(URL, "http") === 0) { if (strpos($SETTINGS['url'], "http") === 0) {
$url = URL; $url = $SETTINGS['url'];
} else { } else {
$url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . URL; $url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . $SETTINGS['url'];
} }
$feed->setLink($url); $feed->setLink($url);

@ -20,7 +20,7 @@ if (empty($VARS['progress'])) {
// Easy way to remove "undefined" warnings. // Easy way to remove "undefined" warnings.
} else if ($VARS['progress'] == "1") { } else if ($VARS['progress'] == "1") {
engageRateLimit(); engageRateLimit();
if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) { if (!$SETTINGS['captcha']['enabled'] || ($SETTINGS['captcha']['enabled'] && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], $SETTINGS['captcha']['server'] . "/api.php"))) {
$autherror = ""; $autherror = "";
$user = User::byUsername($VARS['username']); $user = User::byUsername($VARS['username']);
if ($user->exists()) { if ($user->exists()) {
@ -44,7 +44,7 @@ if (empty($VARS['progress'])) {
break; break;
case "ALERT_ON_ACCESS": case "ALERT_ON_ACCESS":
$mail_resp = $user->sendAlertEmail(); $mail_resp = $user->sendAlertEmail();
if (DEBUG) { if ($SETTINGS['debug']) {
var_dump($mail_resp); var_dump($mail_resp);
} }
$username_ok = true; $username_ok = true;
@ -143,16 +143,16 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <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 rel="icon" href="static/img/logo.svg">
<link href="static/css/bootstrap.min.css" rel="stylesheet"> <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/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/index.css" rel="stylesheet"> <link href="static/css/index.css" rel="stylesheet">
<?php if (CAPTCHA_ENABLED) { ?> <?php if ($SETTINGS['captcha']['enabled']) { ?>
<script src="<?php echo CAPTCHA_SERVER ?>/captcheck.dist.js"></script> <script src="<?php echo $SETTINGS['captcha']['server'] ?>/captcheck.dist.js"></script>
<?php } ?> <?php } ?>
</head> </head>
<body> <body>
<div class="row justify-content-center"> <div class="row justify-content-center">
@ -197,7 +197,7 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
?> ?>
<input type="text" class="form-control" name="username" placeholder="<?php $Strings->get("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br /> <input type="text" class="form-control" name="username" placeholder="<?php $Strings->get("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br /> <input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<?php if (CAPTCHA_ENABLED) { ?> <?php if ($SETTINGS['captcha']['enabled']) { ?>
<div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div> <div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
<br /> <br />
<?php } ?> <?php } ?>
@ -206,7 +206,7 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
} else if ($multiauth) { } else if ($multiauth) {
?> ?>
<div class="alert alert-info"> <div class="alert alert-info">
<?php $Strings->get("2fa prompt"); ?> <?php $Strings->get("2fa prompt"); ?>
</div> </div>
<input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br /> <input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="hidden" name="progress" value="2" /> <input type="hidden" name="progress" value="2" />
@ -222,18 +222,17 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
} }
?> ?>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<?php $Strings->get("continue"); ?> <?php $Strings->get("continue"); ?>
</button> </button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<?php echo FOOTER_TEXT; ?><br /> <?php echo $SETTINGS['footer_text']; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?> Copyright &copy; <?php echo date('Y'); ?> <?php echo $SETTINGS['copyright']; ?>
</div> </div>
</div> <script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/jquery-3.3.1.min.js"></script> <script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script> </body>
</body>
</html> </html>

@ -0,0 +1,257 @@
<?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]\"";
$itemhtml = "";
$itemhtml .= <<<ITEMTOP
\n\n <div class="col-12 col-md-$item[width]">
<div class="form-group mb-3">
<label class="mb-0">$item[label]:</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="$item[icon]"></i></span>
</div>
ITEMTOP;
if (empty($item['type']) || $item['type'] != "select") {
$itemhtml .= <<<INPUT
\n <input type="$item[type]" name="$item[name]" $id class="form-control" aria-label="$item[label]" minlength="$item[minlength]" maxlength="$item[maxlength]" $pattern value="$item[value]" $required />
INPUT;
} else {
$itemhtml .= <<<SELECT
\n <select class="form-control" name="$item[name]" aria-label="$item[label]" $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>";
}
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;
}
}

@ -119,7 +119,7 @@ class User {
* @throws WeakPasswordException * @throws WeakPasswordException
*/ */
function changePassword(string $old, string $new, string $new2) { function changePassword(string $old, string $new, string $new2) {
global $database; global $database, $SETTINGS;
if ($old == $new) { if ($old == $new) {
throw new PasswordMatchException(); throw new PasswordMatchException();
} }
@ -137,7 +137,7 @@ class User {
if ($passrank !== FALSE) { if ($passrank !== FALSE) {
throw new WeakPasswordException(); throw new WeakPasswordException();
} }
if (strlen($new) < MIN_PASSWORD_LENGTH) { if (strlen($new) < $SETTINGS['min_password_length']) {
throw new WeakPasswordException(); throw new WeakPasswordException();
} }
@ -171,10 +171,11 @@ class User {
* @return string OTP provisioning URI (for generating a QR code) * @return string OTP provisioning URI (for generating a QR code)
*/ */
function generate2fa(): string { function generate2fa(): string {
global $SETTINGS;
$secret = random_bytes(20); $secret = random_bytes(20);
$encoded_secret = Base32::encode($secret); $encoded_secret = Base32::encode($secret);
$totp = new TOTP((empty($this->email) ? $this->realname : $this->email), $encoded_secret); $totp = new TOTP((empty($this->email) ? $this->realname : $this->email), $encoded_secret);
$totp->setIssuer(SYSTEM_NAME); $totp->setIssuer($SETTINGS['system_name']);
return $totp->getProvisioningUri(); return $totp->getProvisioningUri();
} }
@ -214,7 +215,11 @@ class User {
return new AccountStatus($statuscode); return new AccountStatus($statuscode);
} }
function sendAlertEmail(string $appname = SITE_TITLE) { function sendAlertEmail(string $appname = null) {
global $SETTINGS;
if (is_null($appname)) {
$appname = $SETTINGS['site_title'];
}
if (empty(ADMIN_EMAIL) || filter_var(ADMIN_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) { if (empty(ADMIN_EMAIL) || filter_var(ADMIN_EMAIL, FILTER_VALIDATE_EMAIL) === FALSE) {
return "invalid_to_email"; return "invalid_to_email";
} }
@ -224,19 +229,19 @@ class User {
$mail = new PHPMailer; $mail = new PHPMailer;
if (DEBUG) { if ($SETTINGS['debug']) {
$mail->SMTPDebug = 2; $mail->SMTPDebug = 2;
} }
if (USE_SMTP) { if ($SETTINGS['email']['use_smtp']) {
$mail->isSMTP(); $mail->isSMTP();
$mail->Host = SMTP_HOST; $mail->Host = $SETTINGS['email']['host'];
$mail->SMTPAuth = SMTP_AUTH; $mail->SMTPAuth = $SETTINGS['email']['auth'];
$mail->Username = SMTP_USER; $mail->Username = $SETTINGS['email']['user'];
$mail->Password = SMTP_PASS; $mail->Password = $SETTINGS['email']['password'];
$mail->SMTPSecure = SMTP_SECURE; $mail->SMTPSecure = $SETTINGS['email']['secure'];
$mail->Port = SMTP_PORT; $mail->Port = $SETTINGS['email']['port'];
if (SMTP_ALLOW_INVALID_CERTIFICATE) { if ($SETTINGS['email']['allow_invalid_certificate']) {
$mail->SMTPOptions = array( $mail->SMTPOptions = array(
'ssl' => array( 'ssl' => array(
'verify_peer' => false, 'verify_peer' => false,

@ -18,7 +18,7 @@ if ($VARS['action'] == "ping") {
exit(json_encode(["status" => "OK"])); exit(json_encode(["status" => "OK"]));
} }
if (MOBILE_ENABLED !== TRUE) { if ($SETTINGS['mobile_enabled'] !== TRUE) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("mobile login disabled", false)])); exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("mobile login disabled", false)]));
} }
@ -106,7 +106,7 @@ switch ($VARS['action']) {
Log::insert(LogType::MOBILE_LOGIN_FAILED, null, "Username: " . $username . ", Key: " . $key); Log::insert(LogType::MOBILE_LOGIN_FAILED, null, "Username: " . $username . ", Key: " . $key);
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)])); exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)]));
case "listapps": case "listapps":
$apps = EXTERNAL_APPS; $apps = $SETTINGS['apps'];
// Format paths as absolute URLs // Format paths as absolute URLs
foreach ($apps as $k => $v) { foreach ($apps as $k => $v) {
if (strpos($apps[$k]['url'], "http") === FALSE) { if (strpos($apps[$k]['url'], "http") === FALSE) {

@ -7,7 +7,7 @@
// List of pages and metadata // List of pages and metadata
define("PAGES", [ define("PAGES", [
"home" => [ "home" => [
"title" => "home", "title" => "Home",
"navbar" => true, "navbar" => true,
"icon" => "fas fa-home", "icon" => "fas fa-home",
"styles" => [ "styles" => [

@ -6,36 +6,9 @@
*/ */
?> ?>
<?php
/*
<div class="d-flex justify-content-center flex-wrap">
<?php
foreach (EXTERNAL_APPS as $a) {
?>
<div class="app-dock-item m-2 mobile-app-hide">
<p class="mb-0">
<a href="<?php echo $a['url']; ?>">
<img class="img-responsive app-icon" src="<?php
if (strpos($a['icon'], "http") !== 0) {
echo $a['url'] . $a['icon'];
} else {
echo $a['icon'];
}
?>"/>
<span class="d-block text-center"><?php echo $a['title']; ?></span>
</a>
</p>
</div>
<?php
}
?>
</div>
*/
?>
<div class="row mt-2"> <div class="row mt-2">
<?php <?php
foreach (EXTERNAL_APPS as $a) { foreach ($SETTINGS['apps'] as $a) {
if (!isset($a['card'])) { if (!isset($a['card'])) {
continue; continue;
} }
@ -141,9 +114,9 @@
<?php <?php
$ts = strtotime($n['timestamp']); $ts = strtotime($n['timestamp']);
if (time() - $ts < 60 * 60 * 12) { if (time() - $ts < 60 * 60 * 12) {
echo date(TIME_FORMAT, $ts); echo date($SETTINGS['time_format'], $ts);
} else { } else {
echo date(DATETIME_FORMAT, $ts); echo date($SETTINGS['datetime_format'], $ts);
} }
?> ?>
</div> </div>

@ -30,7 +30,7 @@ $user = new User($_SESSION['uid']);
</div> </div>
</div> </div>
<?php <?php
if (STATION_KIOSK) { if ($SETTINGS['station_kiosk']) {
?> ?>
<div class="col-sm-6 col-lg-4"> <div class="col-sm-6 col-lg-4">
<div class="card mb-4"> <div class="card mb-4">
@ -71,8 +71,8 @@ $user = new User($_SESSION['uid']);
<?php <?php
} else if (!empty($_GET['2fa']) && $_GET['2fa'] == "generate") { } else if (!empty($_GET['2fa']) && $_GET['2fa'] == "generate") {
$codeuri = $user->generate2fa(); $codeuri = $user->generate2fa();
$label = SYSTEM_NAME . ":" . is_null($user->getEmail()) ? $user->getName() : $user->getEmail(); $label = $SETTINGS['system_name'] . ":" . is_null($user->getEmail()) ? $user->getName() : $user->getEmail();
$issuer = SYSTEM_NAME; $issuer = $SETTINGS['system_name'];
$qrCode = new QrCode($codeuri); $qrCode = new QrCode($codeuri);
$qrCode->setWriterByName('svg'); $qrCode->setWriterByName('svg');
$qrCode->setSize(550); $qrCode->setSize(550);

@ -25,10 +25,10 @@ if (!empty($_GET['delsynccode'])) {
$code = strtoupper(substr(md5(mt_rand() . uniqid("", true)), 0, 20)); $code = strtoupper(substr(md5(mt_rand() . uniqid("", true)), 0, 20));
$desc = htmlspecialchars($_POST['desc']); $desc = htmlspecialchars($_POST['desc']);
$database->insert('mobile_codes', ['uid' => $_SESSION['uid'], 'code' => $code, 'description' => $desc]); $database->insert('mobile_codes', ['uid' => $_SESSION['uid'], 'code' => $code, 'description' => $desc]);
if (strpos(URL, "http") === 0) { if (strpos($SETTINGS['url'], "http") === 0) {
$url = URL . "mobile/index.php"; $url = $SETTINGS['url'] . "mobile/index.php";
} else { } else {
$url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . URL . "mobile/index.php"; $url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . $SETTINGS['url'] . "mobile/index.php";
} }
$encodedurl = str_replace("/", "\\", $url); $encodedurl = str_replace("/", "\\", $url);
$codeuri = "bizsync://" . $encodedurl . "/" . $_SESSION['username'] . "/" . $code; $codeuri = "bizsync://" . $encodedurl . "/" . $_SESSION['username'] . "/" . $code;
@ -68,7 +68,7 @@ if (!empty($_GET['delsynccode'])) {
$activecodes = $database->select("mobile_codes", ["codeid", "code", "description"], ["uid" => $_SESSION['uid']]); $activecodes = $database->select("mobile_codes", ["codeid", "code", "description"], ["uid" => $_SESSION['uid']]);
?> ?>
<p class="card-text"> <p class="card-text">
<?php $Strings->build("sync explained", ["site_name" => SITE_TITLE]); ?> <?php $Strings->build("sync explained", ["site_name" => $SETTINGS['site_title']]); ?>
</p> </p>
<form action="app.php?page=sync&mobilecode=generate" method="POST"> <form action="app.php?page=sync&mobilecode=generate" method="POST">
<input type="text" name="desc" class="form-control" placeholder="<?php $Strings->get("sync code name"); ?>" required /> <input type="text" name="desc" class="form-control" placeholder="<?php $Strings->get("sync code name"); ?>" required />
@ -142,10 +142,10 @@ if (!empty($_GET['delsynccode'])) {
$database->insert('userkeys', ['uid' => $_SESSION['uid'], 'typeid' => 1, 'created' => date('Y-m-d H:i:s'), 'key' => $key]); $database->insert('userkeys', ['uid' => $_SESSION['uid'], 'typeid' => 1, 'created' => date('Y-m-d H:i:s'), 'key' => $key]);
} }
if (strpos(URL, "http") === 0) { if (strpos($SETTINGS['url'], "http") === 0) {
$url = URL; $url = $SETTINGS['url'];
} else { } else {
$url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . URL; $url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . $SETTINGS['url'];
} }
$url = $url . "feed.php?key=$key"; $url = $url . "feed.php?key=$key";
?> ?>

@ -33,7 +33,7 @@ session_start(); // stick some cookies in it
// renew session cookie // renew session cookie
setcookie(session_name(), session_id(), time() + $session_length, "/", false, false); setcookie(session_name(), session_id(), time() + $session_length, "/", false, false);
$captcha_server = (CAPTCHA_ENABLED === true ? preg_replace("/http(s)?:\/\//", "", CAPTCHA_SERVER) : ""); $captcha_server = ($SETTINGS['captcha']['enabled'] === true ? preg_replace("/http(s)?:\/\//", "", $SETTINGS['captcha']['server']) : "");
if ($_SESSION['mobile'] === TRUE) { if ($_SESSION['mobile'] === TRUE) {
header("Content-Security-Policy: " header("Content-Security-Policy: "
. "default-src 'self';" . "default-src 'self';"
@ -70,7 +70,7 @@ foreach ($libs as $lib) {
require_once $lib; require_once $lib;
} }
$Strings = new Strings(LANGUAGE); $Strings = new Strings($SETTINGS['language']);
/** /**
* Kill off the running process and spit out an error message * Kill off the running process and spit out an error message
@ -94,7 +94,7 @@ function sendError($error) {
. "<p>" . htmlspecialchars($error) . "</p>"); . "<p>" . htmlspecialchars($error) . "</p>");
} }
date_default_timezone_set(TIMEZONE); date_default_timezone_set($SETTINGS['timezone']);
// Database settings // Database settings
// Also inits database and stuff // Also inits database and stuff
@ -103,12 +103,12 @@ use Medoo\Medoo;
$database; $database;
try { try {
$database = new Medoo([ $database = new Medoo([
'database_type' => DB_TYPE, 'database_type' => $SETTINGS['database']['type'],
'database_name' => DB_NAME, 'database_name' => $SETTINGS['database']['name'],
'server' => DB_SERVER, 'server' => $SETTINGS['database']['server'],
'username' => DB_USER, 'username' => $SETTINGS['database']['user'],
'password' => DB_PASS, 'password' => $SETTINGS['database']['password'],
'charset' => DB_CHARSET 'charset' => $SETTINGS['database']['charset']
]); ]);
} catch (Exception $ex) { } catch (Exception $ex) {
//header('HTTP/1.1 500 Internal Server Error'); //header('HTTP/1.1 500 Internal Server Error');
@ -116,7 +116,7 @@ try {
} }
if (!DEBUG) { if (!$SETTINGS['debug']) {
error_reporting(0); error_reporting(0);
} else { } else {
error_reporting(E_ALL); error_reporting(E_ALL);
@ -157,10 +157,9 @@ function checkDBError($specials = []) {
} }
} }
function redirectIfNotLoggedIn() { function redirectIfNotLoggedIn() {
if ($_SESSION['loggedin'] !== TRUE) { if ($_SESSION['loggedin'] !== TRUE) {
header('Location: ' . URL . '/login.php'); header('Location: ' . $SETTINGS['url'] . '/index.php');
die(); die();
} }
} }
@ -186,4 +185,4 @@ function engageRateLimit() {
// Add a record for the IP address // Add a record for the IP address
$database->insert('rate_limit', ["ipaddr" => IPUtils::getClientIP(), "lastaction" => date("Y-m-d H:i:s")]); $database->insert('rate_limit', ["ipaddr" => IPUtils::getClientIP(), "lastaction" => date("Y-m-d H:i:s")]);
} }
} }

@ -1,172 +1,164 @@
<?php <?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 * 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. // Settings for the app.
// DO NOT SET TO TRUE IN PRODUCTION!!! // Copy to settings.php and customize.
define("DEBUG", false);
$SETTINGS = [
// Database connection settings // Whether to output debugging info like PHP notices, warnings,
// See http://medoo.in/api/new for info // and stacktraces.
define("DB_TYPE", "mysql"); // Turning this on in production is a security risk and can sometimes break
define("DB_NAME", "accounthub"); // things, such as JSON output where extra content is not expected.
define("DB_SERVER", "localhost"); "debug" => false,
define("DB_USER", "accounthub"); // Database connection settings
define("DB_PASS", ""); // See http://medoo.in/api/new for info
define("DB_CHARSET", "utf8"); "database" => [
"type" => "mysql",
define("SITE_TITLE", "AccountHub"); "name" => "accounthub",
"server" => "localhost",
// Used to identify the system in OTP and other places "user" => "accounthub",
define("SYSTEM_NAME", "Netsyms SSO Demo"); "password" => "",
"charset" => "utf8"
// For supported values, see http://php.net/manual/en/timezones.php
define("TIMEZONE", "America/Denver");
// Allow or prevent users from logging in via the mobile app.
define("MOBILE_ENABLED", TRUE);
// Base URL for site links.
define('URL', 'http://localhost/accounthub');
// 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");
// List of available applications, icons, and other info.
// Used in the mobile app and in the "dock" in AccountHub.
define('EXTERNAL_APPS', [
"accounthub" => [
"url" => "/accounthub",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => SITE_TITLE
], ],
"qwikclock" => [ // Name of the app.
"url" => "/qwikclock", "site_title" => "AccountHub",
"mobileapi" => "/mobile/index.php", // Used to identify the system in OTP and other places
"icon" => "/static/img/logo.svg", "system_name" => "Netsyms AccountHub",
"title" => "QwikClock", // Allow login from the Netsyms mobile app
"station_features" => [ "mobile_enabled" => true,
"qwikclock_punchinout", // For supported values, see http://php.net/manual/en/timezones.php
"qwikclock_myshifts", "timezone" => "America/Denver",
"qwikclock_jobs" // List of external apps connected to this system.
// This list is used for generating the dashboard cards and in the
// mobile app.
"apps" => [
"accounthub" => [
"url" => "/accounthub",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => $SETTINGS['site_title']
], ],
"card" => [ "qwikclock" => [
"color" => "blue", "url" => "/qwikclock",
"string" => "Punch in and check work schedule" "mobileapi" => "/mobile/index.php",
] "icon" => "/static/img/logo.svg",
], "title" => "QwikClock",
"binstack" => [ "station_features" => [
"url" => "/binstack", "qwikclock_punchinout",
"mobileapi" => "/mobile/index.php", "qwikclock_myshifts",
"icon" => "/static/img/logo.svg", "qwikclock_jobs"
"title" => "BinStack", ],
"card" => [ "card" => [
"color" => "green", "color" => "blue",
"string" => "Manage physical items" "string" => "Punch in and check work schedule"
] ]
], ],
"newspen" => [ "binstack" => [
"url" => "/newspen", "url" => "/binstack",
"mobileapi" => "/mobile/index.php", "mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg", "icon" => "/static/img/logo.svg",
"title" => "NewsPen", "title" => "BinStack",
"card" => [ "card" => [
"color" => "purple", "color" => "green",
"string" => "Create and publish e-newsletters" "string" => "Manage physical items"
] ]
], ],
"managepanel" => [ "newspen" => [
"url" => "/managepanel", "url" => "/newspen",
"mobileapi" => "/mobile/index.php", "mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg", "icon" => "/static/img/logo.svg",
"title" => "ManagePanel", "title" => "NewsPen",
"card" => [ "card" => [
"color" => "brown", "color" => "purple",
"string" => "Manage users, permissions, and security" "string" => "Create and publish e-newsletters"
] ]
], ],
"nickelbox" => [ "managepanel" => [
"url" => "/nickelbox", "url" => "/managepanel",
"mobileapi" => "/mobile/index.php", "mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg", "icon" => "/static/img/logo.svg",
"title" => "NickelBox", "title" => "ManagePanel",
"card" => [ "card" => [
"color" => "light-green", "color" => "brown",
"text" => "dark", "string" => "Manage users, permissions, and security"
"string" => "Checkout customers and manage online orders" ]
],
"nickelbox" => [
"url" => "/nickelbox",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "NickelBox",
"card" => [
"color" => "light-green",
"text" => "dark",
"string" => "Checkout customers and manage online orders"
]
],
"sitewriter" => [
"url" => "/sitewriter",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "SiteWriter",
"card" => [
"color" => "light-blue",
"string" => "Build websites and manage contact form messages"
]
],
"taskfloor" => [
"url" => "/taskfloor",
"mobileapi" => "/mobile/index.php",
"icon" => "/static/img/logo.svg",
"title" => "TaskFloor",
"station_features" => [
"taskfloor_viewtasks",
"taskfloor_viewmessages"
],
"card" => [
"color" => "blue-grey",
"string" => "Track jobs and assigned tasks"
]
] ]
], ],
"sitewriter" => [ // Settings for sending emails.
"url" => "/sitewriter", "email" => [
"mobileapi" => "/mobile/index.php", // If false, will use PHP mail() instead of a server
"icon" => "/static/img/logo.svg", "use_smtp" => true,
"title" => "SiteWriter", // Admin email for alerts
"card" => [ "admin_email" => "",
"color" => "light-blue", "from" => "alert-noreply@example.com",
"string" => "Build websites and manage contact form messages" "host" => "",
] "auth" => true,
"port" => 587,
"secure" => "tls",
"user" => "",
"password" => "",
"allow_invalid_certificate" => true
], ],
"taskfloor" => [ "min_password_length" => 8,
"url" => "/taskfloor", // Show or hide the Station PIN setup option.
"mobileapi" => "/mobile/index.php", "station_kiosk" => true,
"icon" => "/static/img/logo.svg", // Used for notification timestamp display.
"title" => "TaskFloor", "datetime_format" => "M j, g:i a",
"station_features" => [ "time_format" => "g:i",
"taskfloor_viewtasks", // Use Captcheck on login screen to slow down bots
"taskfloor_viewmessages" // https://captcheck.netsyms.com
], "captcha" => [
"card" => [ "enabled" => false,
"color" => "blue-grey", "server" => "https://captcheck.netsyms.com"
"string" => "Track jobs and assigned tasks"
]
], ],
]); // Language to use for localization. See langs folder to add a language.
"language" => "en",
// Show or hide the Station PIN setup option. // Shown in the footer of all the pages.
define("STATION_KIOSK", true); "footer_text" => "",
// Also shown in the footer, but with "Copyright <current_year>" in front.
// Used for notification timestamp display. "copyright" => "Netsyms Technologies",
define("DATETIME_FORMAT", "M j, g:i a"); // Base URL for building links relative to the location of the app.
define("TIME_FORMAT", "g:i"); // Only used when there's no good context for the path.
// The default is almost definitely fine.
"url" => "."
// Email settings for receiving admin alerts. ];
define("USE_SMTP", TRUE); // if FALSE, will use PHP's mail() instead
define("ADMIN_EMAIL", "");
define("FROM_EMAIL", "alert-noreply@apps.biz.netsyms.com");
define("SMTP_HOST", "");
define("SMTP_AUTH", true);
define("SMTP_PORT", 587);
define("SMTP_SECURE", 'tls');
define("SMTP_USER", "");
define("SMTP_PASS", "");
define("SMTP_ALLOW_INVALID_CERTIFICATE", TRUE);
// Minimum length for new passwords
// The system checks new passwords against the 500 worst passwords and rejects
// any matches.
// If you want to have additional password requirements, go edit action.php.
// However, all that does is encourage people to use the infamous
// "post-it password manager". See also https://xkcd.com/936/ and
// http://stackoverflow.com/a/34166252 for reasons why forcing passwords
// like CaPs45$% is not actually a great idea.
// Encourage users to use 2-factor auth whenever possible.
define("MIN_PASSWORD_LENGTH", 8);
// Maximum number of rows to get in a query.
define("QUERY_LIMIT", 1000);
define("FOOTER_TEXT", "");
define("COPYRIGHT_NAME", "Netsyms Technologies");
//////////////////////////////////////////////////////////////

@ -1,5 +1,5 @@
/*! /*!
* Font Awesome Free 5.3.1 by @fontawesome - https://fontawesome.com * 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) * 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:fa-spin 2s infinite linear}.fa-pulse{animation:fa-spin 1s infinite steps(8)}@keyframes fa-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1em}.svg-inline--fa.fa-stack-2x{height:2em;width:2em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} .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
Loading…
Cancel
Save