Merge BusinessAppTemplate

master
Skylar Ittner 6 years ago
commit d8613f0baa

@ -2,4 +2,4 @@ NickelBox
=========
NickelBox is a point of sale app. It integrates with BinStack for inventory
management.
management.

@ -8,7 +8,6 @@
* Make things happen when buttons are pressed and forms submitted.
*/
require_once __DIR__ . "/required.php";
require_once __DIR__ . "/lib/userinfo.php";
if ($VARS['action'] !== "signout") {
dieifnotloggedin();
@ -39,7 +38,7 @@ switch ($VARS['action']) {
global $VARS, $binstack, $error, $oktx;
if (empty($VARS['items'])) {
$error = lang("no items", false);
$error = $Strings->get("no items", false);
return false;
}
@ -56,7 +55,7 @@ switch ($VARS['action']) {
$txid = $VARS['txid'];
$cashid = $database->get('transactions', 'cashid', ['txid' => $txid]);
if (!$database->has('cash_drawer', ['AND' => ['cashid' => $cashid, 'close' => null]])) {
$error = lang("cash already closed", false);
$error = $Strings->get("cash already closed", false);
return false;
}
// Nuke the payments to make room for their replacements
@ -72,15 +71,15 @@ switch ($VARS['action']) {
}
if ($customer != "" && !$database->has('customers', ['customerid' => $customer])) {
$error = lang("invalid customer", false);
$error = $Strings->get("invalid customer", false);
return false;
}
if ($register != "" && !$database->has('registers', ['registerid' => $register])) {
$error = lang("invalid register", false);
$error = $Strings->get("invalid register", false);
return false;
}
if ($register != "" && !$database->has('cash_drawer', ['AND' => ['registerid' => $register, 'close' => null]])) {
$error = lang("cash not open", false);
$error = $Strings->get("cash not open", false);
return false;
}
@ -94,19 +93,19 @@ switch ($VARS['action']) {
foreach ($items as $i) {
$totalcharge += $i['each'] * $i['qty'];
if (!$binstack->has('items', ['itemid' => $i['id']])) {
$error = lang("invalid item", false);
$error = $Strings->get("invalid item", false);
return false;
}
}
foreach ($payments as $p) {
if (!$database->has('payment_types', ['typename' => $p['type']])) {
$error = lang("invalid payment type", false);
$error = $Strings->get("invalid payment type", false);
return false;
}
$totalpaid += $p['amount'];
if ($p['type'] == "giftcard") {
if (!$database->has('certificates', ['AND' => ['amount[>=]' => $p['amount'], 'deleted[!]' => 1, 'certcode' => $p['code']]])) {
$error = lang("invalid giftcard", false);
$error = $Strings->get("invalid giftcard", false);
return false;
}
}
@ -120,7 +119,7 @@ switch ($VARS['action']) {
}
if ($totalcharge > $totalpaid) {
$error = lang("insufficient payment", false);
$error = $Strings->get("insufficient payment", false);
return false;
}
@ -225,15 +224,15 @@ switch ($VARS['action']) {
$cashid = null;
if ($customer != "" && !$database->has('customers', ['customerid' => $customer])) {
$error = lang("invalid customer", false);
$error = $Strings->get("invalid customer", false);
return false;
}
if ($register != "" && !$database->has('registers', ['registerid' => $register])) {
$error = lang("invalid register", false);
$error = $Strings->get("invalid register", false);
return false;
}
if ($register != "" && !$database->has('cash_drawer', ['AND' => ['registerid' => $register, 'close' => null]])) {
$error = lang("cash not open", false);
$error = $Strings->get("cash not open", false);
return false;
}
@ -246,19 +245,19 @@ switch ($VARS['action']) {
foreach ($items as $i) {
$totaldue += $i['each'] * $i['qty'];
if (!$binstack->has('items', ['itemid' => $i['id']])) {
$error = lang("invalid item", false);
$error = $Strings->get("invalid item", false);
return false;
}
}
foreach ($payments as $p) {
if (!$database->has('payment_types', ['typename' => $p['type']])) {
$error = lang("invalid payment type", false);
$error = $Strings->get("invalid payment type", false);
return false;
}
$totalrefund += $p['amount'];
if ($p['type'] == "giftcard") {
if (!$database->has('certificates', ['AND' => ['amount[>=]' => $p['amount'], 'deleted[!]' => 1, 'certcode' => $p['code']]])) {
$error = lang("invalid giftcard", false);
$error = $Strings->get("invalid giftcard", false);
return false;
}
}
@ -319,7 +318,7 @@ switch ($VARS['action']) {
$txid = $VARS['txid'];
$cashid = $database->get('transactions', 'cashid', ['txid' => $txid]);
if (!$database->has('cash_drawer', ['AND' => ['cashid' => $cashid, 'close' => null]])) {
$error = lang("cash already closed", false);
$error = $Strings->get("cash already closed", false);
}
$database->action(function ($database) {
@ -350,7 +349,7 @@ switch ($VARS['action']) {
$database->delete('transactions', ['txid' => $txid, 'LIMIT' => 1]);
});
} else {
$error = lang("invalid parameters", false);
$error = $Strings->get("invalid parameters", false);
}
if (!is_null($error)) {
exit(json_encode(["status" => "ERROR", "message" => $error]));
@ -429,10 +428,10 @@ switch ($VARS['action']) {
$transactions[$i]['editable'] = false;
}
if (!is_null($transactions[$i]['cashierid'])) {
$cashier = getUserByID($transactions[$i]['cashierid']);
$cashier = new User($transactions[$i]['cashierid']);
$transactions[$i]['cashier'] = [
"name" => $cashier['name'],
"username" => $cashier['username']
"name" => $cashier->getName(),
"username" => $cashier->getUsername()
];
}
}

@ -12,17 +12,15 @@
* user passwords.
*/
require __DIR__ . '/required.php';
require_once __DIR__ . '/lib/login.php';
require_once __DIR__ . '/lib/userinfo.php';
header("Content-Type: application/json");
$username = $VARS['username'];
$password = $VARS['password'];
if (user_exists($username) !== true || authenticate_user($username, $password, $errmsg) !== true) {
$user = User::byUsername($username);
if ($user->exists() !== true || Login::auth($username, $password) !== Login::LOGIN_OK) {
header("HTTP/1.1 403 Unauthorized");
die("\"403 Unauthorized\"");
}
$userinfo = getUserByUsername($username);
// query max results
$max = 20;

@ -28,10 +28,10 @@ header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
header("Link: <static/css/app.css>; rel=preload; as=style", false);
header("Link: <static/css/fa-svg-with-js.css>; rel=preload; as=style", false);
header("Link: <static/css/svg-with-js.min.css>; rel=preload; as=style", false);
header("Link: <static/js/fontawesome-all.min.js>; rel=preload; as=script", false);
header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
@ -47,7 +47,7 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
<link href="static/css/fa-svg-with-js.css" rel="stylesheet">
<link href="static/css/svg-with-js.min.css" rel="stylesheet">
<script nonce="<?php echo $SECURE_NONCE; ?>">
FontAwesomeConfig = {autoAddCss: false}
</script>
@ -69,9 +69,9 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (!isset($_GET['arg']) || is_empty($_GET['arg'])) {
$alertmsg = lang(MESSAGES[$_GET['msg']]['string'], false);
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
} else {
$alertmsg = lang2(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
@ -146,7 +146,7 @@ END;
if (isset($pg['icon'])) {
?><i class="<?php echo $pg['icon']; ?> fa-fw"></i> <?php
}
lang($pg['title']);
$Strings->get($pg['title']);
?>
</a>
</span>
@ -163,7 +163,7 @@ END;
</span>
<span class="nav-item mr-auto py-<?php echo $navbar_breakpoint; ?>-0">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="action.php?action=signout">
<i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php lang("sign out") ?></span>
<i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php $Strings->get("sign out") ?></span>
</a>
</span>
</div>
@ -182,7 +182,7 @@ END;
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/app.js"></script>
<?php
// custom page scripts

419
composer.lock generated

@ -1,419 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "0e5db12408080dd084cad072b8cfd599",
"content-hash": "348006dfc1d25121fcc3b4cb32bc3369",
"packages": [
{
"name": "catfan/medoo",
"version": "v1.5.3",
"source": {
"type": "git",
"url": "https://github.com/catfan/Medoo.git",
"reference": "1aa25a4001e0cfb739ba2996f00f4a3d2a7fdf07"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/catfan/Medoo/zipball/1aa25a4001e0cfb739ba2996f00f4a3d2a7fdf07",
"reference": "1aa25a4001e0cfb739ba2996f00f4a3d2a7fdf07",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=5.4"
},
"suggest": {
"ext-pdo_dblib": "For MSSQL or Sybase database on Linux/UNIX platform",
"ext-pdo_mysql": "For MySQL or MariaDB database",
"ext-pdo_oci": "For Oracle database",
"ext-pdo_oci8": "For Oracle version 8 database",
"ext-pdo_pqsql": "For PostgreSQL database",
"ext-pdo_sqlite": "For SQLite database",
"ext-pdo_sqlsrv": "For MSSQL database"
},
"type": "framework",
"autoload": {
"psr-4": {
"Medoo\\": "/src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Angel Lai",
"email": "angel@catfan.me"
}
],
"description": "The lightest PHP database framework to accelerate development",
"homepage": "https://medoo.in",
"keywords": [
"database",
"lightweight",
"mariadb",
"mssql",
"mysql",
"oracle",
"php framework",
"postgresql",
"sql",
"sqlite"
],
"time": "2017-12-25 17:02:41"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699",
"reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699",
"shasum": ""
},
"require": {
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.4",
"php": ">=5.5"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^4.0 || ^5.0",
"psr/log": "^1.0"
},
"suggest": {
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.2-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle is a PHP HTTP client library",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"rest",
"web service"
],
"time": "2017-06-22 18:50:49"
},
{
"name": "guzzlehttp/promises",
"version": "v1.3.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"shasum": ""
},
"require": {
"php": ">=5.5.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"time": "2016-12-20 10:07:11"
},
{
"name": "guzzlehttp/psr7",
"version": "1.4.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
"reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Schultze",
"homepage": "https://github.com/Tobion"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"request",
"response",
"stream",
"uri",
"url"
],
"time": "2017-03-20 17:10:46"
},
{
"name": "lapinator/ods-php-generator",
"version": "v0.0.3",
"source": {
"type": "git",
"url": "https://github.com/Lapinator/odsPhpGenerator.git",
"reference": "575314c003c2ec3032813bedcc1d27032b7b7ab2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Lapinator/odsPhpGenerator/zipball/575314c003c2ec3032813bedcc1d27032b7b7ab2",
"reference": "575314c003c2ec3032813bedcc1d27032b7b7ab2",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0"
],
"authors": [
{
"name": "Laurent VUIBERT",
"email": "lapinator@gmx.fr",
"homepage": "http://lapinator.net",
"role": "Developer"
}
],
"description": "Open Document Spreadsheet (.ods) generator ",
"homepage": "https://odsphpgenerator.lapinator.net/",
"keywords": [
"LibreOffice",
"ods"
],
"time": "2016-04-14 21:51:27"
},
{
"name": "league/csv",
"version": "9.1.4",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
"reference": "9c8ad06fb5d747c149875beb6133566c00eaa481"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/9c8ad06fb5d747c149875beb6133566c00eaa481",
"reference": "9c8ad06fb5d747c149875beb6133566c00eaa481",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=7.0.10"
},
"require-dev": {
"ext-curl": "*",
"friendsofphp/php-cs-fixer": "^2.0",
"phpstan/phpstan": "^0.9.2",
"phpstan/phpstan-phpunit": "^0.9.4",
"phpstan/phpstan-strict-rules": "^0.9.0",
"phpunit/phpunit": "^6.0"
},
"suggest": {
"ext-iconv": "Needed to ease transcoding CSV using iconv stream filters"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Csv\\": "src"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://github.com/nyamsprod/",
"role": "Developer"
}
],
"description": "Csv data manipulation made easy in PHP",
"homepage": "http://csv.thephpleague.com",
"keywords": [
"csv",
"export",
"filter",
"import",
"read",
"write"
],
"time": "2018-05-01 18:32:48"
},
{
"name": "psr/http-message",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"time": "2016-08-06 14:39:51"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

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

@ -1,144 +0,0 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
define("STRINGS", [
"sign in" => "Sign In",
"username" => "Username",
"password" => "Password",
"continue" => "Continue",
"authcode" => "Authentication code",
"2fa prompt" => "Enter the six-digit code from your mobile authenticator app.",
"2fa incorrect" => "Authentication code incorrect.",
"login incorrect" => "Login incorrect.",
"login server unavailable" => "Login server unavailable. Try again later or contact technical support.",
"account locked" => "This account has been disabled. Contact technical support.",
"password expired" => "You must change your password before continuing.",
"account terminated" => "Account terminated. Access denied.",
"account state error" => "Your account state is not stable. Log out, restart your browser, and try again.",
"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",
"point of sale" => "Point of Sale",
"barcode" => "Barcode",
"barcode or search" => "Barcode or Search",
"cash" => "Cash",
"check" => "Check",
"card" => "Card",
"crypto" => "Crypto",
"gift card" => "Gift Card",
"free" => "Free",
"paid" => "Paid",
"owed" => "Owed",
"change" => "Change",
"enter payment" => "Enter Payment",
"receipt" => "Receipt",
"close" => "Close",
"print" => "Print",
"customer" => "Customer",
"customer search" => "Search customers",
"new sale" => "New Sale",
"customers" => "Customers",
"actions" => "Actions",
"name" => "Name",
"phone" => "Phone",
"email" => "Email",
"address" => "Address",
"notes" => "Notes",
"edit" => "Edit",
"new customer" => "New Customer",
"adding customer" => "Adding Customer",
"editing customer" => "Editing {name}",
"save" => "Save",
"customer saved" => "Customer saved.",
"invalid customer id" => "Invalid customer ID",
"customer pricing" => "Customer Pricing",
"item" => "Item",
"cost" => "Cost",
"normal price" => "Normal Price",
"customer price" => "Customer Price",
"add price" => "Add Price",
"add customer price" => "Add Customer Price",
"delete" => "Delete",
"cancel" => "Cancel",
"price" => "Price",
"finish" => "Finish",
"registers" => "Registers",
"add register" => "Add Register",
"balance" => "Balance",
"opened" => "Opened",
"closed" => "Closed",
"never" => "Never",
"last opened" => "Last Opened",
"still open" => "Still Open",
"open" => "Open",
"no cash" => "No cash",
"choose register" => "Choose a cash register",
"cash not open" => "Cash not open. Go to Registers to open it.",
"cash opened" => "Cash opened.",
"cash closed" => "Cash closed.",
"register set" => "Register set.",
"change register" => "Change register",
"reports" => "Reports",
"report type" => "Report Type",
"format" => "Format",
"filter" => "Filter",
"generate report" => "Generate Report",
"cashflow" => "Cash Flow",
"z report" => "Z Report",
"csv file" => "CSV text file",
"ods file" => "ODS spreadsheet",
"html file" => "HTML web page",
"register" => "Register",
"all" => "All",
"date range" => "Date Range",
"start" => "Start",
"end" => "End",
"grid view" => "Grid view",
"edit register" => "Edit Register",
"editing register" => "Editing register {name}",
"adding register" => "Adding register",
"register saved" => "Register saved.",
"register name taken" => "Register name already taken. Use a different name.",
"no open registers" => "No open cash registers. Go to the Registers page to open one.",
"register management" => "Register Management",
"manage register" => "Manage register",
"manage" => "Manage",
"x report" => "X Report",
"z report" => "Z Report",
"pick cash" => "Choose",
"cash already closed" => "Cash already closed, cannot edit this transaction. Process a return instead.",
"update" => "Update",
"transaction search" => "Search transactions",
"return" => "Return",
"enter refund" => "Enter Refund",
"refund" => "Refund",
"cannot edit return transaction" => "Cannot edit a return transaction.",
"gift cards" => "Gift Cards",
"add card" => "Add Card",
"card number" => "Card Number",
"start balance" => "Starting Balance",
"issued" => "Issued",
"editing card x" => "Editing card {code}",
"adding card" => "Adding card",
"card added" => "Gift card added.",
"card saved" => "Gift card updated.",
"card x added" => "Gift card #{arg} added.",
"card x saved" => "Gift card #{arg} updated.",
"open drawer" => "Open Drawer",
"no items" => "No items in transaction.",
"delete transaction" => "Delete transaction",
"transaction discount" => "Transaction discount",
"Online Sales" => "Online Sales",
]);

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

@ -0,0 +1,112 @@
{
"point of sale": "Point of Sale",
"barcode": "Barcode",
"barcode or search": "Barcode or Search",
"cash": "Cash",
"check": "Check",
"card": "Card",
"crypto": "Crypto",
"gift card": "Gift Card",
"free": "Free",
"paid": "Paid",
"owed": "Owed",
"change": "Change",
"enter payment": "Enter Payment",
"receipt": "Receipt",
"close": "Close",
"print": "Print",
"customer": "Customer",
"customer search": "Search customers",
"new sale": "New Sale",
"customers": "Customers",
"actions": "Actions",
"name": "Name",
"phone": "Phone",
"email": "Email",
"address": "Address",
"notes": "Notes",
"edit": "Edit",
"new customer": "New Customer",
"adding customer": "Adding Customer",
"editing customer": "Editing {name}",
"save": "Save",
"customer saved": "Customer saved.",
"invalid customer id": "Invalid customer ID",
"customer pricing": "Customer Pricing",
"item": "Item",
"cost": "Cost",
"normal price": "Normal Price",
"customer price": "Customer Price",
"add price": "Add Price",
"add customer price": "Add Customer Price",
"delete": "Delete",
"cancel": "Cancel",
"price": "Price",
"finish": "Finish",
"registers": "Registers",
"add register": "Add Register",
"balance": "Balance",
"opened": "Opened",
"closed": "Closed",
"never": "Never",
"last opened": "Last Opened",
"still open": "Still Open",
"open": "Open",
"no cash": "No cash",
"choose register": "Choose a cash register",
"cash not open": "Cash not open. Go to Registers to open it.",
"cash opened": "Cash opened.",
"cash closed": "Cash closed.",
"register set": "Register set.",
"change register": "Change register",
"reports": "Reports",
"report type": "Report Type",
"format": "Format",
"filter": "Filter",
"generate report": "Generate Report",
"cashflow": "Cash Flow",
"z report": "Z Report",
"csv file": "CSV text file",
"ods file": "ODS spreadsheet",
"html file": "HTML web page",
"register": "Register",
"all": "All",
"date range": "Date Range",
"start": "Start",
"end": "End",
"grid view": "Grid view",
"edit register": "Edit Register",
"editing register": "Editing register {name}",
"adding register": "Adding register",
"register saved": "Register saved.",
"register name taken": "Register name already taken. Use a different name.",
"no open registers": "No open cash registers. Go to the Registers page to open one.",
"register management": "Register Management",
"manage register": "Manage register",
"manage": "Manage",
"x report": "X Report",
"pick cash": "Choose",
"cash already closed": "Cash already closed, cannot edit this transaction. Process a return instead.",
"update": "Update",
"transaction search": "Search transactions",
"return": "Return",
"enter refund": "Enter Refund",
"refund": "Refund",
"cannot edit return transaction": "Cannot edit a return transaction.",
"gift cards": "Gift Cards",
"add card": "Add Card",
"card number": "Card Number",
"start balance": "Starting Balance",
"issued": "Issued",
"editing card x": "Editing card {code}",
"adding card": "Adding card",
"card added": "Gift card added.",
"card saved": "Gift card updated.",
"card x added": "Gift card #{arg} added.",
"card x saved": "Gift card #{arg} updated.",
"open drawer": "Open Drawer",
"no items": "No items in transaction.",
"delete transaction": "Delete transaction",
"transaction discount": "Transaction discount",
"Online Sales": "Online Sales"
}

@ -0,0 +1,4 @@
{
"home": "Home",
"test": "Test"
}

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

@ -6,8 +6,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
require_once __DIR__ . "/receipts.php";
class GenerateReceipt {
const RECEIPT_TYPE_TRANSACTION = 1;
@ -51,7 +49,7 @@ class GenerateReceipt {
if ($p['amount'] < 0) {
continue;
}
$paymentlines[] = new ReceiptLine(lang($p['text'], false), "", '$' . number_format($p['amount'] * 1.0, 2));
$paymentlines[] = new ReceiptLine($Strings->get($p['text'], false), "", '$' . number_format($p['amount'] * 1.0, 2));
$paid += $p['amount'] * 1.0;
}
$change = $paid - $total;
@ -124,7 +122,7 @@ class GenerateReceipt {
'txid' => $txid
]);
foreach ($payments as $p) {
$paymentlines[] = new ReceiptLine(lang($p['text'], false), "", '$' . number_format($p['amount'] * -1.0, 2));
$paymentlines[] = new ReceiptLine($Strings->get($p['text'], false), "", '$' . number_format($p['amount'] * -1.0, 2));
$paid += $p['amount'] * 1.0;
}
@ -191,7 +189,7 @@ class GenerateReceipt {
$balance[$p['type']] += $p['amount'];
}
$receipt->appendHeader(new ReceiptLine(lang("x report", false), "", "", ReceiptLine::LINEFORMAT_BOLD | ReceiptLine::LINEFORMAT_CENTER));
$receipt->appendHeader(new ReceiptLine($Strings->get("x report", false), "", "", ReceiptLine::LINEFORMAT_BOLD | ReceiptLine::LINEFORMAT_CENTER));
$receipt->appendLine(new ReceiptLine("Printed:", "", date(DATETIME_FORMAT)));
$receipt->appendLine(new ReceiptLine("Register:", "", $registername));
@ -209,7 +207,7 @@ class GenerateReceipt {
$receipt->appendLine(new ReceiptLine("Sales", "", "", ReceiptLine::LINEFORMAT_CENTER));
$receipt->appendBreak();
foreach ($paymenttypes as $t) {
$receipt->appendLine(new ReceiptLine(lang($t['text'], false) . ":", "", '$' . number_format($balance[$t['type']], 2)));
$receipt->appendLine(new ReceiptLine($Strings->get($t['text'], false) . ":", "", '$' . number_format($balance[$t['type']], 2)));
}
$receipt->appendBlank();
@ -246,7 +244,7 @@ class GenerateReceipt {
$balance[$p['type']] += $p['amount'];
}
$receipt->appendHeader(new ReceiptLine(lang("z report", false), "", "", ReceiptLine::LINEFORMAT_BOLD | ReceiptLine::LINEFORMAT_CENTER));
$receipt->appendHeader(new ReceiptLine($Strings->get("z report", false), "", "", ReceiptLine::LINEFORMAT_BOLD | ReceiptLine::LINEFORMAT_CENTER));
$receipt->appendLine(new ReceiptLine("Printed:", "", date(DATETIME_FORMAT)));
$receipt->appendLine(new ReceiptLine("Register:", "", $registername));
@ -271,7 +269,7 @@ class GenerateReceipt {
$receipt->appendLine(new ReceiptLine("Sales", "", "", ReceiptLine::LINEFORMAT_CENTER));
$receipt->appendBreak();
foreach ($paymenttypes as $t) {
$receipt->appendLine(new ReceiptLine(lang($t['text'], false) . ":", "", '$' . number_format($balance[$t['type']], 2)));
$receipt->appendLine(new ReceiptLine($Strings->get($t['text'], false) . ":", "", '$' . number_format($balance[$t['type']], 2)));
}
return $receipt;

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

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

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

@ -0,0 +1,124 @@
<?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 Receipt {
private $lines = [];
private $header = [];
private $footer = [];
function __construct() {
}
function appendLine(ReceiptLine $line) {
$this->lines[] = $line;
}
function appendLines($lines) {
foreach ($lines as $l) {
$this->lines[] = $l;
}
}
function appendHeader(ReceiptLine $line) {
$this->header[] = $line;
}
function appendFooter(ReceiptLine $line) {
$this->footer[] = $line;
}
function appendBreak() {
$this->lines[] = new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR);
}
function appendBlank() {
$this->lines[] = new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_BLANK);
}
function getHtml($title = "") {
global $SECURE_NONCE;
$html = <<<END
<!DOCTYPE html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>$title</title>
<style nonce="$SECURE_NONCE">
.flex {
display: flex;
justify-content: space-between;
margin: 0;
}
.bold {
font-weight: bold;
}
.centered {
justify-content: center;
}
</style>
END;
if (count($this->header) > 0) {
foreach ($this->header as $line) {
$html .= $line->getHtml() . "\n";
}
$html .= (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getHtml();
}
foreach ($this->lines as $line) {
$html .= $line->getHtml() . "\n";
}
if (count($this->footer) > 0) {
$html .= (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getHtml();
foreach ($this->footer as $line) {
$html .= $line->getHtml() . "\n";
}
}
return $html;
}
function getPlainText($width) {
$lines = [];
if (count($this->header) > 0) {
foreach ($this->header as $line) {
$lines[] = $line->getPlainText($width);
}
$lines[] = (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getPlainText($width);
}
foreach ($this->lines as $line) {
$lines[] = $line->getPlainText($width);
}
if (count($this->footer) > 0) {
$lines[] = (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getPlainText($width);
foreach ($this->footer as $line) {
$lines[] = $line->getPlainText($width);
}
}
return implode("\n", $lines);
}
function getArray($width = 64) {
$header = [];
$lines = [];
$footer = [];
foreach ($this->header as $line) {
$header[] = $line->getArray($width);
}
foreach ($this->lines as $line) {
$lines[] = $line->getArray($width);
}
foreach ($this->footer as $line) {
$footer[] = $line->getArray($width);
}
return ["header" => $header, "lines" => $lines, "footer" => $footer];
}
function getJson($width = 64) {
return json_encode($this->getArray($width));
}
}

@ -135,120 +135,3 @@ class ReceiptLine {
}
}
class Receipt {
private $lines = [];
private $header = [];
private $footer = [];
function __construct() {
}
function appendLine(ReceiptLine $line) {
$this->lines[] = $line;
}
function appendLines($lines) {
foreach ($lines as $l) {
$this->lines[] = $l;
}
}
function appendHeader(ReceiptLine $line) {
$this->header[] = $line;
}
function appendFooter(ReceiptLine $line) {
$this->footer[] = $line;
}
function appendBreak() {
$this->lines[] = new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR);
}
function appendBlank() {
$this->lines[] = new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_BLANK);
}
function getHtml($title = "") {
global $SECURE_NONCE;
$html = <<<END
<!DOCTYPE html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>$title</title>
<style nonce="$SECURE_NONCE">
.flex {
display: flex;
justify-content: space-between;
margin: 0;
}
.bold {
font-weight: bold;
}
.centered {
justify-content: center;
}
</style>
END;
if (count($this->header) > 0) {
foreach ($this->header as $line) {
$html .= $line->getHtml() . "\n";
}
$html .= (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getHtml();
}
foreach ($this->lines as $line) {
$html .= $line->getHtml() . "\n";
}
if (count($this->footer) > 0) {
$html .= (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getHtml();
foreach ($this->footer as $line) {
$html .= $line->getHtml() . "\n";
}
}
return $html;
}
function getPlainText($width) {
$lines = [];
if (count($this->header) > 0) {
foreach ($this->header as $line) {
$lines[] = $line->getPlainText($width);
}
$lines[] = (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getPlainText($width);
}
foreach ($this->lines as $line) {
$lines[] = $line->getPlainText($width);
}
if (count($this->footer) > 0) {
$lines[] = (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getPlainText($width);
foreach ($this->footer as $line) {
$lines[] = $line->getPlainText($width);
}
}
return implode("\n", $lines);
}
function getArray($width = 64) {
$header = [];
$lines = [];
$footer = [];
foreach ($this->header as $line) {
$header[] = $line->getArray($width);
}
foreach ($this->lines as $line) {
$lines[] = $line->getArray($width);
}
foreach ($this->footer as $line) {
$footer[] = $line->getArray($width);
}
return ["header" => $header, "lines" => $lines, "footer" => $footer];
}
function getJson($width = 64) {
return json_encode($this->getArray($width));
}
}

@ -0,0 +1,137 @@
<?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/.
*/
use League\Csv\Writer;
use League\Csv\HTMLConverter;
use odsPhpGenerator\ods;
use odsPhpGenerator\odsTable;
use odsPhpGenerator\odsTableRow;
use odsPhpGenerator\odsTableColumn;
use odsPhpGenerator\odsTableCellString;
use odsPhpGenerator\odsStyleTableColumn;
use odsPhpGenerator\odsStyleTableCell;
class Report {
private $title = "";
private $header = [];
private $data = [];
public function __construct(string $title = "", array $header = [], array $data = []) {
$this->title = $title;
$this->header = $header;
$this->data = $data;
}
public function setHeader(array $header) {
$this->header = $header;
}
public function addDataRow(array $columns) {
$this->data[] = $columns;
}
public function getHeader(): array {
return $this->header;
}
public function getData(): array {
return $this->data;
}
public function output(string $format) {
switch ($format) {
case "ods":
$this->toODS();
break;
case "html":
$this->toHTML();
break;
case "csv":
default:
$this->toCSV();
break;
}
}
private function toODS() {
$ods = new ods();
$styleColumn = new odsStyleTableColumn();
$styleColumn->setUseOptimalColumnWidth(true);
$headerstyle = new odsStyleTableCell();
$headerstyle->setFontWeight("bold");
$table = new odsTable($this->title);
for ($i = 0; $i < count($this->header); $i++) {
$table->addTableColumn(new odsTableColumn($styleColumn));
}
$row = new odsTableRow();
foreach ($this->header as $cell) {
$row->addCell(new odsTableCellString($cell, $headerstyle));
}
$table->addRow($row);
foreach ($this->data as $cols) {
$row = new odsTableRow();
foreach ($cols as $cell) {
$row->addCell(new odsTableCellString($cell));
}
$table->addRow($row);
}
$ods->addTable($table);
// The @ is a workaround to silence the tempnam notice,
// which breaks the file. This is apparently the intended behavior:
// https://bugs.php.net/bug.php?id=69489
@$ods->downloadOdsFile($this->title . "_" . date("Y-m-d_Hi") . ".ods");
}
private function toHTML() {
global $SECURE_NONCE;
$data = array_merge([$this->header], $this->data);
// HTML exporter doesn't like null values
for ($i = 0; $i < count($data); $i++) {
for ($j = 0; $j < count($data[$i]); $j++) {
if (is_null($data[$i][$j])) {
$data[$i][$j] = '';
}
}
}
header('Content-type: text/html');
$converter = new HTMLConverter();
$out = "<!DOCTYPE html>\n"
. "<meta charset=\"utf-8\">\n"
. "<meta name=\"viewport\" content=\"width=device-width\">\n"
. "<title>" . $this->title . "_" . date("Y-m-d_Hi") . "</title>\n"
. <<<STYLE
<style nonce="$SECURE_NONCE">
.table-csv-data {
border-collapse: collapse;
}
.table-csv-data tr:first-child {
font-weight: bold;
}
.table-csv-data tr td {
border: 1px solid black;
}
</style>
STYLE
. $converter->convert($data);
echo $out;
}
private function toCSV() {
$csv = Writer::createFromString('');
$data = array_merge([$this->header], $this->data);
$csv->insertAll($data);
header('Content-type: text/csv');
header('Content-Disposition: attachment; filename="' . $this->title . "_" . date("Y-m-d_Hi") . ".csv" . '"');
echo $csv;
}
}

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

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

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

@ -12,10 +12,10 @@ $registers = $database->select("registers", ['[>]cash_drawer' => ['registerid' =
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
<form class="card border-green" action="action.php" method="POST">
<h3 class="card-header text-green">
<?php lang("point of sale"); ?>
<?php $Strings->get("point of sale"); ?>
</h3>
<div class="card-body">
<h5 class="card-title"><?php lang("choose register"); ?></h5>
<h5 class="card-title"><?php $Strings->get("choose register"); ?></h5>
<?php
if (count($registers) > 0) {
?>
@ -34,7 +34,7 @@ $registers = $database->select("registers", ['[>]cash_drawer' => ['registerid' =
} else {
?>
<div class="alert alert-info">
<?php lang("no open registers"); ?>
<?php $Strings->get("no open registers"); ?>
</div>
<?php
}

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

@ -14,63 +14,31 @@ if (count(get_included_files()) == 1) {
require_once __DIR__ . "/../required.php";
use League\Csv\Writer;
use League\Csv\HTMLConverter;
use odsPhpGenerator\ods;
use odsPhpGenerator\odsTable;
use odsPhpGenerator\odsTableRow;
use odsPhpGenerator\odsTableColumn;
use odsPhpGenerator\odsTableCellString;
use odsPhpGenerator\odsStyleTableColumn;
use odsPhpGenerator\odsStyleTableCell;
require_once __DIR__ . "/userinfo.php";
require_once __DIR__ . "/login.php";
// Allow access with a download code, for mobile app and stuff
$date = date("Y-m-d H:i:s");
$allowed_users = [];
$requester = -1;
if (isset($VARS['code']) && LOADED) {
if (!$database->has('report_access_codes', ["AND" => ['code' => $VARS['code'], 'expires[>]' => $date]])) {
dieifnotloggedin();
$requester = $_SESSION['uid'];
} else {
$requester = $database->get('report_access_codes', 'uid', ['code' => $VARS['code']]);
}
} else {
dieifnotloggedin();
$requester = $_SESSION['uid'];
}
if (account_has_permission($_SESSION['username'], "ADMIN")) {
$allowed_users = true;
} else {
if (account_has_permission($_SESSION['username'], "QWIKCLOCK_MANAGE")) {
$allowed_users = getManagedUIDs($requester);
}
if (account_has_permission($_SESSION['username'], "QWIKCLOCK_EDITSELF")) {
$allowed_users[] = $_SESSION['uid'];
}
}
// Delete old DB entries
$database->delete('report_access_codes', ['expires[<=]' => $date]);
if (LOADED) {
$user = null;
if (isset($VARS['type']) && isset($VARS['format'])) {
generateReport($VARS['type'], $VARS['format'], $VARS['register'], $VARS['startdate'], $VARS['enddate']);
die();
} else {
lang("invalid parameters");
$Strings->get("invalid parameters");
die();
}
}
function getCashFlowReport($register = null, $start = null, $end = null) {
global $database;
global $database, $Strings;
$where = [];
if (!is_null($register) && $database->has('registers', ['registerid' => $register])) {
@ -108,8 +76,9 @@ function getCashFlowReport($register = null, $start = null, $end = null) {
"payment_types.typename"
], $where
);
$header = [lang("register", false), lang("open", false), lang("close", false), lang("cash", false), lang("card", false), lang("check", false), lang("crypto", false), lang("gift card", false), lang("free", false)];
$out = [$header];
$report = new Report($Strings->get("cashflow", false));
$report->setHeader([$Strings->get("register", false), $Strings->get("open", false), $Strings->get("close", false), $Strings->get("cash", false), $Strings->get("card", false), $Strings->get("check", false), $Strings->get("crypto", false), $Strings->get("gift card", false), $Strings->get("free", false)]);
$registers = [];
@ -135,7 +104,7 @@ function getCashFlowReport($register = null, $start = null, $end = null) {
$r[$t] = 0.0;
}
}
$out[] = [
$report->addDataRow([
$r['name'],
$r['open'],
$r['close'],
@ -145,153 +114,22 @@ function getCashFlowReport($register = null, $start = null, $end = null) {
$r['crypto'] . "",
$r['giftcard'] . "",
$r['free'] . ""
];
]);
}
return $out;
return $report;
}
function getReportData($type, $register = null, $start = null, $end = null) {
function getReport(string $type, $register = null, $start = null, $end = null): Report {
switch ($type) {
case "cashflow":
return getCashFlowReport($register, $start, $end);
default:
return [["error"]];
}
}
function dataToCSV($data, $name = "report", $register = null, $start = null, $end = null) {
$csv = Writer::createFromString('');
$usernotice = "";
$usertitle = "";
$datetitle = "";
if ($start != null && (bool) strtotime($start)) {
$datenotice = lang2("report filtered to start date", ["date" => date(DATE_FORMAT, strtotime($start))], false);
$datetitle = "_" . date(DATE_FORMAT, strtotime($start));
$csv->insertOne([$datenotice]);
}
if ($end != null && (bool) strtotime($end)) {
$datenotice = lang2("report filtered to end date", ["date" => date(DATE_FORMAT, strtotime($end))], false);
$datetitle .= ($datetitle == "" ? "_" : "-") . date(DATE_FORMAT, strtotime($end));
$csv->insertOne([$datenotice]);
return new Report("error", ["ERROR"], ["Invalid report type."]);
}
$csv->insertAll($data);
header('Content-type: text/csv');
header('Content-Disposition: attachment; filename="' . $name . $usertitle . $datetitle . "_" . date("Y-m-d_Hi") . ".csv" . '"');
echo $csv;
die();
}
function dataToODS($data, $name = "report", $register = null, $start = null, $end = null) {
$ods = new ods();
$styleColumn = new odsStyleTableColumn();
$styleColumn->setUseOptimalColumnWidth(true);
$headerstyle = new odsStyleTableCell();
$headerstyle->setFontWeight("bold");
$table = new odsTable($name);
for ($i = 0; $i < count($data[0]); $i++) {
$table->addTableColumn(new odsTableColumn($styleColumn));
}
$usernotice = "";
$usertitle = "";
$datetitle = "";
if ($user != null && array_key_exists('username', $user) && array_key_exists('name', $user)) {
$usernotice = lang2("report filtered to user", ["name" => $user['name'], "username" => $user['username']], false);
$usertitle = "_" . $user['username'];
$row = new odsTableRow();
$row->addCell(new odsTableCellString($usernotice));
$table->addRow($row);
}
if ($start != null && (bool) strtotime($start)) {
$datenotice = lang2("report filtered to start date", ["date" => date(DATE_FORMAT, strtotime($start))], false);
$datetitle = "_" . date(DATE_FORMAT, strtotime($start));
$row = new odsTableRow();
$row->addCell(new odsTableCellString($datenotice));
$table->addRow($row);
}
if ($end != null && (bool) strtotime($end)) {
$datenotice = lang2("report filtered to end date", ["date" => date(DATE_FORMAT, strtotime($end))], false);
$datetitle .= ($datetitle == "" ? "_" : "-") . date(DATE_FORMAT, strtotime($end));
$row = new odsTableRow();
$row->addCell(new odsTableCellString($datenotice));
$table->addRow($row);
}
$rowid = 0;
foreach ($data as $datarow) {
$row = new odsTableRow();
foreach ($datarow as $cell) {
if ($rowid == 0) {
$row->addCell(new odsTableCellString($cell, $headerstyle));
} else {
$row->addCell(new odsTableCellString($cell));
}
}
$table->addRow($row);
$rowid++;
}
$ods->addTable($table);
$ods->downloadOdsFile($name . $usertitle . $datetitle . "_" . date("Y-m-d_Hi") . ".ods");
}
function dataToHTML($data, $name = "report", $register = null, $start = null, $end = null) {
global $SECURE_NONCE;
// HTML exporter doesn't like null values
for ($i = 0; $i < count($data); $i++) {
for ($j = 0; $j < count($data[$i]); $j++) {
if (is_null($data[$i][$j])) {
$data[$i][$j] = '';
}
}
}
$datenotice = "";
$datetitle = "";
if ($start != null && (bool) strtotime($start)) {
$datenotice = "<span>" . lang2("report filtered to start date", ["date" => date(DATE_FORMAT, strtotime($start))], false) . "</span><br />";
$datetitle = "_" . date(DATE_FORMAT, strtotime($start));
}
if ($end != null && (bool) strtotime($end)) {
$datenotice .= "<span>" . lang2("report filtered to end date", ["date" => date(DATE_FORMAT, strtotime($end))], false) . "</span><br />";
$datetitle .= ($datetitle == "" ? "_" : "-") . date(DATE_FORMAT, strtotime($end));
}
header('Content-type: text/html');
$converter = new HTMLConverter();
$out = "<!DOCTYPE html>\n"
. "<meta charset=\"utf-8\">\n"
. "<meta name=\"viewport\" content=\"width=device-width\">\n"
. "<title>" . $name . $datetitle . "_" . date("Y-m-d_Hi") . "</title>\n"
. <<<STYLE
<style nonce="$SECURE_NONCE">
.table-csv-data {
border-collapse: collapse;
}
.table-csv-data tr:first-child {
font-weight: bold;
}
.table-csv-data tr td {
border: 1px solid black;
}
</style>
STYLE
. $datenotice
. $converter->convert($data);
echo $out;
}
function generateReport($type, $format, $register = null, $start = null, $end = null, $deleted = true) {
$data = getReportData($type, $register, $start, $end, $deleted);
switch ($format) {
case "ods":
dataToODS($data, $type, $register, $start, $end);
break;
case "html":
dataToHTML($data, $type, $register, $start, $end);
break;
case "csv":
default:
echo dataToCSV($data, $type, $register, $start, $end);
break;
}
function generateReport($type, $format, $register = null, $start = null, $end = null) {
$report = getReport($type, $register, $start, $end);
$report->output($format);
}

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

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

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

@ -7,26 +7,24 @@
require_once __DIR__ . "/../required.php";
use Medoo\Medoo;
redirectIfNotLoggedIn();
$cards = $database->select('certificates', ['certid (id)', 'certcode (code)', 'amount', 'start_amount (start)', 'issued'], ['deleted[!]' => 1]);
?>
<div class="btn-toolbar">
<a href="app.php?page=editcertificate" class="btn btn-success"><i class="fas fa-plus"></i> <?php lang("add card"); ?></a>
<a href="app.php?page=editcertificate" class="btn btn-success"><i class="fas fa-plus"></i> <?php $Strings->get("add card"); ?></a>
</div>
<table id="certificatetable" class="table table-bordered table-hover table-sm">
<thead>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php lang('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-hashtag d-none d-md-inline"></i> <?php lang('card number'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-balance-scale d-none d-md-inline"></i> <?php lang('balance'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-play d-none d-md-inline"></i> <?php lang('start balance'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-calendar d-none d-md-inline"></i> <?php lang('issued'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-hashtag d-none d-md-inline"></i> <?php $Strings->get('card number'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-balance-scale d-none d-md-inline"></i> <?php $Strings->get('balance'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-play d-none d-md-inline"></i> <?php $Strings->get('start balance'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-calendar d-none d-md-inline"></i> <?php $Strings->get('issued'); ?></th>
</tr>
</thead>
<tbody>
@ -36,7 +34,7 @@ $cards = $database->select('certificates', ['certid (id)', 'certcode (code)', 'a
<tr>
<td></td>
<td>
<a class="btn btn-primary btn-sm" href="app.php?page=editcertificate&id=<?php echo $c['id']; ?>"><i class="fas fa-edit"></i> <?php lang("edit"); ?></a>
<a class="btn btn-primary btn-sm" href="app.php?page=editcertificate&id=<?php echo $c['id']; ?>"><i class="fas fa-edit"></i> <?php $Strings->get("edit"); ?></a>
</td>
<td><?php echo $c['code']; ?></td>
<td>$<?php echo number_format($c['amount'], 2); ?></td>
@ -50,11 +48,11 @@ $cards = $database->select('certificates', ['certid (id)', 'certcode (code)', 'a
<tfoot>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php lang('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-hashtag d-none d-md-inline"></i> <?php lang('card number'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-balance-scale d-none d-md-inline"></i> <?php lang('balance'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-play d-none d-md-inline"></i> <?php lang('start balance'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-calendar d-none d-md-inline"></i> <?php lang('issued'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-hashtag d-none d-md-inline"></i> <?php $Strings->get('card number'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-balance-scale d-none d-md-inline"></i> <?php $Strings->get('balance'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-play d-none d-md-inline"></i> <?php $Strings->get('start balance'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-calendar d-none d-md-inline"></i> <?php $Strings->get('issued'); ?></th>
</tr>
</tfoot>
</table>

@ -13,19 +13,19 @@ $customers = $database->select('customers', ['customerid (id)', 'name', 'email',
?>
<div class="btn-toolbar">
<a href="app.php?page=editcustomer" class="btn btn-success"><i class="fas fa-user-plus"></i> <?php lang("new customer"); ?></a>
<a href="app.php?page=editcustomer" class="btn btn-success"><i class="fas fa-user-plus"></i> <?php $Strings->get("new customer"); ?></a>
</div>
<table id="customertable" class="table table-bordered table-hover table-sm">
<thead>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php lang('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php lang('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-phone d-none d-md-inline"></i> <?php lang('phone'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-at d-none d-md-inline"></i> <?php lang('email'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-map-marker d-none d-md-inline"></i> <?php lang('address'); ?></th>
<th data-priority="5"><i class="fas fa-fw fa-sticky-note d-none d-md-inline"></i> <?php lang('notes'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php $Strings->get('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-phone d-none d-md-inline"></i> <?php $Strings->get('phone'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-at d-none d-md-inline"></i> <?php $Strings->get('email'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-map-marker d-none d-md-inline"></i> <?php $Strings->get('address'); ?></th>
<th data-priority="5"><i class="fas fa-fw fa-sticky-note d-none d-md-inline"></i> <?php $Strings->get('notes'); ?></th>
</tr>
</thead>
<tbody>
@ -35,7 +35,7 @@ $customers = $database->select('customers', ['customerid (id)', 'name', 'email',
<tr>
<td></td>
<td>
<a class="btn btn-primary btn-sm" href="app.php?page=editcustomer&id=<?php echo $c['id']; ?>"><i class="fas fa-edit"></i> <?php lang("edit"); ?></a>
<a class="btn btn-primary btn-sm" href="app.php?page=editcustomer&id=<?php echo $c['id']; ?>"><i class="fas fa-edit"></i> <?php $Strings->get("edit"); ?></a>
</td>
<td><?php echo $c['name']; ?></td>
<td><?php echo $c['phone']; ?></td>
@ -50,12 +50,12 @@ $customers = $database->select('customers', ['customerid (id)', 'name', 'email',
<tfoot>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php lang('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php lang('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-phone d-none d-md-inline"></i> <?php lang('phone'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-at d-none d-md-inline"></i> <?php lang('email'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-map-marker d-none d-md-inline"></i> <?php lang('address'); ?></th>
<th data-priority="5"><i class="fas fa-fw fa-sticky-note d-none d-md-inline"></i> <?php lang('notes'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php $Strings->get('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-phone d-none d-md-inline"></i> <?php $Strings->get('phone'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-at d-none d-md-inline"></i> <?php $Strings->get('email'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-map-marker d-none d-md-inline"></i> <?php $Strings->get('address'); ?></th>
<th data-priority="5"><i class="fas fa-fw fa-sticky-note d-none d-md-inline"></i> <?php $Strings->get('notes'); ?></th>
</tr>
</tfoot>
</table>

@ -50,22 +50,22 @@ if (!$editing) {
<?php
if ($editing) {
?>
<i class="fas fa-edit"></i> <?php lang2("editing card x", ['code' => htmlspecialchars($carddata['code'])]); ?>
<i class="fas fa-edit"></i> <?php $Strings->build("editing card x", ['code' => htmlspecialchars($carddata['code'])]); ?>
<?php
} else {
?>
<i class="fas fa-edit"></i> <?php lang("adding card"); ?>
<i class="fas fa-edit"></i> <?php $Strings->get("adding card"); ?>
<?php
}
?>
</h3>
<div class="card-body row">
<div class="form-group col-sm-6">
<label for="code"><i class="fas fa-hashtag"></i> <?php lang("card number"); ?></label>
<label for="code"><i class="fas fa-hashtag"></i> <?php $Strings->get("card number"); ?></label>
<input type="text" class="form-control" id="code" name="code" value="<?php echo htmlspecialchars($carddata['code']); ?>" />
</div>
<div class="form-group col-sm-6">
<label for="balance"><i class="fas fa-balance-scale"></i> <?php lang("balance"); ?></label>
<label for="balance"><i class="fas fa-balance-scale"></i> <?php $Strings->get("balance"); ?></label>
<input type="money" class="form-control" id="balance" name="balance" value="<?php echo number_format($carddata['amount'], 2); ?>" />
</div>
</div>
@ -75,7 +75,7 @@ if (!$editing) {
<input type="hidden" name="source" value="certificates" />
<div class="card-footer d-flex">
<button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php lang("save"); ?></button>
<button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button>
</div>
</div>
</form>

@ -54,7 +54,7 @@ if ($editing) {
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-user-tag"></i> <?php lang("add customer price"); ?></h5>
<h5 class="modal-title"><i class="fas fa-user-tag"></i> <?php $Strings->get("add customer price"); ?></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@ -64,7 +64,7 @@ if ($editing) {
<div class="input-group-prepend">
<span class="input-group-text px-2"><i class="fas fa-barcode"></i></span>
</div>
<input type="text" class="form-control" id="pricemodalitem" placeholder="<?php lang("barcode or search"); ?>" />
<input type="text" class="form-control" id="pricemodalitem" placeholder="<?php $Strings->get("barcode or search"); ?>" />
<div class="input-group-append">
<button class="btn btn-link" type="button" id="pricemodalsearch"><i class="fas fa-search"></i></button>
</div>
@ -72,15 +72,15 @@ if ($editing) {
<div class="card mt-2">
<div class="card-body">
<h5 class="card-title"><i class="fas fa-box"></i> <?php lang("item"); ?></h5>
<h5 class="card-title"><i class="fas fa-box"></i> <?php $Strings->get("item"); ?></h5>
<p>
<?php lang("name"); ?>: <span id="pricemodalitemname">---</span><br />
<?php lang("cost"); ?>: <span id="pricemodalitemcost">---</span><br />
<?php lang("price"); ?>: <span id="pricemodalitemprice">---</span><br />
<?php $Strings->get("name"); ?>: <span id="pricemodalitemname">---</span><br />
<?php $Strings->get("cost"); ?>: <span id="pricemodalitemcost">---</span><br />
<?php $Strings->get("price"); ?>: <span id="pricemodalitemprice">---</span><br />
</p>
</div>
<div class="card-body">
<h5 class="card-title"><i class="fas fa-user-tag"></i> <?php lang("customer price"); ?></h5>
<h5 class="card-title"><i class="fas fa-user-tag"></i> <?php $Strings->get("customer price"); ?></h5>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">$</span>
@ -92,8 +92,8 @@ if ($editing) {
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php lang("cancel"); ?></button>
<button type="button" class="btn btn-primary" id="pricemodalsave"><?php lang("save"); ?></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php $Strings->get("cancel"); ?></button>
<button type="button" class="btn btn-primary" id="pricemodalsave"><?php $Strings->get("save"); ?></button>
</div>
</div>
</div>
@ -105,34 +105,34 @@ if ($editing) {
<?php
if ($editing) {
?>
<i class="fas fa-edit"></i> <?php lang2("editing customer", ['name' => "<span id=\"name_title\">" . htmlspecialchars($custdata['name']) . "</span>"]); ?>
<i class="fas fa-edit"></i> <?php $Strings->build("editing customer", ['name' => "<span id=\"name_title\">" . htmlspecialchars($custdata['name']) . "</span>"]); ?>
<?php
} else {
?>
<i class="fas fa-edit"></i> <?php lang("adding customer"); ?>
<i class="fas fa-edit"></i> <?php $Strings->get("adding customer"); ?>
<?php
}
?>
</h3>
<div class="card-body row">
<div class="form-group col-12">
<label for="name"><i class="fas fa-user"></i> <?php lang("name"); ?></label>
<label for="name"><i class="fas fa-user"></i> <?php $Strings->get("name"); ?></label>
<input type="text" class="form-control" id="name" name="name" placeholder="Foo Bar" required="required" value="<?php echo htmlspecialchars($custdata['name']); ?>" />
</div>
<div class="form-group col-sm-6">
<label for="email"><i class="fas fa-at"></i> <?php lang("email"); ?></label>
<label for="email"><i class="fas fa-at"></i> <?php $Strings->get("email"); ?></label>
<input type="email" class="form-control" id="email" name="email" placeholder="user@example.com" value="<?php echo htmlspecialchars($custdata['email']); ?>" />
</div>
<div class="form-group col-sm-6">
<label for="phone"><i class="fas fa-phone"></i> <?php lang("phone"); ?></label>
<label for="phone"><i class="fas fa-phone"></i> <?php $Strings->get("phone"); ?></label>
<input type="phone" class="form-control" id="phone" name="phone" placeholder="" value="<?php echo htmlspecialchars($custdata['phone']); ?>" />
</div>
<div class="form-group col-sm-6">
<label for="address"><i class="fas fa-map-marker"></i> <?php lang("address"); ?></label>
<label for="address"><i class="fas fa-map-marker"></i> <?php $Strings->get("address"); ?></label>
<textarea rows="3" class="form-control" id="address" name="address" placeholder=""><?php echo htmlspecialchars($custdata['address']); ?></textarea>
</div>
<div class="form-group col-sm-6">
<label for="notes"><i class="fas fa-sticky-note"></i> <?php lang("notes"); ?></label>
<label for="notes"><i class="fas fa-sticky-note"></i> <?php $Strings->get("notes"); ?></label>
<textarea rows="3" class="form-control" id="notes" name="notes" placeholder=""><?php echo htmlspecialchars($custdata['notes']); ?></textarea>
</div>
</div>
@ -140,21 +140,21 @@ if ($editing) {
<hr />
<div class="card-body">
<h5 class="card-title"><?php lang("customer pricing"); ?></h5>
<h5 class="card-title"><?php $Strings->get("customer pricing"); ?></h5>
<div class="btn-toolbar">
<div class="btn btn-success" id="addcustomerpricebtn">
<i class="fas fa-plus"></i> <?php lang("add price"); ?>
<i class="fas fa-plus"></i> <?php $Strings->get("add price"); ?>
</div>
</div>
<table class="table table-hover table-sm" id="pricingtable">
<thead>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php lang('actions'); ?></th>
<th data-priority="1"><?php lang("item"); ?></th>
<th data-priority="4"><?php lang("cost"); ?></th>
<th data-priority="3"><?php lang("normal price"); ?></th>
<th data-priority="2"><?php lang("customer price"); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><?php $Strings->get("item"); ?></th>
<th data-priority="4"><?php $Strings->get("cost"); ?></th>
<th data-priority="3"><?php $Strings->get("normal price"); ?></th>
<th data-priority="2"><?php $Strings->get("customer price"); ?></th>
</tr>
</thead>
<tbody>
@ -165,7 +165,7 @@ if ($editing) {
<tr data-itemid="<?php echo $p['itemid']; ?>">
<td></td>
<td>
<div class="btn btn-sm btn-danger deletepricebtn" data-itemid="<?php echo $p['itemid']; ?>"><i class="fas fa-trash"></i> <?php lang("delete"); ?></div>
<div class="btn btn-sm btn-danger deletepricebtn" data-itemid="<?php echo $p['itemid']; ?>"><i class="fas fa-trash"></i> <?php $Strings->get("delete"); ?></div>
</td>
<td>
<input type="hidden" name="pricing[<?php echo $i; ?>][item]" value="<?php echo $p['itemid']; ?>" />
@ -191,7 +191,7 @@ if ($editing) {
<input type="hidden" name="source" value="customers" />
<div class="card-footer d-flex">
<button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php lang("save"); ?></button>
<button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button>
</div>
</div>
</form>

@ -44,28 +44,28 @@ if ($editing) {
<?php
if ($editing) {
?>
<i class="fas fa-edit"></i> <?php lang2("editing register", ['name' => "<span id=\"name_title\">" . htmlspecialchars($regdata['name']) . "</span>"]); ?>
<i class="fas fa-edit"></i> <?php $Strings->build("editing register", ['name' => "<span id=\"name_title\">" . htmlspecialchars($regdata['name']) . "</span>"]); ?>
<?php
} else {
?>
<i class="fas fa-edit"></i> <?php lang("adding register"); ?>
<i class="fas fa-edit"></i> <?php $Strings->get("adding register"); ?>
<?php
}
?>
</h3>
<div class="card-body row">
<div class="form-group col-12">
<label for="name"><i class="fas fa-font"></i> <?php lang("name"); ?></label>
<label for="name"><i class="fas fa-font"></i> <?php $Strings->get("name"); ?></label>
<input type="text" class="form-control" id="name" name="name" placeholder="Register #1" required="required" value="<?php echo htmlspecialchars($regdata['name']); ?>" />
</div>
</div>
<div class="card-body row">
<div class="form-group col-12 col-md-6">
<label for="zreport"><i class="fas fa-receipt"></i> <?php lang("z report"); ?></label>
<label for="zreport"><i class="fas fa-receipt"></i> <?php $Strings->get("z report"); ?></label>
<div class="input-group">
<select id="zreport" class="form-control">
<option value=""><?php lang("pick cash") ?></option>
<option value=""><?php $Strings->get("pick cash") ?></option>
<?php
for ($i = count($cash) - 1; $i >= 0; $i--) {
$c = $cash[$i];
@ -78,7 +78,7 @@ if ($editing) {
?>
</select>
<div class="input-group-append">
<button type="button" id="printzreportbtn" class="btn btn-primary"><i class="fas fa-print"></i> <?php lang("print"); ?></button>
<button type="button" id="printzreportbtn" class="btn btn-primary"><i class="fas fa-print"></i> <?php $Strings->get("print"); ?></button>
</div>
</div>
<iframe id="zframe" class="w-100 shadow-lg"></iframe>
@ -89,7 +89,7 @@ if ($editing) {
<input type="hidden" name="source" value="registers" />
<div class="card-footer d-flex">
<button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php lang("save"); ?></button>
<button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button>
</div>
</div>
</form>

@ -16,12 +16,12 @@ $unshipped = $database->select('transactions', ['[>]customers' => 'customerid'],
<thead>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php lang('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php lang('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-phone d-none d-md-inline"></i> <?php lang('phone'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-at d-none d-md-inline"></i> <?php lang('email'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-map-marker d-none d-md-inline"></i> <?php lang('address'); ?></th>
<th data-priority="5"><i class="fas fa-fw fa-sticky-note d-none d-md-inline"></i> <?php lang('notes'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php $Strings->get('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-phone d-none d-md-inline"></i> <?php $Strings->get('phone'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-at d-none d-md-inline"></i> <?php $Strings->get('email'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-map-marker d-none d-md-inline"></i> <?php $Strings->get('address'); ?></th>
<th data-priority="5"><i class="fas fa-fw fa-sticky-note d-none d-md-inline"></i> <?php $Strings->get('notes'); ?></th>
</tr>
</thead>
<tbody>
@ -31,7 +31,7 @@ $unshipped = $database->select('transactions', ['[>]customers' => 'customerid'],
<tr>
<td></td>
<td>
<a class="btn btn-primary btn-sm" href="app.php?page=editcustomer&id=<?php echo $c['id']; ?>"><i class="fas fa-edit"></i> <?php lang("edit"); ?></a>
<a class="btn btn-primary btn-sm" href="app.php?page=editcustomer&id=<?php echo $c['id']; ?>"><i class="fas fa-edit"></i> <?php $Strings->get("edit"); ?></a>
</td>
<td><?php echo $o['name']; ?></td>
<td><?php echo $o['phone']; ?></td>
@ -46,12 +46,12 @@ $unshipped = $database->select('transactions', ['[>]customers' => 'customerid'],
<tfoot>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php lang('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php lang('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-phone d-none d-md-inline"></i> <?php lang('phone'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-at d-none d-md-inline"></i> <?php lang('email'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-map-marker d-none d-md-inline"></i> <?php lang('address'); ?></th>
<th data-priority="5"><i class="fas fa-fw fa-sticky-note d-none d-md-inline"></i> <?php lang('notes'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-user d-none d-md-inline"></i> <?php $Strings->get('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-phone d-none d-md-inline"></i> <?php $Strings->get('phone'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-at d-none d-md-inline"></i> <?php $Strings->get('email'); ?></th>
<th data-priority="4"><i class="fas fa-fw fa-map-marker d-none d-md-inline"></i> <?php $Strings->get('address'); ?></th>
<th data-priority="5"><i class="fas fa-fw fa-sticky-note d-none d-md-inline"></i> <?php $Strings->get('notes'); ?></th>
</tr>
</tfoot>
</table>

@ -5,7 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$register = [
"name" => lang("no cash", false),
"name" => $Strings->get("no cash", false),
"id" => ""
];
@ -45,18 +45,18 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-receipt"></i> <?php lang("receipt"); ?></h5>
<h5 class="modal-title"><i class="fas fa-receipt"></i> <?php $Strings->get("receipt"); ?></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="display-4 text-center" id="receiptchangediv"><?php lang("change"); ?>: $<span id="receiptchange">0.00</span></div>
<div class="display-4 text-center" id="receiptchangediv"><?php $Strings->get("change"); ?>: $<span id="receiptchange">0.00</span></div>
<iframe class="w-100 shadow-lg" id="receiptframe"></iframe>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php lang("close"); ?></button>
<button type="button" class="btn btn-primary" id="receiptprintbtn"><i class="fas fa-print"></i> <?php lang("print"); ?></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php $Strings->get("close"); ?></button>
<button type="button" class="btn btn-primary" id="receiptprintbtn"><i class="fas fa-print"></i> <?php $Strings->get("print"); ?></button>
</div>
</div>
</div>
@ -66,14 +66,14 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-user"></i> <?php lang("customer"); ?></h5>
<h5 class="modal-title"><i class="fas fa-user"></i> <?php $Strings->get("customer"); ?></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="input-group">
<input type="text" class="form-control" id="customersearch" placeholder="<?php lang("customer search"); ?>" />
<input type="text" class="form-control" id="customersearch" placeholder="<?php $Strings->get("customer search"); ?>" />
<div class="input-group-append">
<button class="btn btn-link" type="button" id="customersearchbtn"><i class="fas fa-search"></i></button>
</div>
@ -82,7 +82,7 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php lang("close"); ?></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php $Strings->get("close"); ?></button>
</div>
</div>
</div>
@ -92,7 +92,7 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-cog"></i> <?php lang("register management"); ?></h5>
<h5 class="modal-title"><i class="fas fa-cog"></i> <?php $Strings->get("register management"); ?></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
@ -101,7 +101,7 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
<div class="row">
<div class="col-12 col-md-6">
<div class="input-group">
<input type="text" class="form-control" id="transactionsearch" placeholder="<?php lang("transaction search"); ?>" />
<input type="text" class="form-control" id="transactionsearch" placeholder="<?php $Strings->get("transaction search"); ?>" />
<div class="input-group-append">
<span class="btn btn-link open-number-pad-btn">
<i class="fas fa-keyboard"></i>
@ -135,15 +135,15 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
</div>
<div class="col-12 col-md-6">
<div class="d-flex justify-content-between flex-wrap">
<button type="button" class="btn btn-default" id="opendrawerbtn"><i class="fas fa-lock-open"></i> <?php lang("open drawer"); ?></button>
<button type="button" class="btn btn-primary ml-auto" id="xprintbtn"><i class="fas fa-print"></i> <?php lang("print"); ?></button>
<button type="button" class="btn btn-default" id="opendrawerbtn"><i class="fas fa-lock-open"></i> <?php $Strings->get("open drawer"); ?></button>
<button type="button" class="btn btn-primary ml-auto" id="xprintbtn"><i class="fas fa-print"></i> <?php $Strings->get("print"); ?></button>
</div>
<iframe class="w-100 shadow-lg" id="xframe" src="action.php?action=xreport&format=html&register=<?php echo $register['id']; ?>"></iframe>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php lang("close"); ?></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php $Strings->get("close"); ?></button>
</div>
</div>
</div>
@ -155,7 +155,7 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
<div class="card-header p-1">
<div class="input-group">
<div class="input-group-prepend">
<span class="btn btn-default" id="gridviewbtn" title="<?php lang("grid view"); ?>"><i class="fas fa-th fa-fw"></i></span>
<span class="btn btn-default" id="gridviewbtn" title="<?php $Strings->get("grid view"); ?>"><i class="fas fa-th fa-fw"></i></span>
</div>
<?php
if (isset($_SESSION['mobile'])) {
@ -166,7 +166,7 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
</span>
</div>
<?php } ?>
<input type="text" class="form-control" id="barcode" placeholder="<?php lang("barcode or search"); ?>" />
<input type="text" class="form-control" id="barcode" placeholder="<?php $Strings->get("barcode or search"); ?>" />
<div class="input-group-append">
<button class="btn btn-link" type="button" id="barcodebtn"><i class="fas fa-search"></i></button>
</div>
@ -235,8 +235,8 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
<div class="col-12 col-md-6 order-0 order-md-1">
<div class="card mb-3 mb-md-0">
<div class="w-100 position-absolute d-flex align-items-start px-3 pt-2">
<a href="#" class="mr-auto text-body" id="openmanagement" data-toggle="tooltip" title="<?php lang("manage register") ?>"><i class="fas fa-cog"></i> <?php lang("manage"); ?></a>
<a href="app.php?page=pos&switch" class="ml-auto text-body" id="register" data-id="<?php echo $register['id']; ?>" data-toggle="tooltip" title="<?php lang("change register") ?>"><i class="fas fa-exchange-alt"></i> <?php echo $register['name']; ?></a>
<a href="#" class="mr-auto text-body" id="openmanagement" data-toggle="tooltip" title="<?php $Strings->get("manage register") ?>"><i class="fas fa-cog"></i> <?php $Strings->get("manage"); ?></a>
<a href="app.php?page=pos&switch" class="ml-auto text-body" id="register" data-id="<?php echo $register['id']; ?>" data-toggle="tooltip" title="<?php $Strings->get("change register") ?>"><i class="fas fa-exchange-alt"></i> <?php echo $register['name']; ?></a>
</div>
<div class="display-4 p-1 p-md-3 text-center">$<span id="grand-total">0.00</span></div>
<div class="card-body d-flex justify-content-center flex-wrap py-0 my-0">
@ -247,7 +247,7 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
echo $tx['customername'];
}
?></span>
<span class="sr-only"><?php lang("customer"); ?></span>
<span class="sr-only"><?php $Strings->get("customer"); ?></span>
</div>
<?php
if (!$returning) {
@ -265,23 +265,23 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
}
?></span>
<i class="fas fa-percent"></i>
<span class="sr-only"><?php lang("transaction discount"); ?></span>
<span class="sr-only"><?php $Strings->get("transaction discount"); ?></span>
</div>
<?php
}
?>
<div class="btn m-1" id="deletetxbtn">
<i class="fas fa-trash"></i>
<span class="sr-only"><?php lang("delete transaction"); ?></span>
<span class="sr-only"><?php $Strings->get("delete transaction"); ?></span>
</div>
</div>
<div class="card-body d-md-none">
<span class="btn btn-green btn-lg btn-block" id="paymentbtn"><i class="fas fa-money-bill-wave"></i> <?php
if ($returning) {
lang("enter refund");
$Strings->get("enter refund");
} else {
lang("enter payment");
$Strings->get("enter payment");
}
?></span>
</div>
@ -297,9 +297,9 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
continue;
}
?>
<div class="card p-2 text-center m-1 payment-method-button" data-payment-method="<?php echo $data['name']; ?>" data-icon="<?php echo $data['icon']; ?>" data-text="<?php lang($data['text']); ?>">
<div class="card p-2 text-center m-1 payment-method-button" data-payment-method="<?php echo $data['name']; ?>" data-icon="<?php echo $data['icon']; ?>" data-text="<?php $Strings->get($data['text']); ?>">
<i class="<?php echo $data['icon']; ?> fa-3x fa-fw"></i>
<?php lang($data['text']); ?>
<?php $Strings->get($data['text']); ?>
</div>
<?php
}
@ -312,25 +312,25 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
if ($returning) {
?>
<div class="col-12 col-sm-4">
<?php lang("refund"); ?> $<span id="paid-amount">0.00</span>
<?php $Strings->get("refund"); ?> $<span id="paid-amount">0.00</span>
</div>
<div class="col-12 col-sm-4">
<?php lang("owed"); ?> $<span id="owed-amount">0.00</span>
<?php $Strings->get("owed"); ?> $<span id="owed-amount">0.00</span>
</div>
<div class="col-12 col-sm-4">
<?php lang("change"); ?> $<span id="change-amount">0.00</span>
<?php $Strings->get("change"); ?> $<span id="change-amount">0.00</span>
</div>
<?php
} else {
?>
<div class="col-12 col-sm-4">
<?php lang("paid"); ?> $<span id="paid-amount">0.00</span>
<?php $Strings->get("paid"); ?> $<span id="paid-amount">0.00</span>
</div>
<div class="col-12 col-sm-4">
<?php lang("owed"); ?> $<span id="owed-amount">0.00</span>
<?php $Strings->get("owed"); ?> $<span id="owed-amount">0.00</span>
</div>
<div class="col-12 col-sm-4">
<?php lang("change"); ?> $<span id="change-amount">0.00</span>
<?php $Strings->get("change"); ?> $<span id="change-amount">0.00</span>
</div>
<?php
}
@ -349,7 +349,7 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
<div class="input-group-prepend">
<span class="input-group-text">
<i class="<?php echo $p['icon']; ?> fa-fw mr-1"></i>
<?php lang($p['text']); ?>
<?php $Strings->get($p['text']); ?>
</span>
</div>
<div class="input-group-prepend">
@ -397,11 +397,11 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
<div class="card-body">
<span class="btn btn-green btn-lg btn-block" id="finishbtn"><i class="fas fa-receipt"></i> <?php
if ($editing) {
lang("update");
$Strings->get("update");
} else if ($returning) {
lang("return");
$Strings->get("return");
} else {
lang("finish");
$Strings->get("finish");
}
?></span>
</div>

@ -15,18 +15,18 @@ $registers = $database->select('registers', ['registerid (id)', 'registername (n
?>
<div class="btn-toolbar">
<a href="app.php?page=editregister" class="btn btn-success"><i class="fas fa-plus"></i> <?php lang("add register"); ?></a>
<a href="app.php?page=editregister" class="btn btn-success"><i class="fas fa-plus"></i> <?php $Strings->get("add register"); ?></a>
</div>
<table id="registertable" class="table table-bordered table-hover table-sm">
<thead>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php lang('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-font d-none d-md-inline"></i> <?php lang('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-balance-scale d-none d-md-inline"></i> <?php lang('balance'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-play d-none d-md-inline"></i> <?php lang('last opened'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-stop d-none d-md-inline"></i> <?php lang('closed'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-font d-none d-md-inline"></i> <?php $Strings->get('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-balance-scale d-none d-md-inline"></i> <?php $Strings->get('balance'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-play d-none d-md-inline"></i> <?php $Strings->get('last opened'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-stop d-none d-md-inline"></i> <?php $Strings->get('closed'); ?></th>
</tr>
</thead>
<tbody>
@ -46,8 +46,8 @@ $registers = $database->select('registers', ['registerid (id)', 'registername (n
$open = "";
$close = "";
if ($cash === false) {
$open = lang("never", false);
$close = lang("never", false);
$open = $Strings->get("never", false);
$close = $Strings->get("never", false);
} else {
if (!is_null($cash['end_amount']) && !is_null($cash['close'])) {
$balance = (float) $cash['end_amount'];
@ -66,13 +66,13 @@ $registers = $database->select('registers', ['registerid (id)', 'registername (n
}
}
$open = date(DATETIME_FORMAT, strtotime($cash['open']));
$close = is_null($cash['close']) ? lang("still open", false) : date(DATETIME_FORMAT, strtotime($cash['close']));
$close = is_null($cash['close']) ? $Strings->get("still open", false) : date(DATETIME_FORMAT, strtotime($cash['close']));
}
?>
<tr>
<td></td>
<td>
<a class="btn btn-primary btn-sm" href="app.php?page=editregister&id=<?php echo $r['id']; ?>"><i class="fas fa-edit"></i> <?php lang("edit"); ?></a>
<a class="btn btn-primary btn-sm" href="app.php?page=editregister&id=<?php echo $r['id']; ?>"><i class="fas fa-edit"></i> <?php $Strings->get("edit"); ?></a>
<?php
if (is_null($cash['close']) && !is_null($cash['open'])) {
?>
@ -80,12 +80,12 @@ $registers = $database->select('registers', ['registerid (id)', 'registername (n
<input type="hidden" name="action" value="closecash" />
<input type="hidden" name="source" value="registers" />
<input type="hidden" name="register" value="<?php echo $r['id']; ?>" />
<button class="btn btn-danger btn-sm" type="submit"><i class="fas fa-stop"></i> <?php lang("close"); ?></button>
<button class="btn btn-danger btn-sm" type="submit"><i class="fas fa-stop"></i> <?php $Strings->get("close"); ?></button>
</form>
<?php
} else {
?>
<button class="btn btn-success btn-sm btn-opencash" data-register="<?php echo $r['id']; ?>"><i class="fas fa-play"></i> <?php lang("open"); ?></button>
<button class="btn btn-success btn-sm btn-opencash" data-register="<?php echo $r['id']; ?>"><i class="fas fa-play"></i> <?php $Strings->get("open"); ?></button>
<?php
}
?>
@ -102,11 +102,11 @@ $registers = $database->select('registers', ['registerid (id)', 'registername (n
<tfoot>
<tr>
<th data-priority="0"></th>
<th data-priority="1"><?php lang('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-font d-none d-md-inline"></i> <?php lang('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-balance-scale d-none d-md-inline"></i> <?php lang('balance'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-play d-none d-md-inline"></i> <?php lang('last opened'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-stop d-none d-md-inline"></i> <?php lang('closed'); ?></th>
<th data-priority="1"><?php $Strings->get('actions'); ?></th>
<th data-priority="1"><i class="fas fa-fw fa-font d-none d-md-inline"></i> <?php $Strings->get('name'); ?></th>
<th data-priority="2"><i class="fas fa-fw fa-balance-scale d-none d-md-inline"></i> <?php $Strings->get('balance'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-play d-none d-md-inline"></i> <?php $Strings->get('last opened'); ?></th>
<th data-priority="3"><i class="fas fa-fw fa-stop d-none d-md-inline"></i> <?php $Strings->get('closed'); ?></th>
</tr>
</tfoot>
</table>

@ -9,7 +9,7 @@ redirectifnotloggedin();
if (false) {
?>
<div class="alert alert-danger"><?php lang("missing permission") ?></div>
<div class="alert alert-danger"><?php $Strings->get("missing permission") ?></div>
<?php
} else {
?>
@ -20,9 +20,9 @@ if (false) {
<div class="col-12 col-sm-6 col-md-12 col-lg-6">
<div class="card mb-4">
<div class="card-body">
<h4 class="card-title"><label for="type"><i class="fas fa-list"></i> <?php lang("report type"); ?></label></h4>
<h4 class="card-title"><label for="type"><i class="fas fa-list"></i> <?php $Strings->get("report type"); ?></label></h4>
<select name="type" id="type" class="form-control" required>
<option value="cashflow"><?php lang("cashflow") ?></option>
<option value="cashflow"><?php $Strings->get("cashflow") ?></option>
</select>
</div>
</div>
@ -30,17 +30,17 @@ if (false) {
<div class="col-12 col-sm-6 col-md-12 col-lg-6">
<div class="card mb-4">
<div class="card-body">
<h4 class="card-title"><label for="format"><i class="fas fa-file"></i> <?php lang("format"); ?></label></h4>
<h4 class="card-title"><label for="format"><i class="fas fa-file"></i> <?php $Strings->get("format"); ?></label></h4>
<select name="format" class="form-control" required>
<option value="csv"><?php lang("csv file") ?></option>
<option value="ods"><?php lang("ods file") ?></option>
<option value="html"><?php lang("html file") ?></option>
<option value="csv"><?php $Strings->get("csv file") ?></option>
<option value="ods"><?php $Strings->get("ods file") ?></option>
<option value="html"><?php $Strings->get("html file") ?></option>
</select>
</div>
</div>
</div>
<div class="col-12">
<button type="submit" class="btn btn-success btn-block d-none d-lg-block genrptbtn"><i class="fas fa-download"></i> <?php lang("generate report"); ?></button>
<button type="submit" class="btn btn-success btn-block d-none d-lg-block genrptbtn"><i class="fas fa-download"></i> <?php $Strings->get("generate report"); ?></button>
</div>
</div>
</div>
@ -49,9 +49,9 @@ if (false) {
<div class="col-12">
<div class="card">
<div class="card-body">
<h4 class="card-title"><label><i class="fas fa-filter"></i> <?php lang("filter"); ?></label></h4>
<h4 class="card-title"><label><i class="fas fa-filter"></i> <?php $Strings->get("filter"); ?></label></h4>
<div id="date-filter">
<label><i class="fas fa-calendar"></i> <?php lang("date range") ?></label><br />
<label><i class="fas fa-calendar"></i> <?php $Strings->get("date range") ?></label><br />
<div class="input-group">
<input type="text" id="startdate" name="startdate" data-toggle="datetimepicker" data-target="#startdate" class="form-control" />
<span class="input-group-text"><i class="fas fa-chevron-right"></i></span>
@ -59,9 +59,9 @@ if (false) {
</div>
</div>
<div id="register-filter" class="mt-2">
<label for="register"><i class="fas fa-store-alt"></i> <?php lang("register"); ?></label>
<label for="register"><i class="fas fa-store-alt"></i> <?php $Strings->get("register"); ?></label>
<select name="register" class="form-control">
<option value=""><?php lang("all"); ?></option>
<option value=""><?php $Strings->get("all"); ?></option>
<?php
$registers = $database->select('registers', ['registerid (id)', 'registername (name)']);
foreach ($registers as $r) {
@ -80,7 +80,7 @@ if (false) {
?>
<input type="hidden" name="code" value="<?php echo $code; ?>" />
<button type="submit" class="btn btn-success btn-block d-lg-none genrptbtn"><i class="fas fa-download"></i> <?php lang("generate report"); ?></button>
<button type="submit" class="btn btn-success btn-block d-lg-none genrptbtn"><i class="fas fa-download"></i> <?php $Strings->get("generate report"); ?></button>
</div>
</div>
</div>

@ -62,9 +62,14 @@ if ($_SESSION['mobile'] === TRUE) {
require __DIR__ . '/vendor/autoload.php';
// List of alert messages
require __DIR__ . '/lang/messages.php';
// text strings (i18n)
require __DIR__ . '/lang/' . LANGUAGE . ".php";
require __DIR__ . '/langs/messages.php';
$libs = glob(__DIR__ . "/lib/*.lib.php");
foreach ($libs as $lib) {
require_once $lib;
}
$Strings = new Strings(LANGUAGE);
/**
* Kill off the running process and spit out an error message
@ -145,52 +150,6 @@ function is_empty($str) {
return (is_null($str) || !isset($str) || $str == '');
}
/**
* I18N string getter. If the key doesn't exist, outputs the key itself.
* @param string $key I18N string key
* @param boolean $echo whether to echo the result or return it (default echo)
*/
function lang($key, $echo = true) {
if (array_key_exists($key, STRINGS)) {
$str = STRINGS[$key];
} else {
trigger_error("Language key \"$key\" does not exist in " . LANGUAGE, E_USER_WARNING);
$str = $key;
}
if ($echo) {
echo $str;
} else {
return $str;
}
}
/**
* I18N string getter (with builder). If the key doesn't exist, outputs the key itself.
* @param string $key I18N string key
* @param array $replace key-value array of replacements.
* If the string value is "hello {abc}" and you give ["abc" => "123"], the
* result will be "hello 123".
* @param boolean $echo whether to echo the result or return it (default echo)
*/
function lang2($key, $replace, $echo = true) {
if (array_key_exists($key, STRINGS)) {
$str = STRINGS[$key];
} else {
trigger_error("Language key \"$key\" does not exist in " . LANGUAGE, E_USER_WARNING);
$str = $key;
}
foreach ($replace as $find => $repl) {
$str = str_replace("{" . $find . "}", $repl, $str);
}
if ($echo) {
echo $str;
} else {
return $str;
}
}
function dieifnotloggedin() {
if ($_SESSION['loggedin'] != true) {

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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