diff --git a/README.md b/README.md index eb517fd..3ca9a62 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ NickelBox ========= NickelBox is a point of sale app. It integrates with BinStack for inventory -management. +management. \ No newline at end of file diff --git a/action.php b/action.php index 54e85c5..dce5208 100644 --- a/action.php +++ b/action.php @@ -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() ]; } } diff --git a/api.php b/api.php index f3490fb..03178ea 100644 --- a/api.php +++ b/api.php @@ -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; diff --git a/app.php b/app.php index 4401b32..51826fe 100644 --- a/app.php +++ b/app.php @@ -28,10 +28,10 @@ header("Link: ; rel=preload; as=style", false); header("Link: ; rel=preload; as=style", false); header("Link: ; rel=preload; as=style", false); header("Link: ; rel=preload; as=style", false); -header("Link: ; rel=preload; as=style", false); +header("Link: ; rel=preload; as=style", false); header("Link: ; rel=preload; as=script", false); header("Link: ; rel=preload; as=script", false); -header("Link: ; rel=preload; as=script", false); +header("Link: ; rel=preload; as=script", false); ?> @@ -47,7 +47,7 @@ header("Link: ; rel=preload; as=script", false); - + @@ -69,9 +69,9 @@ header("Link: ; 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'])) { ?> get($pg['title']); ?> @@ -163,7 +163,7 @@ END; -   +  get("sign out") ?> @@ -182,7 +182,7 @@ END; - + =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": [] -} diff --git a/index.php b/index.php index 59a8242..3c992b3 100644 --- a/index.php +++ b/index.php @@ -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: ; rel=preload; as=style", false); header("Link: ; rel=preload; as=style", false); header("Link: ; rel=preload; as=style", false); header("Link: ; rel=preload; as=style", false); header("Link: ; rel=preload; as=script", false); -header("Link: ; rel=preload; as=script", false); +header("Link: ; rel=preload; as=script", false); ?> @@ -114,7 +125,7 @@ header("Link: ; rel=preload; as=script", false);
-
+
get("sign in"); ?>
; rel=preload; as=script", false); if ($multiauth != true) { ?> - " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus />
- " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
+ " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus />
+ " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />

