Browse Source

Merge BusinessAppTemplate

master
Skylar Ittner 1 year ago
parent
commit
d8613f0baa

+ 1
- 1
README.md View File

@@ -2,4 +2,4 @@ NickelBox
=========

NickelBox is a point of sale app. It integrates with BinStack for inventory
management.
management.

+ 20
- 21
action.php View File

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

+ 2
- 4
api.php View File

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

+ 8
- 8
app.php View File

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

+ 0
- 419
composer.lock View File

@@ -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": []
}

+ 58
- 47
index.php View File

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

+ 0
- 144
lang/en_us.php View File

@@ -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",
]);

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

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

+ 112
- 0
langs/en/strings.json View File

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

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

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

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


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

@@ -0,0 +1,13 @@
<?php

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

class IncorrectPasswordException extends Exception {
public function __construct(string $message = "Incorrect password.", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}

lib/generatereceipt.php → lib/GenerateReceipt.lib.php View File

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

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

@@ -0,0 +1,135 @@
<?php

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

class IPUtils {

/**
* Check if a given ipv4 address is in a given cidr
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
* @author Thorsten Ott <https://gist.github.com/tott/7684443>
*/
public static function ip4_in_cidr($ip, $cidr) {
if (strpos($cidr, '/') == false) {
$cidr .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $cidr, $netmask ) = explode('/', $cidr, 2);
$range_decimal = ip2long($cidr);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, ( 32 - $netmask)) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}

/**
* Check if a given ipv6 address is in a given cidr
* @param string $ip IP to check in IPV6 format
* @param string $cidr CIDR netmask
* @return boolean true if the IP is in this range, false otherwise.
* @author MW. <https://stackoverflow.com/a/7952169>
*/
public static function ip6_in_cidr($ip, $cidr) {
$address = inet_pton($ip);
$subnetAddress = inet_pton(explode("/", $cidr)[0]);
$subnetMask = explode("/", $cidr)[1];

$addr = str_repeat("f", $subnetMask / 4);
switch ($subnetMask % 4) {
case 0:
break;
case 1:
$addr .= "8";
break;
case 2:
$addr .= "c";
break;
case 3:
$addr .= "e";
break;
}
$addr = str_pad($addr, 32, '0');
$addr = pack("H*", $addr);

$binMask = $addr;
return ($address & $binMask) == $subnetAddress;
}

/**
* Check if the REMOTE_ADDR is on Cloudflare's network.
* @return boolean true if it is, otherwise false
*/
public static function validateCloudflare() {
if (filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Using IPv6
$cloudflare_ips_v6 = [
"2400:cb00::/32",
"2405:8100::/32",
"2405:b500::/32",
"2606:4700::/32",
"2803:f800::/32",
"2c0f:f248::/32",
"2a06:98c0::/29"
];
$valid = false;
foreach ($cloudflare_ips_v6 as $cidr) {
if (ip6_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
} else {
// Using IPv4
$cloudflare_ips_v4 = [
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"104.16.0.0/12",
"108.162.192.0/18",
"131.0.72.0/22",
"141.101.64.0/18",
"162.158.0.0/15",
"172.64.0.0/13",
"173.245.48.0/20",
"188.114.96.0/20",
"190.93.240.0/20",
"197.234.240.0/22",
"198.41.128.0/17"
];
$valid = false;
foreach ($cloudflare_ips_v4 as $cidr) {
if (ip4_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
}
return $valid;
}

/**
* Makes a good guess at the client's real IP address.
*
* @return string Client IP or `0.0.0.0` if we can't find anything
*/
public static function getClientIP() {
// If CloudFlare is in the mix, we should use it.
// Check if the request is actually from CloudFlare before trusting it.
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
if (validateCloudflare()) {
return $_SERVER["HTTP_CF_CONNECTING_IP"];
}
}

if (isset($_SERVER["REMOTE_ADDR"])) {
return $_SERVER["REMOTE_ADDR"];
}

return "0.0.0.0"; // This will not happen unless we aren't a web server
}

}

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

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

}

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

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

}

+ 124
- 0
lib/Receipt.lib.php View File

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

}

lib/receipts.php → lib/ReceiptLine.lib.php View File

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

}

+ 137
- 0
lib/Report.lib.php View File

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

}

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

@@ -0,0 +1,19 @@
<?php

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

class Session {

public static function start(User $user) {
$_SESSION['username'] = $user->getUsername();
$_SESSION['uid'] = $user->getUID();
$_SESSION['email'] = $user->getEmail();
$_SESSION['realname'] = $user->getName();
$_SESSION['loggedin'] = true;
}

}

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

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