Browse Source

Merge BusinessAppTemplate

master
Skylar Ittner 6 months ago
parent
commit
a5e20f87fd

+ 3
- 15
LICENSE.md View File

@@ -1,19 +1,7 @@
Copyright (c) 2018 Netsyms Technologies.
Copyright (c) 2017-2019 Netsyms Technologies. Some rights reserved.

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

The branding assets include:
* the application icon
* the Netsyms N punchcard logo
* the Netsyms for Business graph logo

If you are unsure if your usage is allowed, please contact us:
https://netsyms.com/contact
legal@netsyms.com

All other portions of this application,
unless otherwise noted (in comments, headers, etc), are licensed as follows:
Licensed under the Mozilla Public License Version 2.0. Files without MPL header
comments, including third party code, may be under a different license.

Mozilla Public License Version 2.0
==================================

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

+ 13
- 13
action.php View File

@@ -21,11 +21,11 @@ if ($VARS['action'] !== "signout") {
*/
function returnToSender($msg, $arg = "") {
global $VARS;
if ($arg == "") {
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=" . $msg);
} else {
header("Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg&arg=$arg");
$header = "Location: app.php?page=" . urlencode($VARS['source']) . "&msg=$msg";
if ($arg != "") {
$header .= "&arg=$arg";
}
header($header);
die();
}

@@ -35,7 +35,7 @@ switch ($VARS['action']) {
$error = null;
$oktx = null;
$database->action(function ($database) {
global $VARS, $binstack, $error, $oktx;
global $VARS, $binstack, $Strings, $error, $oktx;

if (empty($VARS['items'])) {
$error = $Strings->get("no items", false);
@@ -215,7 +215,7 @@ switch ($VARS['action']) {
$error = null;
$oktx = null;
$database->action(function ($database) {
global $VARS, $binstack, $error, $oktx;
global $VARS, $binstack, $Strings, $error, $oktx;

$items = $VARS['items'];
$payments = $VARS['payments'];
@@ -438,7 +438,7 @@ switch ($VARS['action']) {
exit(json_encode(["status" => "OK", "transactions" => $transactions]));
case "itemsearch":
header("Content-Type: application/json");
if (!is_empty($VARS['q'])) {
if (!empty($VARS['q'])) {
$where["AND"]["OR"] = [
"name[~]" => $VARS['q'],
"code1[~]" => $VARS['q'],
@@ -499,7 +499,7 @@ switch ($VARS['action']) {
exit(json_encode(["status" => "OK", "items" => $items]));
case "customersearch":
header("Content-Type: application/json");
if (!is_empty($VARS['q'])) {
if (!empty($VARS['q'])) {
$where["AND"]["OR"] = [
"customerid" => $VARS['q'],
"name[~]" => $VARS['q'],
@@ -533,7 +533,7 @@ switch ($VARS['action']) {
break;
case "editcustomer":
$insert = true;
if (is_empty($VARS['id'])) {
if (empty($VARS['id'])) {
$insert = true;
} else {
if ($database->has('customers', ['customerid' => $VARS['id']])) {
@@ -542,7 +542,7 @@ switch ($VARS['action']) {
returnToSender("invalid_customerid");
}
}
if (is_empty($VARS['name'])) {
if (empty($VARS['name'])) {
returnToSender('invalid_parameters');
}

@@ -673,7 +673,7 @@ switch ($VARS['action']) {
returnToSender("invalid_parameters");
}
}
if (is_empty($VARS['name'])) {
if (empty($VARS['name'])) {
returnToSender('invalid_parameters');
}

@@ -742,7 +742,7 @@ switch ($VARS['action']) {
}
}

if ($insert && (is_empty($code) || $database->has('certificates', ['certcode' => $code]))) {
if ($insert && (empty($code) || $database->has('certificates', ['certcode' => $code]))) {
do {
$code = random_int(100000000000, 999999999999);
} while ($database->has('certificates', ['certcode' => $code]));
@@ -775,6 +775,6 @@ switch ($VARS['action']) {
exit(json_encode(["status" => "OK"]));
case "signout":
session_destroy();
header('Location: index.php');
header('Location: index.php?logout=1');
die("Logged out.");
}

+ 2
- 31
api.php View File

@@ -4,35 +4,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

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

$username = $VARS['username'];
$password = $VARS['password'];
$user = User::byUsername($username);
if ($user->exists() !== true || Login::auth($username, $password) !== Login::LOGIN_OK) {
header("HTTP/1.1 403 Unauthorized");
die("\"403 Unauthorized\"");
}

// query max results
$max = 20;
if (preg_match("/^[0-9]+$/", $VARS['max']) === 1 && $VARS['max'] <= 1000) {
$max = (int) $VARS['max'];
}

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

+ 5
- 0
api/.htaccess View File

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

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

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

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

sendJsonResp();

+ 15
- 0
api/apisettings.php View File

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

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

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

+ 149
- 0
api/functions.php View File

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

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

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

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

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

/**
* Check if the request is allowed
* @global array $VARS
* @return bool true if the request should continue, false if the request is bad
*/
function authenticate(): bool {
global $VARS, $SETTINGS;
// HTTP basic auth
if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
} else if (!empty($VARS['username']) && !empty($VARS['password'])) {
$username = $VARS['username'];
$password = $VARS['password'];
} else {
return false;
}
$user = User::byUsername($username);
if (!$user->exists()) {
return false;
}
if ($user->checkPassword($password, true)) {
// Check that the user has permission to access the app
$perms = is_array($SETTINGS['api_permissions']) ? $SETTINGS['api_permissions'] : $SETTINGS['permissions'];
foreach ($perms as $perm) {
if (!$user->hasPermission($perm)) {
return false;
}
}
return true;
}
return false;
}

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

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

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

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

+ 81
- 0
api/index.php View File

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

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

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

header("Access-Control-Allow-Origin: *");

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

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

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

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

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

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

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

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

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

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

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

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

+ 34
- 28
app.php View File

@@ -1,5 +1,4 @@
<?php

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -14,7 +13,7 @@ if ($_SESSION['loggedin'] != true) {
require_once __DIR__ . "/pages.php";

$pageid = "home";
if (isset($_GET['page']) && !is_empty($_GET['page'])) {
if (!empty($_GET['page'])) {
$pg = strtolower($_GET['page']);
$pg = preg_replace('/[^0-9a-z_]/', "", $pg);
if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) {
@@ -40,7 +39,7 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

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

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

@@ -66,28 +65,35 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals

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

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

+ 111
- 155
index.php View File

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

require_once __DIR__ . "/required.php";

// if we're logged in, we don't need to be here.
if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && !isset($_GET['permissionerror'])) {
header('Location: app.php');
die();
}

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

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

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

<link href="static/css/bootstrap.min.css" rel="stylesheet">
<style nonce="<?php echo $SECURE_NONCE; ?>">
.display-5 {
font-size: 2.5rem;
font-weight: 300;
line-height: 1.2;
}

.banner-image {
max-height: 100px;
margin: 2em auto;
border: 1px solid grey;
border-radius: 15%;
}
</style>

<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-12 text-center">
<img class="banner-image" src="./static/img/logo.svg" />
</div>

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

<div class="col-12 col-sm-8 col-lg-6">
<div class="card mt-4">
<div class="card-body">
<a href="<?php echo $url; ?>" class="btn btn-primary btn-block"><?php $Strings->get($button); ?></a>
</div>
</div>
</div>
</div>
</div>
<?php
}

/* Authenticate user */
$userpass_ok = false;
$multiauth = false;
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 = $Strings->get("account locked", false);
break;
case "TERMINATED":
$alert = $Strings->get("account terminated", false);
break;
case "CHANGE_PASSWORD":
$alert = $Strings->get("password expired", false);
break;
case "NORMAL":
$username_ok = true;
break;
case "ALERT_ON_ACCESS":
$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 ($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 {
$alert = $Strings->get("login incorrect", false);
}
if (!empty($_GET['logout'])) {
showHTML("You have been logged out.", "Log in again", "./index.php");
die();
}
if (empty($_SESSION["login_code"])) {
$redirecttologin = true;
} else {
try {
$uidinfo = AccountHubApi::get("checkloginkey", ["code" => $_SESSION["login_code"]]);
if ($uidinfo["status"] == "ERROR") {
throw new Exception();
}
if (is_numeric($uidinfo['uid'])) {
$user = new User($uidinfo['uid'] * 1);
foreach ($SETTINGS['permissions'] as $perm) {
if (!$user->hasPermission($perm)) {
showHTML("no access permission", "sign out", "./action.php?action=signout");
die();
}
} else { // User does not exist anywhere
$alert = $Strings->get("login incorrect", false);
}
} else {
$alert = $Strings->get("captcha error", false);
}
} 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 ($user->check2fa($VARS['authcode'])) {
Session::start($user);
$_SESSION["login_code"] = null;
header('Location: app.php');
die("Logged in, go to app.php");
showHTML("Logged in", "Continue", "./app.php");
die();
} else {
$alert = $Strings->get("2fa incorrect", false);
throw new Exception();
}
} catch (Exception $ex) {
$redirecttologin = true;
}
} else {
$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.bundle.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

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

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

<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/index.css" rel="stylesheet">
<?php if (CAPTCHA_ENABLED) { ?>
<script src="<?php echo CAPTCHA_SERVER ?>/captcheck.dist.js"></script>
<?php } ?>
</head>
<body>
<div class="row justify-content-center">
<div class="col-auto">
<img class="banner-image" src="static/img/logo.svg" />
</div>
</div>
<div class="row justify-content-center">
<div class="card col-11 col-xs-11 col-sm-8 col-md-6 col-lg-4">
<div class="card-body">
<h5 class="card-title"><?php $Strings->get("sign in"); ?></h5>
<form action="" method="POST">
<?php
if (!empty($alert)) {
?>
<div class="alert alert-danger">
<i class="fa fa-fw fa-exclamation-triangle"></i> <?php echo $alert; ?>
</div>
<?php
}

if ($multiauth != true) {
?>
<input type="text" class="form-control" name="username" placeholder="<?php $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 />
<?php } ?>
<input type="hidden" name="progress" value="1" />
<?php
} else if ($multiauth) {
?>
<div class="alert alert-info">
<?php $Strings->get("2fa prompt"); ?>
</div>
<input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="hidden" name="progress" value="2" />
<input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
<?php
}
?>
<button type="submit" class="btn btn-primary">
<?php $Strings->get("continue"); ?>
</button>
</form>
</div>
</div>
</div>
<div class="footer">
<?php echo FOOTER_TEXT; ?><br />
Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
</body>
</html>

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

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

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

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

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

+ 1
- 20
langs/en/core.json View File

@@ -1,26 +1,7 @@
{
"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."
"login server error": "The login server returned an error: {arg}"
}

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

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

+ 2
- 2
langs/en/titles.json View File

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

+ 4
- 0
langs/messages.php View File

@@ -41,6 +41,10 @@ define("MESSAGES", [
"string" => "cash opened",
"type" => "success"
],
"cash_closed" => [
"string" => "cash closed",
"type" => "success"
],
"register_saved" => [
"string" => "register saved",
"type" => "success"

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

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

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

class AccountHubApi {

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

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

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

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

}

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

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

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

class FormBuilder {

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

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

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

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

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

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

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

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

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

/**
* Add a select dropdown.
*
* @param string $name Element name
* @param string $value Element value
* @param bool $required If the element is required for form submission.
* @param string $id Element ID
* @param array $options Array of [value => text] pairs for a select element
* @param string $label Text label to display near the input
* @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
* @param int $width Bootstrap column width for the input, out of 12.
*/
public function addSelect(string $name, string $value = "", bool $required = true, string $id = null, array $options = null, string $label = "", string $icon = "", int $width = 4) {
$this->addInput($name, $value, "select", $required, $id, $options, $label, $icon, $width);
}

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

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

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

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

foreach ($this->items as $item) {
$required = $item["required"] ? "required" : "";
$id = empty($item["id"]) ? "" : "id=\"$item[id]\"";
$pattern = empty($item["pattern"]) ? "" : "pattern=\"$item[pattern]\"";
if (empty($item['type'])) {
$item['type'] = "text";
}
$itemhtml = "";
$itemlabel = "";

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

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

$html .= <<<HTMLBOTTOM

</div>
</div>
HTMLBOTTOM;

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

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

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

}

+ 13
- 13
lib/GenerateReceipt.lib.php View File

@@ -13,14 +13,14 @@ class GenerateReceipt {
const RECEIPT_TYPE_Z = 3;

private static function saleReceipt($transaction) {
global $database;
global $database, $Strings, $SETTINGS;
$receipt = new Receipt();
$tx = $database->get('transactions', ['txid', 'txdate', 'customerid', 'type', 'cashier', 'discountpercent'], ['txid' => $transaction]);
// Info
$txid = $tx['txid'];
$datetime = date(DATETIME_FORMAT, strtotime($tx['txdate']));
$datetime = date($SETTINGS['datetime_format'], strtotime($tx['txdate']));
$type = $tx['type'];
$cashier = getUserByID($tx['cashier'])['name'];
$cashier = (new User($tx['cashier']))->getName();
$customerid = $tx['customerid'];

// Items
@@ -89,14 +89,14 @@ class GenerateReceipt {
}

private static function returnReceipt($transaction) {
global $database;
global $database, $Strings, $SETTINGS;
$receipt = new Receipt();
$tx = $database->get('transactions', ['txid', 'txdate', 'customerid', 'type', 'cashier', 'discountpercent'], ['txid' => $transaction]);
// Info
$txid = $tx['txid'];
$datetime = date(DATETIME_FORMAT, strtotime($tx['txdate']));
$datetime = date($SETTINGS['datetime_format'], strtotime($tx['txdate']));
$type = $tx['type'];
$cashier = getUserByID($tx['cashier'])['name'];
$cashier = (new User($tx['cashier']))->getName();
$customerid = $tx['customerid'];

// Items
@@ -165,7 +165,7 @@ class GenerateReceipt {
}

static function xReceipt($registerid) {
global $database;
global $database, $Strings, $SETTINGS;
$receipt = new Receipt();

$registername = $database->get('registers', 'registername', ['registerid' => $registerid]);
@@ -191,7 +191,7 @@ class GenerateReceipt {

$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("Printed:", "", date($SETTINGS['datetime_format'])));
$receipt->appendLine(new ReceiptLine("Register:", "", $registername));
$receipt->appendLine(new ReceiptLine("Transactions:", "", $transactioncount));

@@ -199,7 +199,7 @@ class GenerateReceipt {
$receipt->appendBreak();
$receipt->appendLine(new ReceiptLine("Opening", "", "", ReceiptLine::LINEFORMAT_CENTER));
$receipt->appendBreak();
$receipt->appendLine(new ReceiptLine("Date:", "", date(DATETIME_FORMAT, strtotime($cash['open']))));
$receipt->appendLine(new ReceiptLine("Date:", "", date($SETTINGS['datetime_format'], strtotime($cash['open']))));
$receipt->appendLine(new ReceiptLine("Cash:", "", '$' . number_format($cash['start_amount'], 2)));

$receipt->appendBlank();
@@ -220,7 +220,7 @@ class GenerateReceipt {
}

static function zReceipt($cashid) {
global $database;
global $database, $Strings, $SETTINGS;
$receipt = new Receipt();

$cash = $database->get('cash_drawer', ['open', 'close', 'start_amount', 'end_amount', 'cashid', 'registerid'], ['cashid' => $cashid]);
@@ -246,7 +246,7 @@ class GenerateReceipt {

$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("Printed:", "", date($SETTINGS['datetime_format'])));
$receipt->appendLine(new ReceiptLine("Register:", "", $registername));
$receipt->appendLine(new ReceiptLine("Transactions:", "", $transactioncount));

@@ -254,14 +254,14 @@ class GenerateReceipt {
$receipt->appendBreak();
$receipt->appendLine(new ReceiptLine("Opening", "", "", ReceiptLine::LINEFORMAT_CENTER));
$receipt->appendBreak();
$receipt->appendLine(new ReceiptLine("Date:", "", date(DATETIME_FORMAT, strtotime($cash['open']))));
$receipt->appendLine(new ReceiptLine("Date:", "", date($SETTINGS['datetime_format'], strtotime($cash['open']))));
$receipt->appendLine(new ReceiptLine("Cash:", "", '$' . number_format($cash['start_amount'], 2)));

$receipt->appendBlank();
$receipt->appendBreak();
$receipt->appendLine(new ReceiptLine("Closing", "", "", ReceiptLine::LINEFORMAT_CENTER));
$receipt->appendBreak();
$receipt->appendLine(new ReceiptLine("Date:", "", date(DATETIME_FORMAT, strtotime($cash['close']))));
$receipt->appendLine(new ReceiptLine("Date:", "", date($SETTINGS['datetime_format'], strtotime($cash['close']))));
$receipt->appendLine(new ReceiptLine("Cash:", "", '$' . number_format($cash['end_amount'], 2)));

$receipt->appendBlank();

+ 2
- 51
lib/Login.lib.php View File

@@ -45,50 +45,13 @@ class Login {
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);
$resp = AccountHubApi::get("ping");
if ($resp['status'] == "OK") {
return true;
} else {
@@ -107,19 +70,7 @@ class Login {
*/
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;
}
$resp = AccountHubApi::get("ping", null, true);
return false;
} catch (Exception $e) {
return false;

+ 9
- 21
lib/Notifications.lib.php View File

@@ -32,27 +32,15 @@ class Notifications {
$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);
$resp = AccountHubApi::get("addnotification", [
'uid' => $user->getUID(),
'title' => $title,
'content' => $content,
'timestamp' => $timestamp,
'url' => $url,
'sensitive' => $sensitive
]
);
if ($resp['status'] == "OK") {
return $resp['id'] * 1;
} else {

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

@@ -21,6 +21,10 @@ class Strings {

$this->load("en");

if ($language == "en") {
return;
}

if (file_exists(__DIR__ . "/../langs/$language/")) {
$this->language = $language;
$this->load($language);

+ 16
- 147
lib/User.lib.php View File

@@ -17,22 +17,7 @@ class User {

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);
$resp = AccountHubApi::get("userexists", ["uid" => $uid]);
if ($resp['status'] == "OK" && $resp['exists'] === true) {
$this->exists = true;
} else {
@@ -43,22 +28,7 @@ class User {

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);
$resp = AccountHubApi::get("userinfo", ["uid" => $uid]);
if ($resp['status'] == "OK") {
$this->uid = $resp['data']['uid'] * 1;
$this->username = $resp['data']['username'];
@@ -71,22 +41,7 @@ class User {
}

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);
$resp = AccountHubApi::get("userinfo", ["username" => $username]);
if (!isset($resp['status'])) {
sendError("Login server error: " . $resp);
}
@@ -105,22 +60,8 @@ class User {
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);
$resp = AccountHubApi::get("hastotp", ['username' => $this->username]);
if ($resp['status'] == "OK") {
return $resp['otp'] == true;
} else {
@@ -147,26 +88,11 @@ class User {
/**
* Check the given plaintext password against the stored hash.
* @param string $password
* @param bool $apppass Set to true to enforce app passwords when 2fa is on.
* @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);
function checkPassword(string $password, bool $apppass = false): bool {
$resp = AccountHubApi::get("auth", ['username' => $this->username, 'password' => $password, 'apppass' => ($apppass ? "1" : "0")]);
if ($resp['status'] == "OK") {
return true;
} else {
@@ -174,27 +100,13 @@ class User {
}
}


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);
$resp = AccountHubApi::get("verifytotp", ['username' => $this->username, 'code' => $code]);
if ($resp['status'] == "OK") {
return $resp['valid'];
} else {
@@ -209,23 +121,7 @@ class User {
* @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);
$resp = AccountHubApi::get("permission", ['username' => $this->username, 'code' => $code]);
if ($resp['status'] == "OK") {
return $resp['has_permission'];
} else {
@@ -238,23 +134,7 @@ class User {
* @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);
$resp = AccountHubApi::get("acctstatus", ['username' => $this->username]);
if ($resp['status'] == "OK") {
return AccountStatus::fromString($resp['account']);
} else {
@@ -262,24 +142,13 @@ class User {
}
}

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.";
function sendAlertEmail(string $appname = null) {
global $SETTINGS;
if (is_null($appname)) {
$appname = $SETTINGS['site_title'];
}
$resp = AccountHubApi::get("alertemail", ['username' => $this->username, 'appname' => $SETTINGS['site_title']]);

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return true;
} else {

+ 4
- 4
lib/reports.php View File

@@ -38,7 +38,7 @@ if (LOADED) {
}

function getCashFlowReport($register = null, $start = null, $end = null) {
global $database, $Strings;
global $database, $Strings, $SETTINGS;
$where = [];

if (!is_null($register) && $database->has('registers', ['registerid' => $register])) {
@@ -85,11 +85,11 @@ function getCashFlowReport($register = null, $start = null, $end = null) {
foreach ($cash as $c) {
$registers[$c['registerid']]['name'] = $c['registername'];
$registers[$c['registerid']]['id'] = $c['registerid'];
$registers[$c['registerid']]['open'] = date(DATETIME_FORMAT, strtotime($c['open']));
$registers[$c['registerid']]['open'] = date($SETTINGS['datetime_format'], strtotime($c['open']));
if (is_null($c['close'])) {
$registers[$c['registerid']]['close'] = date(DATETIME_FORMAT);
$registers[$c['registerid']]['close'] = date($SETTINGS['datetime_format']);
} else {
$registers[$c['registerid']]['close'] = date(DATETIME_FORMAT, strtotime($c['close']));
$registers[$c['registerid']]['close'] = date($SETTINGS['datetime_format'], strtotime($c['close']));
}
if (!isset($registers[$c['registerid']][$c['typename']])) {
$registers[$c['registerid']][$c['typename']] = 0.0;

+ 17
- 45
mobile/index.php View File

@@ -8,10 +8,6 @@
* Mobile app API
*/

// The name of the permission needed to log in.
// Set to null if you don't need it.
$access_permission = null;

require __DIR__ . "/../required.php";

header('Content-Type: application/json');
@@ -23,21 +19,7 @@ if ($VARS['action'] == "ping") {
}

function mobile_enabled() {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "mobileenabled"
]
]);

if ($response->getStatusCode() > 299) {
return false;
}

$resp = json_decode($response->getBody(), TRUE);
$resp = AccountHubApi::get("mobileenabled");
if ($resp['status'] == "OK" && $resp['mobile'] === TRUE) {
return true;
} else {
@@ -46,26 +28,15 @@ function mobile_enabled() {
}

function mobile_valid($username, $code) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
"code" => $code,
"username" => $username,
'action' => "mobilevalid"
]
]);
try {
$resp = AccountHubApi::get("mobilevalid", ["code" => $code, "username" => $username], true);

if ($response->getStatusCode() > 299) {
return false;
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK" && $resp['valid'] === TRUE) {
return true;
} else {
if ($resp['status'] == "OK" && $resp['valid'] === TRUE) {
return true;
} else {
return false;
}
} catch (Exception $ex) {
return false;
}
}
@@ -75,7 +46,7 @@ if (mobile_enabled() !== TRUE) {
}

// Make sure we have a username and access key
if (is_empty($VARS['username']) || is_empty($VARS['key'])) {
if (empty($VARS['username']) || empty($VARS['key'])) {
http_response_code(401);
die(json_encode(["status" => "ERROR", "msg" => "Missing username and/or access key."]));
}
@@ -95,13 +66,14 @@ switch ($VARS['action']) {
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" => $Strings->get("no admin permission", false)]));
foreach ($SETTINGS['permissions'] as $perm) {
if (!$user->hasPermission($perm)) {
exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no permission", false)]));
}
}
Session::start($user);
$_SESSION['mobile'] = true;
exit(json_encode(["status" => "OK"]));
}
}
}

+ 8
- 0
pages.php View File

@@ -138,5 +138,13 @@ define("PAGES", [
],
"404" => [
"title" => "404 error"
],
"form" => [
"title" => "Form",
"navbar" => true,
"icon" => "fas fa-file-alt",
"scripts" => [
"static/js/form.js"
]
]
]);

+ 1
- 1
pages/certificates.php View File

@@ -39,7 +39,7 @@ $cards = $database->select('certificates', ['certid (id)', 'certcode (code)', 'a
<td><?php echo $c['code']; ?></td>
<td>$<?php echo number_format($c['amount'], 2); ?></td>
<td>$<?php echo number_format($c['start'], 2); ?></td>
<td><?php echo date(DATETIME_FORMAT, strtotime($c['issued'])); ?></td>
<td><?php echo date($SETTINGS['datetime_format'], strtotime($c['issued'])); ?></td>
</tr>
<?php
}

+ 1
- 1
pages/editcertificate.php View File

@@ -16,7 +16,7 @@ $carddata = [

$editing = false;

if (!empty($VARS['id']) && !is_empty($VARS['id'])) {
if (!empty($VARS['id'])) {
if ($database->has('certificates', ['certid' => $VARS['id']])) {
$editing = true;
$carddata = $database->get(

+ 1
- 1
pages/editcustomer.php View File

@@ -18,7 +18,7 @@ $custdata = [

$editing = false;

if (!empty($VARS['id']) && !is_empty($VARS['id'])) {
if (!empty($VARS['id'])) {
if ($database->has('customers', ['customerid' => $VARS['id']])) {
$editing = true;
$custdata = $database->get(

+ 2
- 2
pages/editregister.php View File

@@ -70,9 +70,9 @@ if ($editing) {
for ($i = count($cash) - 1; $i >= 0; $i--) {
$c = $cash[$i];
echo "<option value=\"$c[cashid]\">"
. date(DATETIME_FORMAT, strtotime($c['open']))
. date($SETTINGS['datetime_format'], strtotime($c['open']))
. ' - '
. date(DATETIME_FORMAT, strtotime($c['close']))
. date($SETTINGS['datetime_format'], strtotime($c['close']))
. "</option>";
}
?>

+ 1
- 1
pages/pos.php View File

@@ -18,7 +18,7 @@ if (isset($_GET['switch']) || !isset($_SESSION['register']) || !$registeropen) {
require_once __DIR__ . "/../lib/chooseregister.php";
} else {
$register = $database->get('registers', ['registerid (id)', 'registername (name)'], ['registerid' => $_SESSION['register']]);
$showgridbydefault = $binstack->count('items', ['AND' => ['price[!]' => null, 'price[!]' => 0]]) <= GRID_BY_DEFAULT_MAX_ITEMS;
$showgridbydefault = $binstack->count('items', ['AND' => ['price[!]' => null, 'price[!]' => 0]]) <= $SETTINGS['grid_default_max_items'];
$items = [];
$payments = [];
$editing = false;

+ 2
- 2
pages/registers.php View File

@@ -65,8 +65,8 @@ $registers = $database->select('registers', ['registerid (id)', 'registername (n
$balance += $row;
}
}
$open = date(DATETIME_FORMAT, strtotime($cash['open']));
$close = is_null($cash['close']) ? $Strings->get("still open", false) : date(DATETIME_FORMAT, strtotime($cash['close']));
$open = date($SETTINGS['datetime_format'], strtotime($cash['open']));
$close = is_null($cash['close']) ? $Strings->get("still open", false) : date($SETTINGS['datetime_format'], strtotime($cash['close']));
}
?>
<tr>

+ 2
- 1
public/lib/item.php View File

@@ -108,7 +108,8 @@ class ItemImage {
}

function getAbsoluteUrl(): string {
return BINSTACK_URL_IMAGEPHP . "?i=" . $this->url;
global $SETTINGS;
return $SETTINGS['binstack_image.php'] . "?i=" . $this->url;
}

function isPrimary(): bool {

+ 1
- 1
public/parts/account.php View File

@@ -34,7 +34,7 @@ if ($loggedin !== true || is_null($account)) {
}
?>
<div class="list-group-item">
Date: <?php echo date(DATETIME_FORMAT, strtotime($o['txdate'])); ?><br />
Date: <?php echo date($SETTINGS['datetime_format'], strtotime($o['txdate'])); ?><br />
Type: <?php
switch ($o['type']) {
case 1:

+ 17
- 26
public/required.php View File

@@ -40,8 +40,8 @@ header("Content-Security-Policy: "
. "frame-src 'self'; "
. "font-src 'self'; "
. "connect-src *; "
. "style-src 'self' 'nonce-$SECURE_NONCE' $captcha_server; "
. "script-src 'self' 'nonce-$SECURE_NONCE' $captcha_server");
. "style-src 'self' 'nonce-$SECURE_NONCE'; "
. "script-src 'self' 'nonce-$SECURE_NONCE'");

//
// Composer
@@ -69,7 +69,7 @@ function sendError($error) {
. "<p>" . htmlspecialchars($error) . "</p>");
}

date_default_timezone_set(TIMEZONE);
date_default_timezone_set($SETTINGS['timezone']);

// Database settings
// Also inits database and stuff
@@ -79,20 +79,20 @@ $database;
$binstack;
try {
$database = new Medoo([
'database_type' => DB_TYPE,
'database_name' => DB_NAME,
'server' => DB_SERVER,
'username' => DB_USER,
'password' => DB_PASS,
'charset' => DB_CHARSET
'database_type' => $SETTINGS['database']['type'],
'database_name' => $SETTINGS['database']['name'],
'server' => $SETTINGS['database']['server'],
'username' => $SETTINGS['database']['user'],
'password' => $SETTINGS['database']['password'],
'charset' => $SETTINGS['database']['charset']
]);
$binstack = new Medoo([
'database_type' => BINSTACK_DB_TYPE,
'database_name' => BINSTACK_DB_NAME,
'server' => BINSTACK_DB_SERVER,
'username' => BINSTACK_DB_USER,
'password' => BINSTACK_DB_PASS,
'charset' => BINSTACK_DB_CHARSET
'database_type' => $SETTINGS['binstack_database']['type'],
'database_name' => $SETTINGS['binstack_database']['name'],
'server' => $SETTINGS['binstack_database']['server'],
'username' => $SETTINGS['binstack_database']['user'],
'password' => $SETTINGS['binstack_database']['password'],
'charset' => $SETTINGS['binstack_database']['charset']
]);
} catch (Exception $ex) {
//header('HTTP/1.1 500 Internal Server Error');
@@ -100,7 +100,7 @@ try {
}


if (!DEBUG) {
if (!$SETTINGS['debug']) {
error_reporting(0);
} else {
error_reporting(E_ALL);
@@ -127,13 +127,4 @@ if (!empty($_SESSION['shop_account'])) {
} else {
$account = null;
}
}

/**