@@ -138,16 +149,16 @@ header("Link: ; rel=preload; as=script", false); } else if ($multiauth) { ?>
- + get("2fa prompt"); ?>
- " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus />
+ " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus />
@@ -159,6 +170,6 @@ header("Link: ; rel=preload; as=script", false);
- + \ No newline at end of file diff --git a/lang/en_us.php b/lang/en_us.php deleted file mode 100644 index 350f114..0000000 --- a/lang/en_us.php +++ /dev/null @@ -1,144 +0,0 @@ - "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", -]); diff --git a/langs/en/core.json b/langs/en/core.json new file mode 100644 index 0000000..5e55996 --- /dev/null +++ b/langs/en/core.json @@ -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." +} diff --git a/langs/en/strings.json b/langs/en/strings.json new file mode 100644 index 0000000..f368133 --- /dev/null +++ b/langs/en/strings.json @@ -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" +} \ No newline at end of file diff --git a/langs/en/titles.json b/langs/en/titles.json new file mode 100644 index 0000000..6fbf103 --- /dev/null +++ b/langs/en/titles.json @@ -0,0 +1,4 @@ +{ + "home": "Home", + "test": "Test" +} diff --git a/lang/messages.php b/langs/messages.php similarity index 100% rename from lang/messages.php rename to langs/messages.php diff --git a/lib/Exceptions.lib.php b/lib/Exceptions.lib.php new file mode 100644 index 0000000..baec83f --- /dev/null +++ b/lib/Exceptions.lib.php @@ -0,0 +1,13 @@ +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; diff --git a/lib/IPUtils.lib.php b/lib/IPUtils.lib.php new file mode 100644 index 0000000..fe70f21 --- /dev/null +++ b/lib/IPUtils.lib.php @@ -0,0 +1,135 @@ + + */ + 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. + */ + 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 + } + +} diff --git a/lib/Login.lib.php b/lib/Login.lib.php new file mode 100644 index 0000000..fe22a38 --- /dev/null +++ b/lib/Login.lib.php @@ -0,0 +1,129 @@ +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; + } + } + +} diff --git a/lib/Notifications.lib.php b/lib/Notifications.lib.php new file mode 100644 index 0000000..c1d93a9 --- /dev/null +++ b/lib/Notifications.lib.php @@ -0,0 +1,65 @@ +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)); + } + +} diff --git a/lib/Receipt.lib.php b/lib/Receipt.lib.php new file mode 100644 index 0000000..4c75a3f --- /dev/null +++ b/lib/Receipt.lib.php @@ -0,0 +1,124 @@ +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 = << + + +$title + +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)); + } + +} \ No newline at end of file diff --git a/lib/receipts.php b/lib/ReceiptLine.lib.php similarity index 57% rename from lib/receipts.php rename to lib/ReceiptLine.lib.php index 31a5c25..6f3fe05 100644 --- a/lib/receipts.php +++ b/lib/ReceiptLine.lib.php @@ -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 = << - - -$title - -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)); - } - -} diff --git a/lib/Report.lib.php b/lib/Report.lib.php new file mode 100644 index 0000000..73f1305 --- /dev/null +++ b/lib/Report.lib.php @@ -0,0 +1,137 @@ +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 = "\n" + . "\n" + . "\n" + . "" . $this->title . "_" . date("Y-m-d_Hi") . "\n" + . << +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; + } + +} diff --git a/lib/Session.lib.php b/lib/Session.lib.php new file mode 100644 index 0000000..4987732 --- /dev/null +++ b/lib/Session.lib.php @@ -0,0 +1,19 @@ +getUsername(); + $_SESSION['uid'] = $user->getUID(); + $_SESSION['email'] = $user->getEmail(); + $_SESSION['realname'] = $user->getName(); + $_SESSION['loggedin'] = true; + } + +} diff --git a/lib/Strings.lib.php b/lib/Strings.lib.php new file mode 100644 index 0000000..b094bbf --- /dev/null +++ b/lib/Strings.lib.php @@ -0,0 +1,118 @@ +load("en"); + + if (file_exists(__DIR__ . "/../langs/$language/")) { + $this->language = $language; + $this->load($language); + } else { + trigger_error("Language $language could not be found.", E_USER_WARNING); + } + } + + /** + * Load all JSON files for the specified language. + * @param string $language + */ + private function load(string $language) { + $files = glob(__DIR__ . "/../langs/$language/*.json"); + foreach ($files as $file) { + $strings = json_decode(file_get_contents($file), true); + foreach ($strings as $key => $val) { + if (array_key_exists($key, $this->strings)) { + trigger_error("Language key \"$key\" is defined more than once.", E_USER_WARNING); + } + $this->strings[$key] = $val; + } + } + } + + /** + * Add language strings dynamically. + * @param array $strings ["key" => "value", ...] + */ + public function addStrings(array $strings) { + foreach ($strings as $key => $val) { + $this->strings[$key] = $val; + } + } + + /** + * I18N string getter. If the key isn't found, it outputs the key itself. + * @param string $key + * @param bool $echo True to echo the result, false to return it. Default is true. + * @return string + */ + public function get(string $key, bool $echo = true): string { + $str = $key; + if (array_key_exists($key, $this->strings)) { + $str = $this->strings[$key]; + } else { + trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING); + } + + if ($echo) { + echo $str; + } + return $str; + } + + /** + * I18N string getter (with builder). If the key doesn't exist, outputs the key itself. + * @param string $key + * @param array $replace key-value array of replacements. + * If the string value is "hello {abc}" and you give ["abc" => "123"], the + * result will be "hello 123". + * @param bool $echo True to echo the result, false to return it. Default is true. + * @return string + */ + public function build(string $key, array $replace, bool $echo = true): string { + $str = $key; + if (array_key_exists($key, $this->strings)) { + $str = $this->strings[$key]; + } else { + trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING); + } + + foreach ($replace as $find => $repl) { + $str = str_replace("{" . $find . "}", $repl, $str); + } + + if ($echo) { + echo $str; + } + return $str; + } + + /** + * Builds and returns a JSON key:value string for the supplied array of keys. + * @param array $keys ["key1", "key2", ...] + */ + public function getJSON(array $keys): string { + $strings = []; + foreach ($keys as $k) { + $strings[$k] = $this->get($k, false); + } + return json_encode($strings); + } + +} diff --git a/lib/User.lib.php b/lib/User.lib.php new file mode 100644 index 0000000..7852e31 --- /dev/null +++ b/lib/User.lib.php @@ -0,0 +1,352 @@ +request('POST', PORTAL_API, [ + 'form_params' => [ + 'key' => PORTAL_KEY, + 'action' => "userexists", + 'uid' => $uid + ] + ]); + + if ($response->getStatusCode() > 299) { + sendError("Login server error: " . $response->getBody()); + } + + $resp = json_decode($response->getBody(), TRUE); + if ($resp['status'] == "OK" && $resp['exists'] === true) { + $this->exists = true; + } else { + $this->uid = $uid; + $this->username = $username; + $this->exists = false; + } + + if ($this->exists) { + // Get user info + $client = new GuzzleHttp\Client(); + + $response = $client + ->request('POST', PORTAL_API, [ + 'form_params' => [ + 'key' => PORTAL_KEY, + 'action' => "userinfo", + 'uid' => $uid + ] + ]); + + if ($response->getStatusCode() > 299) { + sendError("Login server error: " . $response->getBody()); + } + + $resp = json_decode($response->getBody(), TRUE); + if ($resp['status'] == "OK") { + $this->uid = $resp['data']['uid'] * 1; + $this->username = $resp['data']['username']; + $this->email = $resp['data']['email']; + $this->realname = $resp['data']['name']; + } else { + sendError("Login server error: " . $resp['msg']); + } + } + } + + public static function byUsername(string $username): User { + $client = new GuzzleHttp\Client(); + + $response = $client + ->request('POST', PORTAL_API, [ + 'form_params' => [ + 'key' => PORTAL_KEY, + 'username' => $username, + 'action' => "userinfo" + ] + ]); + + if ($response->getStatusCode() > 299) { + sendError("Login server error: " . $response->getBody()); + } + + $resp = json_decode($response->getBody(), TRUE); + if (!isset($resp['status'])) { + sendError("Login server error: " . $resp); + } + if ($resp['status'] == "OK") { + return new self($resp['data']['uid'] * 1); + } else { + return new self(-1, $username); + } + } + + public function exists(): bool { + return $this->exists; + } + + public function has2fa(): bool { + if (!$this->exists) { + return false; + } + $client = new GuzzleHttp\Client(); + + $response = $client + ->request('POST', PORTAL_API, [ + 'form_params' => [ + 'key' => PORTAL_KEY, + 'action' => "hastotp", + 'username' => $this->username + ] + ]); + + if ($response->getStatusCode() > 299) { + sendError("Login server error: " . $response->getBody()); + } + + $resp = json_decode($response->getBody(), TRUE); + if ($resp['status'] == "OK") { + return $resp['otp'] == true; + } else { + return false; + } + } + + function getUsername() { + return $this->username; + } + + function getUID() { + return $this->uid; + } + + function getEmail() { + return $this->email; + } + + function getName() { + return $this->realname; + } + + /** + * Check the given plaintext password against the stored hash. + * @param string $password + * @return bool + */ + function checkPassword(string $password): bool { + $client = new GuzzleHttp\Client(); + + $response = $client + ->request('POST', PORTAL_API, [ + 'form_params' => [ + 'key' => PORTAL_KEY, + 'action' => "auth", + 'username' => $this->username, + 'password' => $password + ] + ]); + + if ($response->getStatusCode() > 299) { + sendError("Login server error: " . $response->getBody()); + } + + $resp = json_decode($response->getBody(), TRUE); + if ($resp['status'] == "OK") { + return true; + } else { + return false; + } + } + + function check2fa(string $code): bool { + if (!$this->has2fa) { + return true; + } + $client = new GuzzleHttp\Client(); + + $response = $client + ->request('POST', PORTAL_API, [ + 'form_params' => [ + 'key' => PORTAL_KEY, + 'action' => "verifytotp", + 'username' => $this->username, + 'code' => $code + ] + ]); + + if ($response->getStatusCode() > 299) { + sendError("Login server error: " . $response->getBody()); + } + + $resp = json_decode($response->getBody(), TRUE); + if ($resp['status'] == "OK") { + return $resp['valid']; + } else { + return false; + } + } + + /** + * Check if the given username has the given permission (or admin access) + * @global $database $database + * @param string $code + * @return boolean TRUE if the user has the permission (or admin access), else FALSE + */ + function hasPermission(string $code): bool { + $client = new GuzzleHttp\Client(); + + $response = $client + ->request('POST', PORTAL_API, [ + 'form_params' => [ + 'key' => PORTAL_KEY, + 'action' => "permission", + 'username' => $this->username, + 'code' => $code + ] + ]); + + if ($response->getStatusCode() > 299) { + sendError("Login server error: " . $response->getBody()); + } + + $resp = json_decode($response->getBody(), TRUE); + if ($resp['status'] == "OK") { + return $resp['has_permission']; + } else { + return false; + } + } + + /** + * Get the account status. + * @return \AccountStatus + */ + function getStatus(): AccountStatus { + + $client = new GuzzleHttp\Client(); + + $response = $client + ->request('POST', PORTAL_API, [ + 'form_params' => [ + 'key' => PORTAL_KEY, + 'action' => "acctstatus", + 'username' => $this->username + ] + ]); + + if ($response->getStatusCode() > 299) { + sendError("Login server error: " . $response->getBody()); + } + + $resp = json_decode($response->getBody(), TRUE); + if ($resp['status'] == "OK") { + return AccountStatus::fromString($resp['account']); + } else { + return null; + } + } + + function sendAlertEmail(string $appname = SITE_TITLE) { + $client = new GuzzleHttp\Client(); + + $response = $client + ->request('POST', PORTAL_API, [ + 'form_params' => [ + 'key' => PORTAL_KEY, + 'action' => "alertemail", + 'username' => $this->username, + 'appname' => SITE_TITLE + ] + ]); + + if ($response->getStatusCode() > 299) { + return "An unknown error occurred."; + } + + $resp = json_decode($response->getBody(), TRUE); + if ($resp['status'] == "OK") { + return true; + } else { + return $resp['msg']; + } + } + +} + +class AccountStatus { + + const NORMAL = 1; + const LOCKED_OR_DISABLED = 2; + const CHANGE_PASSWORD = 3; + const TERMINATED = 4; + const ALERT_ON_ACCESS = 5; + + private $status; + + public function __construct(int $status) { + $this->status = $status; + } + + public static function fromString(string $status): AccountStatus { + switch ($status) { + case "NORMAL": + return new self(self::NORMAL); + case "LOCKED_OR_DISABLED": + return new self(self::LOCKED_OR_DISABLED); + case "CHANGE_PASSWORD": + return new self(self::CHANGE_PASSWORD); + case "TERMINATED": + return new self(self::TERMINATED); + case "ALERT_ON_ACCESS": + return new self(self::ALERT_ON_ACCESS); + default: + return new self(0); + } + } + + /** + * Get the account status/state as an integer. + * @return int + */ + public function get(): int { + return $this->status; + } + + /** + * Get the account status/state as a string representation. + * @return string + */ + public function getString(): string { + switch ($this->status) { + case self::NORMAL: + return "NORMAL"; + case self::LOCKED_OR_DISABLED: + return "LOCKED_OR_DISABLED"; + case self::CHANGE_PASSWORD: + return "CHANGE_PASSWORD"; + case self::TERMINATED: + return "TERMINATED"; + case self::ALERT_ON_ACCESS: + return "ALERT_ON_ACCESS"; + default: + return "OTHER_" . $this->status; + } + } + +} diff --git a/lib/chooseregister.php b/lib/chooseregister.php index 7b87c6d..746ed2c 100644 --- a/lib/chooseregister.php +++ b/lib/chooseregister.php @@ -12,10 +12,10 @@ $registers = $database->select("registers", ['[>]cash_drawer' => ['registerid' =

- + get("point of sale"); ?>

-
+
get("choose register"); ?>
0) { ?> @@ -34,7 +34,7 @@ $registers = $database->select("registers", ['[>]cash_drawer' => ['registerid' = } else { ?>
- + get("no open registers"); ?>
- */ -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. - */ -function ip6_in_cidr($ip, $cidr) { - $address = inet_pton($ip); - $subnetAddress = inet_pton(explode("/", $cidr)[0]); - $subnetMask = explode("/", $cidr)[1]; - - $addr = str_repeat("f", $subnetMask / 4); - switch ($subnetMask % 4) { - case 0: - break; - case 1: - $addr .= "8"; - break; - case 2: - $addr .= "c"; - break; - case 3: - $addr .= "e"; - break; - } - $addr = str_pad($addr, 32, '0'); - $addr = pack("H*", $addr); - - $binMask = $addr; - return ($address & $binMask) == $subnetAddress; -} - -/** - * Check if the REMOTE_ADDR is on Cloudflare's network. - * @return boolean true if it is, otherwise false - */ -function validateCloudflare() { - if (filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - // Using IPv6 - $cloudflare_ips_v6 = [ - "2400:cb00::/32", - "2405:8100::/32", - "2405:b500::/32", - "2606:4700::/32", - "2803:f800::/32", - "2c0f:f248::/32", - "2a06:98c0::/29" - ]; - $valid = false; - foreach ($cloudflare_ips_v6 as $cidr) { - if (ip6_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) { - $valid = true; - break; - } - } - } else { - // Using IPv4 - $cloudflare_ips_v4 = [ - "103.21.244.0/22", - "103.22.200.0/22", - "103.31.4.0/22", - "104.16.0.0/12", - "108.162.192.0/18", - "131.0.72.0/22", - "141.101.64.0/18", - "162.158.0.0/15", - "172.64.0.0/13", - "173.245.48.0/20", - "188.114.96.0/20", - "190.93.240.0/20", - "197.234.240.0/22", - "198.41.128.0/17" - ]; - $valid = false; - foreach ($cloudflare_ips_v4 as $cidr) { - if (ip4_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) { - $valid = true; - break; - } - } - } - return $valid; -} - -/** - * Makes a good guess at the client's real IP address. - * - * @return string Client IP or `0.0.0.0` if we can't find anything - */ -function getClientIP() { - // If CloudFlare is in the mix, we should use it. - // Check if the request is actually from CloudFlare before trusting it. - if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) { - if (validateCloudflare()) { - return $_SERVER["HTTP_CF_CONNECTING_IP"]; - } - } - - if (isset($_SERVER["REMOTE_ADDR"])) { - return $_SERVER["REMOTE_ADDR"]; - } - - return "0.0.0.0"; // This will not happen unless we aren't a web server -} diff --git a/lib/login.php b/lib/login.php deleted file mode 100644 index a9b290f..0000000 --- a/lib/login.php +++ /dev/null @@ -1,402 +0,0 @@ -request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "ping" - ] - ]); - - if ($response->getStatusCode() != 200) { - return false; - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return true; - } else { - return false; - } - } catch (Exception $e) { - return false; - } -} - -/** - * Checks if the given AccountHub API key is valid by attempting to - * access the API with it. - * @param String $key The API key to check - * @return boolean TRUE if the key is valid, FALSE if invalid or something went wrong - */ -function checkAPIKey($key) { - try { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => $key, - 'action' => "ping" - ] - ]); - - if ($response->getStatusCode() === 200) { - return true; - } - return false; - } catch (Exception $e) { - return false; - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Account handling // -//////////////////////////////////////////////////////////////////////////////// - -/** - * Checks the given credentials against the API. - * @param string $username - * @param string $password - * @return boolean True if OK, else false - */ -function authenticate_user($username, $password, &$errmsg) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "auth", - 'username' => $username, - 'password' => $password - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return true; - } else { - $errmsg = $resp['msg']; - return false; - } -} - -/** - * Check if a username exists. - * @param String $username - */ -function user_exists($username) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "userexists", - 'username' => $username - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK" && $resp['exists'] === true) { - return true; - } else { - return false; - } -} - -/** - * Check if a UID exists. - * @param String $uid - */ -function uid_exists($uid) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "userexists", - 'uid' => $uid - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK" && $resp['exists'] === true) { - return true; - } else { - return false; - } -} - -/** - * Get the account status: NORMAL, TERMINATED, LOCKED_OR_DISABLED, - * CHANGE_PASSWORD, or ALERT_ON_ACCESS - * @param string $username - * @return string - */ -function get_account_status($username) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "acctstatus", - 'username' => $username - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return $resp['account']; - } else { - return false; - } -} - -/** - * Check if the given username has the given permission (or admin access) - * @param string $username - * @param string $permcode - * @return boolean TRUE if the user has the permission (or admin access), else FALSE - */ -function account_has_permission($username, $permcode) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "permission", - 'username' => $username, - 'code' => $permcode - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return $resp['has_permission']; - } else { - return false; - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Login handling // -//////////////////////////////////////////////////////////////////////////////// - -/** - * Setup $_SESSION values with user data and set loggedin flag to true - * @param string $username - */ -function doLoginUser($username) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "userinfo", - 'username' => $username - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - - if ($resp['status'] == "OK") { - $userinfo = $resp['data']; - session_regenerate_id(true); - $newSession = session_id(); - session_write_close(); - session_id($newSession); - session_start(); - $_SESSION['username'] = $username; - $_SESSION['uid'] = $userinfo['uid']; - $_SESSION['email'] = $userinfo['email']; - $_SESSION['realname'] = $userinfo['name']; - $_SESSION['loggedin'] = true; - return true; - } else { - return false; - } -} - -function sendLoginAlertEmail($username) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "alertemail", - 'username' => $username, - 'appname' => SITE_TITLE - ] - ]); - - if ($response->getStatusCode() > 299) { - return "An unknown error occurred."; - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return true; - } else { - return $resp['msg']; - } -} - -function simLogin($username, $password) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "login", - 'username' => $username, - 'password' => $password - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return true; - } else { - return $resp['msg']; - } -} - -function verifyCaptcheck($session, $answer, $url) { - $data = [ - 'session_id' => $session, - 'answer_id' => $answer, - 'action' => "verify" - ]; - $options = [ - 'http' => [ - 'header' => "Content-type: application/x-www-form-urlencoded\r\n", - 'method' => 'POST', - 'content' => http_build_query($data) - ] - ]; - $context = stream_context_create($options); - $result = file_get_contents($url, false, $context); - $resp = json_decode($result, TRUE); - if (!$resp['result']) { - return false; - } else { - return true; - } -} - -//////////////////////////////////////////////////////////////////////////////// -// 2-factor authentication // -//////////////////////////////////////////////////////////////////////////////// - -/** - * Check if a user has TOTP setup - * @param string $username - * @return boolean true if TOTP secret exists, else false - */ -function userHasTOTP($username) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "hastotp", - 'username' => $username - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return $resp['otp']; - } else { - return false; - } -} - -/** - * Verify a TOTP multiauth code - * @global $database - * @param string $username - * @param int $code - * @return boolean true if it's legit, else false - */ -function verifyTOTP($username, $code) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "verifytotp", - 'username' => $username, - 'code' => $code - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return $resp['valid']; - } else { - return false; - } -} diff --git a/lib/reports.php b/lib/reports.php index 7fce0e0..639360d 100644 --- a/lib/reports.php +++ b/lib/reports.php @@ -14,63 +14,31 @@ if (count(get_included_files()) == 1) { require_once __DIR__ . "/../required.php"; -use League\Csv\Writer; -use League\Csv\HTMLConverter; -use odsPhpGenerator\ods; -use odsPhpGenerator\odsTable; -use odsPhpGenerator\odsTableRow; -use odsPhpGenerator\odsTableColumn; -use odsPhpGenerator\odsTableCellString; -use odsPhpGenerator\odsStyleTableColumn; -use odsPhpGenerator\odsStyleTableCell; - -require_once __DIR__ . "/userinfo.php"; -require_once __DIR__ . "/login.php"; - // Allow access with a download code, for mobile app and stuff $date = date("Y-m-d H:i:s"); -$allowed_users = []; -$requester = -1; if (isset($VARS['code']) && LOADED) { if (!$database->has('report_access_codes', ["AND" => ['code' => $VARS['code'], 'expires[>]' => $date]])) { dieifnotloggedin(); - $requester = $_SESSION['uid']; - } else { - $requester = $database->get('report_access_codes', 'uid', ['code' => $VARS['code']]); } } else { dieifnotloggedin(); - $requester = $_SESSION['uid']; -} - -if (account_has_permission($_SESSION['username'], "ADMIN")) { - $allowed_users = true; -} else { - if (account_has_permission($_SESSION['username'], "QWIKCLOCK_MANAGE")) { - $allowed_users = getManagedUIDs($requester); - } - - if (account_has_permission($_SESSION['username'], "QWIKCLOCK_EDITSELF")) { - $allowed_users[] = $_SESSION['uid']; - } } // Delete old DB entries $database->delete('report_access_codes', ['expires[<=]' => $date]); if (LOADED) { - $user = null; if (isset($VARS['type']) && isset($VARS['format'])) { generateReport($VARS['type'], $VARS['format'], $VARS['register'], $VARS['startdate'], $VARS['enddate']); die(); } else { - lang("invalid parameters"); + $Strings->get("invalid parameters"); die(); } } function getCashFlowReport($register = null, $start = null, $end = null) { - global $database; + global $database, $Strings; $where = []; if (!is_null($register) && $database->has('registers', ['registerid' => $register])) { @@ -108,8 +76,9 @@ function getCashFlowReport($register = null, $start = null, $end = null) { "payment_types.typename" ], $where ); - $header = [lang("register", false), lang("open", false), lang("close", false), lang("cash", false), lang("card", false), lang("check", false), lang("crypto", false), lang("gift card", false), lang("free", false)]; - $out = [$header]; + + $report = new Report($Strings->get("cashflow", false)); + $report->setHeader([$Strings->get("register", false), $Strings->get("open", false), $Strings->get("close", false), $Strings->get("cash", false), $Strings->get("card", false), $Strings->get("check", false), $Strings->get("crypto", false), $Strings->get("gift card", false), $Strings->get("free", false)]); $registers = []; @@ -135,7 +104,7 @@ function getCashFlowReport($register = null, $start = null, $end = null) { $r[$t] = 0.0; } } - $out[] = [ + $report->addDataRow([ $r['name'], $r['open'], $r['close'], @@ -145,153 +114,22 @@ function getCashFlowReport($register = null, $start = null, $end = null) { $r['crypto'] . "", $r['giftcard'] . "", $r['free'] . "" - ]; + ]); } - return $out; + return $report; } -function getReportData($type, $register = null, $start = null, $end = null) { +function getReport(string $type, $register = null, $start = null, $end = null): Report { switch ($type) { case "cashflow": return getCashFlowReport($register, $start, $end); default: - return [["error"]]; + return new Report("error", ["ERROR"], ["Invalid report type."]); } } -function dataToCSV($data, $name = "report", $register = null, $start = null, $end = null) { - $csv = Writer::createFromString(''); - $usernotice = ""; - $usertitle = ""; - $datetitle = ""; - if ($start != null && (bool) strtotime($start)) { - $datenotice = lang2("report filtered to start date", ["date" => date(DATE_FORMAT, strtotime($start))], false); - $datetitle = "_" . date(DATE_FORMAT, strtotime($start)); - $csv->insertOne([$datenotice]); - } - if ($end != null && (bool) strtotime($end)) { - $datenotice = lang2("report filtered to end date", ["date" => date(DATE_FORMAT, strtotime($end))], false); - $datetitle .= ($datetitle == "" ? "_" : "-") . date(DATE_FORMAT, strtotime($end)); - $csv->insertOne([$datenotice]); - } - $csv->insertAll($data); - header('Content-type: text/csv'); - header('Content-Disposition: attachment; filename="' . $name . $usertitle . $datetitle . "_" . date("Y-m-d_Hi") . ".csv" . '"'); - echo $csv; - die(); -} - -function dataToODS($data, $name = "report", $register = null, $start = null, $end = null) { - $ods = new ods(); - $styleColumn = new odsStyleTableColumn(); - $styleColumn->setUseOptimalColumnWidth(true); - $headerstyle = new odsStyleTableCell(); - $headerstyle->setFontWeight("bold"); - $table = new odsTable($name); - - for ($i = 0; $i < count($data[0]); $i++) { - $table->addTableColumn(new odsTableColumn($styleColumn)); - } - - $usernotice = ""; - $usertitle = ""; - $datetitle = ""; - if ($user != null && array_key_exists('username', $user) && array_key_exists('name', $user)) { - $usernotice = lang2("report filtered to user", ["name" => $user['name'], "username" => $user['username']], false); - $usertitle = "_" . $user['username']; - $row = new odsTableRow(); - $row->addCell(new odsTableCellString($usernotice)); - $table->addRow($row); - } - if ($start != null && (bool) strtotime($start)) { - $datenotice = lang2("report filtered to start date", ["date" => date(DATE_FORMAT, strtotime($start))], false); - $datetitle = "_" . date(DATE_FORMAT, strtotime($start)); - $row = new odsTableRow(); - $row->addCell(new odsTableCellString($datenotice)); - $table->addRow($row); - } - if ($end != null && (bool) strtotime($end)) { - $datenotice = lang2("report filtered to end date", ["date" => date(DATE_FORMAT, strtotime($end))], false); - $datetitle .= ($datetitle == "" ? "_" : "-") . date(DATE_FORMAT, strtotime($end)); - $row = new odsTableRow(); - $row->addCell(new odsTableCellString($datenotice)); - $table->addRow($row); - } - - $rowid = 0; - foreach ($data as $datarow) { - $row = new odsTableRow(); - foreach ($datarow as $cell) { - if ($rowid == 0) { - $row->addCell(new odsTableCellString($cell, $headerstyle)); - } else { - $row->addCell(new odsTableCellString($cell)); - } - } - $table->addRow($row); - $rowid++; - } - $ods->addTable($table); - $ods->downloadOdsFile($name . $usertitle . $datetitle . "_" . date("Y-m-d_Hi") . ".ods"); -} - -function dataToHTML($data, $name = "report", $register = null, $start = null, $end = null) { - global $SECURE_NONCE; - // HTML exporter doesn't like null values - for ($i = 0; $i < count($data); $i++) { - for ($j = 0; $j < count($data[$i]); $j++) { - if (is_null($data[$i][$j])) { - $data[$i][$j] = ''; - } - } - } - $datenotice = ""; - $datetitle = ""; - if ($start != null && (bool) strtotime($start)) { - $datenotice = "" . lang2("report filtered to start date", ["date" => date(DATE_FORMAT, strtotime($start))], false) . "
"; - $datetitle = "_" . date(DATE_FORMAT, strtotime($start)); - } - if ($end != null && (bool) strtotime($end)) { - $datenotice .= "" . lang2("report filtered to end date", ["date" => date(DATE_FORMAT, strtotime($end))], false) . "
"; - $datetitle .= ($datetitle == "" ? "_" : "-") . date(DATE_FORMAT, strtotime($end)); - } - header('Content-type: text/html'); - $converter = new HTMLConverter(); - $out = "\n" - . "\n" - . "\n" - . "" . $name . $datetitle . "_" . date("Y-m-d_Hi") . "\n" - . << -STYLE - . $datenotice - . $converter->convert($data); - echo $out; -} - -function generateReport($type, $format, $register = null, $start = null, $end = null, $deleted = true) { - $data = getReportData($type, $register, $start, $end, $deleted); - switch ($format) { - case "ods": - dataToODS($data, $type, $register, $start, $end); - break; - case "html": - dataToHTML($data, $type, $register, $start, $end); - break; - case "csv": - default: - echo dataToCSV($data, $type, $register, $start, $end); - break; - } +function generateReport($type, $format, $register = null, $start = null, $end = null) { + $report = getReport($type, $register, $start, $end); + $report->output($format); } diff --git a/lib/userinfo.php b/lib/userinfo.php deleted file mode 100644 index 65f6b38..0000000 --- a/lib/userinfo.php +++ /dev/null @@ -1,127 +0,0 @@ -request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "userinfo", - 'username' => $u - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return $resp['data']; - } else { - // this shouldn't happen, but in case it does just fake it. - return ["name" => $u, "username" => $u, "uid" => $u]; - } -} - -/** - * Get user info for the given UID. - * @param int $u user ID - * @return [string] Array of [uid, username, name] - */ -function getUserByID($u) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "userinfo", - 'uid' => $u - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return $resp['data']; - } else { - // this shouldn't happen, but in case it does just fake it. - return ["name" => $u, "username" => $u, "uid" => $u]; - } -} - -/** - * Check if the first UID is a manager of the second UID. - * @param int $m Manager UID - * @param int $e Employee UID - * @return boolean - */ -function isManagerOf($m, $e) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "ismanagerof", - 'manager' => $m, - 'employee' => $e, - 'uid' => 1 - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return $resp['managerof'] === true; - } else { - // this shouldn't happen, but in case it does just fake it. - return false; - } -} - -/** - * Get an array of UIDs the given UID is a manager of. - * @param int $manageruid The UID of the manager to find employees for. - * @return [int] - */ -function getManagedUIDs($manageruid) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "getmanaged", - 'uid' => $manageruid - ] - ]); - - if ($response->getStatusCode() > 299) { - sendError("Login server error: " . $response->getBody()); - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK") { - return $resp['employees']; - } else { - return []; - } -} diff --git a/mobile/index.php b/mobile/index.php index 4143217..5cc4575 100644 --- a/mobile/index.php +++ b/mobile/index.php @@ -14,8 +14,6 @@ $access_permission = null; require __DIR__ . "/../required.php"; -require __DIR__ . "/../lib/login.php"; - header('Content-Type: application/json'); header('Access-Control-Allow-Origin: *'); @@ -73,7 +71,7 @@ function mobile_valid($username, $code) { } if (mobile_enabled() !== TRUE) { - exit(json_encode(["status" => "ERROR", "msg" => lang("mobile login disabled", false)])); + exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("mobile login disabled", false)])); } // Make sure we have a username and access key @@ -93,20 +91,21 @@ if (!mobile_valid($VARS['username'], $VARS['key'])) { switch ($VARS['action']) { case "start_session": // Do a web login. - if (user_exists($VARS['username'])) { - if (get_account_status($VARS['username']) == "NORMAL") { - if (authenticate_user($VARS['username'], $VARS['password'], $autherror)) { - if (is_null($access_permission) || account_has_permission($VARS['username'], $access_permission)) { - doLoginUser($VARS['username'], $VARS['password']); + $user = User::byUsername($VARS['username']); + if ($user->exists()) { + if ($user->getStatus()->getString() == "NORMAL") { + if ($user->checkPassword($VARS['password'])) { + if (is_null($access_permission) || $user->hasPermission($access_permission)) { + Session::start($user); $_SESSION['mobile'] = true; exit(json_encode(["status" => "OK"])); } else { - exit(json_encode(["status" => "ERROR", "msg" => lang("no admin permission", false)])); + exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no admin permission", false)])); } } } } - exit(json_encode(["status" => "ERROR", "msg" => lang("login incorrect", false)])); + exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("login incorrect", false)])); default: http_response_code(404); die(json_encode(["status" => "ERROR", "msg" => "The requested action is not available."])); diff --git a/pages/404.php b/pages/404.php index 62a0f89..7a2be4e 100644 --- a/pages/404.php +++ b/pages/404.php @@ -5,6 +5,6 @@ ?>
-

+
get("404 error");?>
get("page not found"); ?>
\ No newline at end of file diff --git a/pages/certificates.php b/pages/certificates.php index 2a1611c..133e89f 100644 --- a/pages/certificates.php +++ b/pages/certificates.php @@ -7,26 +7,24 @@ require_once __DIR__ . "/../required.php"; -use Medoo\Medoo; - redirectIfNotLoggedIn(); $cards = $database->select('certificates', ['certid (id)', 'certcode (code)', 'amount', 'start_amount (start)', 'issued'], ['deleted[!]' => 1]); ?> - - - - - + + + + + @@ -36,7 +34,7 @@ $cards = $database->select('certificates', ['certid (id)', 'certcode (code)', 'a @@ -50,11 +48,11 @@ $cards = $database->select('certificates', ['certid (id)', 'certcode (code)', 'a - - - - - + + + + +
get('actions'); ?> get('card number'); ?> get('balance'); ?> get('start balance'); ?> get('issued'); ?>
- + get("edit"); ?> $
get('actions'); ?> get('card number'); ?> get('balance'); ?> get('start balance'); ?> get('issued'); ?>
\ No newline at end of file diff --git a/pages/customers.php b/pages/customers.php index bf03672..5ac83a9 100644 --- a/pages/customers.php +++ b/pages/customers.php @@ -13,19 +13,19 @@ $customers = $database->select('customers', ['customerid (id)', 'name', 'email', ?> - - - - - - + + + + + + @@ -35,7 +35,7 @@ $customers = $database->select('customers', ['customerid (id)', 'name', 'email', @@ -50,12 +50,12 @@ $customers = $database->select('customers', ['customerid (id)', 'name', 'email', - - - - - - + + + + + +
get('actions'); ?> get('name'); ?> get('phone'); ?> get('email'); ?> get('address'); ?> get('notes'); ?>
- + get("edit"); ?>
get('actions'); ?> get('name'); ?> get('phone'); ?> get('email'); ?> get('address'); ?> get('notes'); ?>
\ No newline at end of file diff --git a/pages/editcertificate.php b/pages/editcertificate.php index 42ee20e..47e5ab7 100644 --- a/pages/editcertificate.php +++ b/pages/editcertificate.php @@ -50,22 +50,22 @@ if (!$editing) { - htmlspecialchars($carddata['code'])]); ?> + build("editing card x", ['code' => htmlspecialchars($carddata['code'])]); ?> - + get("adding card"); ?>
- +
- +
@@ -75,7 +75,7 @@ if (!$editing) {
\ No newline at end of file diff --git a/pages/editcustomer.php b/pages/editcustomer.php index a9d2cb2..a393f45 100644 --- a/pages/editcustomer.php +++ b/pages/editcustomer.php @@ -54,7 +54,7 @@ if ($editing) {