Browse Source

Merge BusinessAppTemplate

Skylar Ittner 5 months ago
parent
commit
d8613f0baa

+ 1
- 1
README.md View File

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

+ 20
- 21
action.php View File

@@ -8,7 +8,6 @@
8 8
  * Make things happen when buttons are pressed and forms submitted.
9 9
  */
10 10
 require_once __DIR__ . "/required.php";
11
-require_once __DIR__ . "/lib/userinfo.php";
12 11
 
13 12
 if ($VARS['action'] !== "signout") {
14 13
     dieifnotloggedin();
@@ -39,7 +38,7 @@ switch ($VARS['action']) {
39 38
             global $VARS, $binstack, $error, $oktx;
40 39
 
41 40
             if (empty($VARS['items'])) {
42
-                $error = lang("no items", false);
41
+                $error = $Strings->get("no items", false);
43 42
                 return false;
44 43
             }
45 44
 
@@ -56,7 +55,7 @@ switch ($VARS['action']) {
56 55
                 $txid = $VARS['txid'];
57 56
                 $cashid = $database->get('transactions', 'cashid', ['txid' => $txid]);
58 57
                 if (!$database->has('cash_drawer', ['AND' => ['cashid' => $cashid, 'close' => null]])) {
59
-                    $error = lang("cash already closed", false);
58
+                    $error = $Strings->get("cash already closed", false);
60 59
                     return false;
61 60
                 }
62 61
                 // Nuke the payments to make room for their replacements
@@ -72,15 +71,15 @@ switch ($VARS['action']) {
72 71
             }
73 72
 
74 73
             if ($customer != "" && !$database->has('customers', ['customerid' => $customer])) {
75
-                $error = lang("invalid customer", false);
74
+                $error = $Strings->get("invalid customer", false);
76 75
                 return false;
77 76
             }
78 77
             if ($register != "" && !$database->has('registers', ['registerid' => $register])) {
79
-                $error = lang("invalid register", false);
78
+                $error = $Strings->get("invalid register", false);
80 79
                 return false;
81 80
             }
82 81
             if ($register != "" && !$database->has('cash_drawer', ['AND' => ['registerid' => $register, 'close' => null]])) {
83
-                $error = lang("cash not open", false);
82
+                $error = $Strings->get("cash not open", false);
84 83
                 return false;
85 84
             }
86 85
 
@@ -94,19 +93,19 @@ switch ($VARS['action']) {
94 93
             foreach ($items as $i) {
95 94
                 $totalcharge += $i['each'] * $i['qty'];
96 95
                 if (!$binstack->has('items', ['itemid' => $i['id']])) {
97
-                    $error = lang("invalid item", false);
96
+                    $error = $Strings->get("invalid item", false);
98 97
                     return false;
99 98
                 }
100 99
             }
101 100
             foreach ($payments as $p) {
102 101
                 if (!$database->has('payment_types', ['typename' => $p['type']])) {
103
-                    $error = lang("invalid payment type", false);
102
+                    $error = $Strings->get("invalid payment type", false);
104 103
                     return false;
105 104
                 }
106 105
                 $totalpaid += $p['amount'];
107 106
                 if ($p['type'] == "giftcard") {
108 107
                     if (!$database->has('certificates', ['AND' => ['amount[>=]' => $p['amount'], 'deleted[!]' => 1, 'certcode' => $p['code']]])) {
109
-                        $error = lang("invalid giftcard", false);
108
+                        $error = $Strings->get("invalid giftcard", false);
110 109
                         return false;
111 110
                     }
112 111
                 }
@@ -120,7 +119,7 @@ switch ($VARS['action']) {
120 119
             }
121 120
 
122 121
             if ($totalcharge > $totalpaid) {
123
-                $error = lang("insufficient payment", false);
122
+                $error = $Strings->get("insufficient payment", false);
124 123
                 return false;
125 124
             }
126 125
 
@@ -225,15 +224,15 @@ switch ($VARS['action']) {
225 224
             $cashid = null;
226 225
 
227 226
             if ($customer != "" && !$database->has('customers', ['customerid' => $customer])) {
228
-                $error = lang("invalid customer", false);
227
+                $error = $Strings->get("invalid customer", false);
229 228
                 return false;
230 229
             }
231 230
             if ($register != "" && !$database->has('registers', ['registerid' => $register])) {
232
-                $error = lang("invalid register", false);
231
+                $error = $Strings->get("invalid register", false);
233 232
                 return false;
234 233
             }
235 234
             if ($register != "" && !$database->has('cash_drawer', ['AND' => ['registerid' => $register, 'close' => null]])) {
236
-                $error = lang("cash not open", false);
235
+                $error = $Strings->get("cash not open", false);
237 236
                 return false;
238 237
             }
239 238
 
@@ -246,19 +245,19 @@ switch ($VARS['action']) {
246 245
             foreach ($items as $i) {
247 246
                 $totaldue += $i['each'] * $i['qty'];
248 247
                 if (!$binstack->has('items', ['itemid' => $i['id']])) {
249
-                    $error = lang("invalid item", false);
248
+                    $error = $Strings->get("invalid item", false);
250 249
                     return false;
251 250
                 }
252 251
             }
253 252
             foreach ($payments as $p) {
254 253
                 if (!$database->has('payment_types', ['typename' => $p['type']])) {
255
-                    $error = lang("invalid payment type", false);
254
+                    $error = $Strings->get("invalid payment type", false);
256 255
                     return false;
257 256
                 }
258 257
                 $totalrefund += $p['amount'];
259 258
                 if ($p['type'] == "giftcard") {
260 259
                     if (!$database->has('certificates', ['AND' => ['amount[>=]' => $p['amount'], 'deleted[!]' => 1, 'certcode' => $p['code']]])) {
261
-                        $error = lang("invalid giftcard", false);
260
+                        $error = $Strings->get("invalid giftcard", false);
262 261
                         return false;
263 262
                     }
264 263
                 }
@@ -319,7 +318,7 @@ switch ($VARS['action']) {
319 318
             $txid = $VARS['txid'];
320 319
             $cashid = $database->get('transactions', 'cashid', ['txid' => $txid]);
321 320
             if (!$database->has('cash_drawer', ['AND' => ['cashid' => $cashid, 'close' => null]])) {
322
-                $error = lang("cash already closed", false);
321
+                $error = $Strings->get("cash already closed", false);
323 322
             }
324 323
 
325 324
             $database->action(function ($database) {
@@ -350,7 +349,7 @@ switch ($VARS['action']) {
350 349
                 $database->delete('transactions', ['txid' => $txid, 'LIMIT' => 1]);
351 350
             });
352 351
         } else {
353
-            $error = lang("invalid parameters", false);
352
+            $error = $Strings->get("invalid parameters", false);
354 353
         }
355 354
         if (!is_null($error)) {
356 355
             exit(json_encode(["status" => "ERROR", "message" => $error]));
@@ -429,10 +428,10 @@ switch ($VARS['action']) {
429 428
                 $transactions[$i]['editable'] = false;
430 429
             }
431 430
             if (!is_null($transactions[$i]['cashierid'])) {
432
-                $cashier = getUserByID($transactions[$i]['cashierid']);
431
+                $cashier = new User($transactions[$i]['cashierid']);
433 432
                 $transactions[$i]['cashier'] = [
434
-                    "name" => $cashier['name'],
435
-                    "username" => $cashier['username']
433
+                    "name" => $cashier->getName(),
434
+                    "username" => $cashier->getUsername()
436 435
                 ];
437 436
             }
438 437
         }

+ 2
- 4
api.php View File

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

+ 8
- 8
app.php View File

@@ -28,10 +28,10 @@ header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
28 28
 header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
29 29
 header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
30 30
 header("Link: <static/css/app.css>; rel=preload; as=style", false);
31
-header("Link: <static/css/fa-svg-with-js.css>; rel=preload; as=style", false);
31
+header("Link: <static/css/svg-with-js.min.css>; rel=preload; as=style", false);
32 32
 header("Link: <static/js/fontawesome-all.min.js>; rel=preload; as=script", false);
33 33
 header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
34
-header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
34
+header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", false);
35 35
 ?>
36 36
 <!DOCTYPE html>
37 37
 <html>
@@ -47,7 +47,7 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
47 47
         <link href="static/css/bootstrap.min.css" rel="stylesheet">
48 48
         <link href="static/css/material-color/material-color.min.css" rel="stylesheet">
49 49
         <link href="static/css/app.css" rel="stylesheet">
50
-        <link href="static/css/fa-svg-with-js.css" rel="stylesheet">
50
+        <link href="static/css/svg-with-js.min.css" rel="stylesheet">
51 51
         <script nonce="<?php echo $SECURE_NONCE; ?>">
52 52
             FontAwesomeConfig = {autoAddCss: false}
53 53
         </script>
@@ -69,9 +69,9 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
69 69
         if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
70 70
             // optional string generation argument
71 71
             if (!isset($_GET['arg']) || is_empty($_GET['arg'])) {
72
-                $alertmsg = lang(MESSAGES[$_GET['msg']]['string'], false);
72
+                $alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
73 73
             } else {
74
-                $alertmsg = lang2(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
74
+                $alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
75 75
             }
76 76
             $alerttype = MESSAGES[$_GET['msg']]['type'];
77 77
             $alerticon = "square-o";
@@ -146,7 +146,7 @@ END;
146 146
                                         if (isset($pg['icon'])) {
147 147
                                             ?><i class="<?php echo $pg['icon']; ?> fa-fw"></i> <?php
148 148
                                         }
149
-                                        lang($pg['title']);
149
+                                        $Strings->get($pg['title']);
150 150
                                         ?>
151 151
                                     </a>
152 152
                                 </span>
@@ -163,7 +163,7 @@ END;
163 163
                     </span>
164 164
                     <span class="nav-item mr-auto py-<?php echo $navbar_breakpoint; ?>-0">
165 165
                         <a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="action.php?action=signout">
166
-                            <i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php lang("sign out") ?></span>
166
+                            <i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php $Strings->get("sign out") ?></span>
167 167
                         </a>
168 168
                     </span>
169 169
                 </div>
@@ -182,7 +182,7 @@ END;
182 182
             </div>
183 183
         </div>
184 184
         <script src="static/js/jquery-3.3.1.min.js"></script>
185
-        <script src="static/js/bootstrap.min.js"></script>
185
+        <script src="static/js/bootstrap.bundle.min.js"></script>
186 186
         <script src="static/js/app.js"></script>
187 187
         <?php
188 188
 // custom page scripts

+ 0
- 419
composer.lock View File

@@ -1,419 +0,0 @@
1
-{
2
-    "_readme": [
3
-        "This file locks the dependencies of your project to a known state",
4
-        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5
-        "This file is @generated automatically"
6
-    ],
7
-    "hash": "0e5db12408080dd084cad072b8cfd599",
8
-    "content-hash": "348006dfc1d25121fcc3b4cb32bc3369",
9
-    "packages": [
10
-        {
11
-            "name": "catfan/medoo",
12
-            "version": "v1.5.3",
13
-            "source": {
14
-                "type": "git",
15
-                "url": "https://github.com/catfan/Medoo.git",
16
-                "reference": "1aa25a4001e0cfb739ba2996f00f4a3d2a7fdf07"
17
-            },
18
-            "dist": {
19
-                "type": "zip",
20
-                "url": "https://api.github.com/repos/catfan/Medoo/zipball/1aa25a4001e0cfb739ba2996f00f4a3d2a7fdf07",
21
-                "reference": "1aa25a4001e0cfb739ba2996f00f4a3d2a7fdf07",
22
-                "shasum": ""
23
-            },
24
-            "require": {
25
-                "ext-pdo": "*",
26
-                "php": ">=5.4"
27
-            },
28
-            "suggest": {
29
-                "ext-pdo_dblib": "For MSSQL or Sybase database on Linux/UNIX platform",
30
-                "ext-pdo_mysql": "For MySQL or MariaDB database",
31
-                "ext-pdo_oci": "For Oracle database",
32
-                "ext-pdo_oci8": "For Oracle version 8 database",
33
-                "ext-pdo_pqsql": "For PostgreSQL database",
34
-                "ext-pdo_sqlite": "For SQLite database",
35
-                "ext-pdo_sqlsrv": "For MSSQL database"
36
-            },
37
-            "type": "framework",
38
-            "autoload": {
39
-                "psr-4": {
40
-                    "Medoo\\": "/src"
41
-                }
42
-            },
43
-            "notification-url": "https://packagist.org/downloads/",
44
-            "license": [
45
-                "MIT"
46
-            ],
47
-            "authors": [
48
-                {
49
-                    "name": "Angel Lai",
50
-                    "email": "angel@catfan.me"
51
-                }
52
-            ],
53
-            "description": "The lightest PHP database framework to accelerate development",
54
-            "homepage": "https://medoo.in",
55
-            "keywords": [
56
-                "database",
57
-                "lightweight",
58
-                "mariadb",
59
-                "mssql",
60
-                "mysql",
61
-                "oracle",
62
-                "php framework",
63
-                "postgresql",
64
-                "sql",
65
-                "sqlite"
66
-            ],
67
-            "time": "2017-12-25 17:02:41"
68
-        },
69
-        {
70
-            "name": "guzzlehttp/guzzle",
71
-            "version": "6.3.0",
72
-            "source": {
73
-                "type": "git",
74
-                "url": "https://github.com/guzzle/guzzle.git",
75
-                "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699"
76
-            },
77
-            "dist": {
78
-                "type": "zip",
79
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699",
80
-                "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699",
81
-                "shasum": ""
82
-            },
83
-            "require": {
84
-                "guzzlehttp/promises": "^1.0",
85
-                "guzzlehttp/psr7": "^1.4",
86
-                "php": ">=5.5"
87
-            },
88
-            "require-dev": {
89
-                "ext-curl": "*",
90
-                "phpunit/phpunit": "^4.0 || ^5.0",
91
-                "psr/log": "^1.0"
92
-            },
93
-            "suggest": {
94
-                "psr/log": "Required for using the Log middleware"
95
-            },
96
-            "type": "library",
97
-            "extra": {
98
-                "branch-alias": {
99
-                    "dev-master": "6.2-dev"
100
-                }
101
-            },
102
-            "autoload": {
103
-                "files": [
104
-                    "src/functions_include.php"
105
-                ],
106
-                "psr-4": {
107
-                    "GuzzleHttp\\": "src/"
108
-                }
109
-            },
110
-            "notification-url": "https://packagist.org/downloads/",
111
-            "license": [
112
-                "MIT"
113
-            ],
114
-            "authors": [
115
-                {
116
-                    "name": "Michael Dowling",
117
-                    "email": "mtdowling@gmail.com",
118
-                    "homepage": "https://github.com/mtdowling"
119
-                }
120
-            ],
121
-            "description": "Guzzle is a PHP HTTP client library",
122
-            "homepage": "http://guzzlephp.org/",
123
-            "keywords": [
124
-                "client",
125
-                "curl",
126
-                "framework",
127
-                "http",
128
-                "http client",
129
-                "rest",
130
-                "web service"
131
-            ],
132
-            "time": "2017-06-22 18:50:49"
133
-        },
134
-        {
135
-            "name": "guzzlehttp/promises",
136
-            "version": "v1.3.1",
137
-            "source": {
138
-                "type": "git",
139
-                "url": "https://github.com/guzzle/promises.git",
140
-                "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
141
-            },
142
-            "dist": {
143
-                "type": "zip",
144
-                "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
145
-                "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
146
-                "shasum": ""
147
-            },
148
-            "require": {
149
-                "php": ">=5.5.0"
150
-            },
151
-            "require-dev": {
152
-                "phpunit/phpunit": "^4.0"
153
-            },
154
-            "type": "library",
155
-            "extra": {
156
-                "branch-alias": {
157
-                    "dev-master": "1.4-dev"
158
-                }
159
-            },
160
-            "autoload": {
161
-                "psr-4": {
162
-                    "GuzzleHttp\\Promise\\": "src/"
163
-                },
164
-                "files": [
165
-                    "src/functions_include.php"
166
-                ]
167
-            },
168
-            "notification-url": "https://packagist.org/downloads/",
169
-            "license": [
170
-                "MIT"
171
-            ],
172
-            "authors": [
173
-                {
174
-                    "name": "Michael Dowling",
175
-                    "email": "mtdowling@gmail.com",
176
-                    "homepage": "https://github.com/mtdowling"
177
-                }
178
-            ],
179
-            "description": "Guzzle promises library",
180
-            "keywords": [
181
-                "promise"
182
-            ],
183
-            "time": "2016-12-20 10:07:11"
184
-        },
185
-        {
186
-            "name": "guzzlehttp/psr7",
187
-            "version": "1.4.2",
188
-            "source": {
189
-                "type": "git",
190
-                "url": "https://github.com/guzzle/psr7.git",
191
-                "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
192
-            },
193
-            "dist": {
194
-                "type": "zip",
195
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
196
-                "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
197
-                "shasum": ""
198
-            },
199
-            "require": {
200
-                "php": ">=5.4.0",
201
-                "psr/http-message": "~1.0"
202
-            },
203
-            "provide": {
204
-                "psr/http-message-implementation": "1.0"
205
-            },
206
-            "require-dev": {
207
-                "phpunit/phpunit": "~4.0"
208
-            },
209
-            "type": "library",
210
-            "extra": {
211
-                "branch-alias": {
212
-                    "dev-master": "1.4-dev"
213
-                }
214
-            },
215
-            "autoload": {
216
-                "psr-4": {
217
-                    "GuzzleHttp\\Psr7\\": "src/"
218
-                },
219
-                "files": [
220
-                    "src/functions_include.php"
221
-                ]
222
-            },
223
-            "notification-url": "https://packagist.org/downloads/",
224
-            "license": [
225
-                "MIT"
226
-            ],
227
-            "authors": [
228
-                {
229
-                    "name": "Michael Dowling",
230
-                    "email": "mtdowling@gmail.com",
231
-                    "homepage": "https://github.com/mtdowling"
232
-                },
233
-                {
234
-                    "name": "Tobias Schultze",
235
-                    "homepage": "https://github.com/Tobion"
236
-                }
237
-            ],
238
-            "description": "PSR-7 message implementation that also provides common utility methods",
239
-            "keywords": [
240
-                "http",
241
-                "message",
242
-                "request",
243
-                "response",
244
-                "stream",
245
-                "uri",
246
-                "url"
247
-            ],
248
-            "time": "2017-03-20 17:10:46"
249
-        },
250
-        {
251
-            "name": "lapinator/ods-php-generator",
252
-            "version": "v0.0.3",
253
-            "source": {
254
-                "type": "git",
255
-                "url": "https://github.com/Lapinator/odsPhpGenerator.git",
256
-                "reference": "575314c003c2ec3032813bedcc1d27032b7b7ab2"
257
-            },
258
-            "dist": {
259
-                "type": "zip",
260
-                "url": "https://api.github.com/repos/Lapinator/odsPhpGenerator/zipball/575314c003c2ec3032813bedcc1d27032b7b7ab2",
261
-                "reference": "575314c003c2ec3032813bedcc1d27032b7b7ab2",
262
-                "shasum": ""
263
-            },
264
-            "require": {
265
-                "php": ">=5.3"
266
-            },
267
-            "type": "library",
268
-            "autoload": {
269
-                "classmap": [
270
-                    "src/"
271
-                ]
272
-            },
273
-            "notification-url": "https://packagist.org/downloads/",
274
-            "license": [
275
-                "LGPL-3.0"
276
-            ],
277
-            "authors": [
278
-                {
279
-                    "name": "Laurent VUIBERT",
280
-                    "email": "lapinator@gmx.fr",
281
-                    "homepage": "http://lapinator.net",
282
-                    "role": "Developer"
283
-                }
284
-            ],
285
-            "description": "Open Document Spreadsheet (.ods) generator ",
286
-            "homepage": "https://odsphpgenerator.lapinator.net/",
287
-            "keywords": [
288
-                "LibreOffice",
289
-                "ods"
290
-            ],
291
-            "time": "2016-04-14 21:51:27"
292
-        },
293
-        {
294
-            "name": "league/csv",
295
-            "version": "9.1.4",
296
-            "source": {
297
-                "type": "git",
298
-                "url": "https://github.com/thephpleague/csv.git",
299
-                "reference": "9c8ad06fb5d747c149875beb6133566c00eaa481"
300
-            },
301
-            "dist": {
302
-                "type": "zip",
303
-                "url": "https://api.github.com/repos/thephpleague/csv/zipball/9c8ad06fb5d747c149875beb6133566c00eaa481",
304
-                "reference": "9c8ad06fb5d747c149875beb6133566c00eaa481",
305
-                "shasum": ""
306
-            },
307
-            "require": {
308
-                "ext-mbstring": "*",
309
-                "php": ">=7.0.10"
310
-            },
311
-            "require-dev": {
312
-                "ext-curl": "*",
313
-                "friendsofphp/php-cs-fixer": "^2.0",
314
-                "phpstan/phpstan": "^0.9.2",
315
-                "phpstan/phpstan-phpunit": "^0.9.4",
316
-                "phpstan/phpstan-strict-rules": "^0.9.0",
317
-                "phpunit/phpunit": "^6.0"
318
-            },
319
-            "suggest": {
320
-                "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters"
321
-            },
322
-            "type": "library",
323
-            "extra": {
324
-                "branch-alias": {
325
-                    "dev-master": "9.x-dev"
326
-                }
327
-            },
328
-            "autoload": {
329
-                "psr-4": {
330
-                    "League\\Csv\\": "src"
331
-                },
332
-                "files": [
333
-                    "src/functions_include.php"
334
-                ]
335
-            },
336
-            "notification-url": "https://packagist.org/downloads/",
337
-            "license": [
338
-                "MIT"
339
-            ],
340
-            "authors": [
341
-                {
342
-                    "name": "Ignace Nyamagana Butera",
343
-                    "email": "nyamsprod@gmail.com",
344
-                    "homepage": "https://github.com/nyamsprod/",
345
-                    "role": "Developer"
346
-                }
347
-            ],
348
-            "description": "Csv data manipulation made easy in PHP",
349
-            "homepage": "http://csv.thephpleague.com",
350
-            "keywords": [
351
-                "csv",
352
-                "export",
353
-                "filter",
354
-                "import",
355
-                "read",
356
-                "write"
357
-            ],
358
-            "time": "2018-05-01 18:32:48"
359
-        },
360
-        {
361
-            "name": "psr/http-message",
362
-            "version": "1.0.1",
363
-            "source": {
364
-                "type": "git",
365
-                "url": "https://github.com/php-fig/http-message.git",
366
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
367
-            },
368
-            "dist": {
369
-                "type": "zip",
370
-                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
371
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
372
-                "shasum": ""
373
-            },
374
-            "require": {
375
-                "php": ">=5.3.0"
376
-            },
377
-            "type": "library",
378
-            "extra": {
379
-                "branch-alias": {
380
-                    "dev-master": "1.0.x-dev"
381
-                }
382
-            },
383
-            "autoload": {
384
-                "psr-4": {
385
-                    "Psr\\Http\\Message\\": "src/"
386
-                }
387
-            },
388
-            "notification-url": "https://packagist.org/downloads/",
389
-            "license": [
390
-                "MIT"
391
-            ],
392
-            "authors": [
393
-                {
394
-                    "name": "PHP-FIG",
395
-                    "homepage": "http://www.php-fig.org/"
396
-                }
397
-            ],
398
-            "description": "Common interface for HTTP messages",
399
-            "homepage": "https://github.com/php-fig/http-message",
400
-            "keywords": [
401
-                "http",
402
-                "http-message",
403
-                "psr",
404
-                "psr-7",
405
-                "request",
406
-                "response"
407
-            ],
408
-            "time": "2016-08-06 14:39:51"
409
-        }
410
-    ],
411
-    "packages-dev": [],
412
-    "aliases": [],
413
-    "minimum-stability": "stable",
414
-    "stability-flags": [],
415
-    "prefer-stable": false,
416
-    "prefer-lowest": false,
417
-    "platform": [],
418
-    "platform-dev": []
419
-}

+ 58
- 47
index.php View File

@@ -5,87 +5,98 @@
5 5
 
6 6
 require_once __DIR__ . "/required.php";
7 7
 
8
-require_once __DIR__ . "/lib/login.php";
9
-
10 8
 // if we're logged in, we don't need to be here.
11 9
 if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && !isset($_GET['permissionerror'])) {
12 10
     header('Location: app.php');
13 11
 }
14 12
 
15 13
 if (isset($_GET['permissionerror'])) {
16
-    $alert = lang("no access permission", false);
14
+    $alert = $Strings->get("no access permission", false);
17 15
 }
18 16
 
19 17
 /* Authenticate user */
20 18
 $userpass_ok = false;
21 19
 $multiauth = false;
22
-if (checkLoginServer()) {
23
-    if (!empty($VARS['progress']) && $VARS['progress'] == "1") {
24
-        if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && verifyCaptcheck($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
25
-            $errmsg = "";
26
-            if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) {
27
-                switch (get_account_status($VARS['username'])) {
20
+if (Login::checkLoginServer()) {
21
+    if (empty($VARS['progress'])) {
22
+        // Easy way to remove "undefined" warnings.
23
+    } else if ($VARS['progress'] == "1") {
24
+        if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
25
+            $autherror = "";
26
+            $user = User::byUsername($VARS['username']);
27
+            if ($user->exists()) {
28
+                $status = $user->getStatus()->getString();
29
+                switch ($status) {
28 30
                     case "LOCKED_OR_DISABLED":
29
-                        $alert = lang("account locked", false);
31
+                        $alert = $Strings->get("account locked", false);
30 32
                         break;
31 33
                     case "TERMINATED":
32
-                        $alert = lang("account terminated", false);
34
+                        $alert = $Strings->get("account terminated", false);
33 35
                         break;
34 36
                     case "CHANGE_PASSWORD":
35
-                        $alert = lang("password expired", false);
37
+                        $alert = $Strings->get("password expired", false);
38
+                        break;
36 39
                     case "NORMAL":
37
-                        $userpass_ok = true;
40
+                        $username_ok = true;
38 41
                         break;
39 42
                     case "ALERT_ON_ACCESS":
40
-                        sendLoginAlertEmail($VARS['username']);
41
-                        $userpass_ok = true;
43
+                        $mail_resp = $user->sendAlertEmail();
44
+                        if (DEBUG) {
45
+                            var_dump($mail_resp);
46
+                        }
47
+                        $username_ok = true;
48
+                        break;
49
+                    default:
50
+                        if (!is_empty($error)) {
51
+                            $alert = $error;
52
+                        } else {
53
+                            $alert = $Strings->get("login error", false);
54
+                        }
42 55
                         break;
43 56
                 }
44
-                if ($userpass_ok) {
45
-                    $_SESSION['passok'] = true; // stop logins using only username and authcode
46
-                    if (userHasTOTP($VARS['username'])) {
47
-                        $multiauth = true;
57
+                if ($username_ok) {
58
+                    if ($user->checkPassword($VARS['password'])) {
59
+                        $_SESSION['passok'] = true; // stop logins using only username and authcode
60
+                        if ($user->has2fa()) {
61
+                            $multiauth = true;
62
+                        } else {
63
+                            Session::start($user);
64
+                            header('Location: app.php');
65
+                            die("Logged in, go to app.php");
66
+                        }
48 67
                     } else {
49
-                        doLoginUser($VARS['username'], $VARS['password']);
50
-                        header('Location: app.php');
51
-                        die("Logged in, go to app.php");
68
+                        $alert = $Strings->get("login incorrect", false);
52 69
                     }
53 70
                 }
54
-            } else {
55
-                if (!is_empty($errmsg)) {
56
-                    $alert = lang2("login server error", ['arg' => $errmsg], false);
57
-                } else {
58
-                    $alert = lang("login incorrect", false);
59
-                }
71
+            } else { // User does not exist anywhere
72
+                $alert = $Strings->get("login incorrect", false);
60 73
             }
61 74
         } else {
62
-            $alert = lang("captcha error", false);
75
+            $alert = $Strings->get("captcha error", false);
63 76
         }
64
-    } else if (!empty($VARS['progress']) && $VARS['progress'] == "2") {
77
+    } else if ($VARS['progress'] == "2") {
78
+        $user = User::byUsername($VARS['username']);
65 79
         if ($_SESSION['passok'] !== true) {
66 80
             // stop logins using only username and authcode
67 81
             sendError("Password integrity check failed!");
68 82
         }
69
-        if (verifyTOTP($VARS['username'], $VARS['authcode'])) {
70
-            if (doLoginUser($VARS['username'])) {
71
-                header('Location: app.php');
72
-                die("Logged in, go to app.php");
73
-            } else {
74
-                $alert = lang("login server user data error", false);
75
-            }
83
+        if ($user->check2fa($VARS['authcode'])) {
84
+            Session::start($user);
85
+            header('Location: app.php');
86
+            die("Logged in, go to app.php");
76 87
         } else {
77
-            $alert = lang("2fa incorrect", false);
88
+            $alert = $Strings->get("2fa incorrect", false);
78 89
         }
79 90
     }
80 91
 } else {
81
-    $alert = lang("login server unavailable", false);
92
+    $alert = $Strings->get("login server unavailable", false);
82 93
 }
83 94
 header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
84 95
 header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
85 96
 header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
86 97
 header("Link: <static/css/index.css>; rel=preload; as=style", false);
87 98
 header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
88
-header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
99
+header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", false);
89 100
 ?>
90 101
 <!DOCTYPE html>
91 102
 <html>
@@ -114,7 +125,7 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
114 125
         <div class="row justify-content-center">
115 126
             <div class="card col-11 col-xs-11 col-sm-8 col-md-6 col-lg-4">
116 127
                 <div class="card-body">
117
-                    <h5 class="card-title"><?php lang("sign in"); ?></h5>
128
+                    <h5 class="card-title"><?php $Strings->get("sign in"); ?></h5>
118 129
                     <form action="" method="POST">
119 130
                         <?php
120 131
                         if (!empty($alert)) {
@@ -127,8 +138,8 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
127 138
 
128 139
                         if ($multiauth != true) {
129 140
                             ?>
130
-                            <input type="text" class="form-control" name="username" placeholder="<?php lang("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
131
-                            <input type="password" class="form-control" name="password" placeholder="<?php lang("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
141
+                            <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 />
142
+                            <input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
132 143
                             <?php if (CAPTCHA_ENABLED) { ?>
133 144
                                 <div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
134 145
                                 <br />
@@ -138,16 +149,16 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
138 149
                         } else if ($multiauth) {
139 150
                             ?>
140 151
                             <div class="alert alert-info">
141
-                                <?php lang("2fa prompt"); ?>
152
+                                <?php $Strings->get("2fa prompt"); ?>
142 153
                             </div>
143
-                            <input type="text" class="form-control" name="authcode" placeholder="<?php lang("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
154
+                            <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 />
144 155
                             <input type="hidden" name="progress" value="2" />
145 156
                             <input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
146 157
                             <?php
147 158
                         }
148 159
                         ?>
149 160
                         <button type="submit" class="btn btn-primary">
150
-                            <?php lang("continue"); ?>
161
+                            <?php $Strings->get("continue"); ?>
151 162
                         </button>
152 163
                     </form>
153 164
                 </div>
@@ -159,6 +170,6 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
159 170
         </div>
160 171
     </div>
161 172
     <script src="static/js/jquery-3.3.1.min.js"></script>
162
-    <script src="static/js/bootstrap.min.js"></script>
173
+    <script src="static/js/bootstrap.bundle.min.js"></script>
163 174
 </body>
164 175
 </html>

+ 0
- 144
lang/en_us.php View File

@@ -1,144 +0,0 @@
1
-<?php
2
-
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
-
7
-define("STRINGS", [
8
-    "sign in" => "Sign In",
9
-    "username" => "Username",
10
-    "password" => "Password",
11
-    "continue" => "Continue",
12
-    "authcode" => "Authentication code",
13
-    "2fa prompt" => "Enter the six-digit code from your mobile authenticator app.",
14
-    "2fa incorrect" => "Authentication code incorrect.",
15
-    "login incorrect" => "Login incorrect.",
16
-    "login server unavailable" => "Login server unavailable.  Try again later or contact technical support.",
17
-    "account locked" => "This account has been disabled. Contact technical support.",
18
-    "password expired" => "You must change your password before continuing.",
19
-    "account terminated" => "Account terminated.  Access denied.",
20
-    "account state error" => "Your account state is not stable.  Log out, restart your browser, and try again.",
21
-    "welcome user" => "Welcome, {user}!",
22
-    "sign out" => "Sign out",
23
-    "settings" => "Settings",
24
-    "options" => "Options",
25
-    "404 error" => "404 Error",
26
-    "page not found" => "Page not found.",
27
-    "invalid parameters" => "Invalid request parameters.",
28
-    "login server error" => "The login server returned an error: {arg}",
29
-    "login server user data error" => "The login server refused to provide account information.  Try again or contact technical support.",
30
-    "captcha error" => "There was a problem with the CAPTCHA (robot test).  Try again.",
31
-    "no access permission" => "You do not have permission to access this system.",
32
-    "home" => "Home",
33
-    "point of sale" => "Point of Sale",
34
-    "barcode" => "Barcode",
35
-    "barcode or search" => "Barcode or Search",
36
-    "cash" => "Cash",
37
-    "check" => "Check",
38
-    "card" => "Card",
39
-    "crypto" => "Crypto",
40
-    "gift card" => "Gift Card",
41
-    "free" => "Free",
42
-    "paid" => "Paid",
43
-    "owed" => "Owed",
44
-    "change" => "Change",
45
-    "enter payment" => "Enter Payment",
46
-    "receipt" => "Receipt",
47
-    "close" => "Close",
48
-    "print" => "Print",
49
-    "customer" => "Customer",
50
-    "customer search" => "Search customers",
51
-    "new sale" => "New Sale",
52
-    "customers" => "Customers",
53
-    "actions" => "Actions",
54
-    "name" => "Name",
55
-    "phone" => "Phone",
56
-    "email" => "Email",
57
-    "address" => "Address",
58
-    "notes" => "Notes",
59
-    "edit" => "Edit",
60
-    "new customer" => "New Customer",
61
-    "adding customer" => "Adding Customer",
62
-    "editing customer" => "Editing {name}",
63
-    "save" => "Save",
64
-    "customer saved" => "Customer saved.",
65
-    "invalid customer id" => "Invalid customer ID",
66
-    "customer pricing" => "Customer Pricing",
67
-    "item" => "Item",
68
-    "cost" => "Cost",
69
-    "normal price" => "Normal Price",
70
-    "customer price" => "Customer Price",
71
-    "add price" => "Add Price",
72
-    "add customer price" => "Add Customer Price",
73
-    "delete" => "Delete",
74
-    "cancel" => "Cancel",
75
-    "price" => "Price",
76
-    "finish" => "Finish",
77
-    "registers" => "Registers",
78
-    "add register" => "Add Register",
79
-    "balance" => "Balance",
80
-    "opened" => "Opened",
81
-    "closed" => "Closed",
82
-    "never" => "Never",
83
-    "last opened" => "Last Opened",
84
-    "still open" => "Still Open",
85
-    "open" => "Open",
86
-    "no cash" => "No cash",
87
-    "choose register" => "Choose a cash register",
88
-    "cash not open" => "Cash not open.  Go to Registers to open it.",
89
-    "cash opened" => "Cash opened.",
90
-    "cash closed" => "Cash closed.",
91
-    "register set" => "Register set.",
92
-    "change register" => "Change register",
93
-    "reports" => "Reports",
94
-    "report type" => "Report Type",
95
-    "format" => "Format",
96
-    "filter" => "Filter",
97
-    "generate report" => "Generate Report",
98
-    "cashflow" => "Cash Flow",
99
-    "z report" => "Z Report",
100
-    "csv file" => "CSV text file",
101
-    "ods file" => "ODS spreadsheet",
102
-    "html file" => "HTML web page",
103
-    "register" => "Register",
104
-    "all" => "All",
105
-    "date range" => "Date Range",
106
-    "start" => "Start",
107
-    "end" => "End",
108
-    "grid view" => "Grid view",
109
-    "edit register" => "Edit Register",
110
-    "editing register" => "Editing register {name}",
111
-    "adding register" => "Adding register",
112
-    "register saved" => "Register saved.",
113
-    "register name taken" => "Register name already taken.  Use a different name.",
114
-    "no open registers" => "No open cash registers.  Go to the Registers page to open one.",
115
-    "register management" => "Register Management",
116
-    "manage register" => "Manage register",
117
-    "manage" => "Manage",
118
-    "x report" => "X Report",
119
-    "z report" => "Z Report",
120
-    "pick cash" => "Choose",
121
-    "cash already closed" => "Cash already closed, cannot edit this transaction.  Process a return instead.",
122
-    "update" => "Update",
123
-    "transaction search" => "Search transactions",
124
-    "return" => "Return",
125
-    "enter refund" => "Enter Refund",
126
-    "refund" => "Refund",
127
-    "cannot edit return transaction" => "Cannot edit a return transaction.",
128
-    "gift cards" => "Gift Cards",
129
-    "add card" => "Add Card",
130
-    "card number" => "Card Number",
131
-    "start balance" => "Starting Balance",
132
-    "issued" => "Issued",
133
-    "editing card x" => "Editing card {code}",
134
-    "adding card" => "Adding card",
135
-    "card added" => "Gift card added.",
136
-    "card saved" => "Gift card updated.",
137
-    "card x added" => "Gift card #{arg} added.",
138
-    "card x saved" => "Gift card #{arg} updated.",
139
-    "open drawer" => "Open Drawer",
140
-    "no items" => "No items in transaction.",
141
-    "delete transaction" => "Delete transaction",
142
-    "transaction discount" => "Transaction discount",
143
-    "Online Sales" => "Online Sales",
144
-]);

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

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

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

@@ -0,0 +1,112 @@
1
+{
2
+    "point of sale": "Point of Sale",
3
+    "barcode": "Barcode",
4
+    "barcode or search": "Barcode or Search",
5
+    "cash": "Cash",
6
+    "check": "Check",
7
+    "card": "Card",
8
+    "crypto": "Crypto",
9
+    "gift card": "Gift Card",
10
+    "free": "Free",
11
+    "paid": "Paid",
12
+    "owed": "Owed",
13
+    "change": "Change",
14
+    "enter payment": "Enter Payment",
15
+    "receipt": "Receipt",
16
+    "close": "Close",
17
+    "print": "Print",
18
+    "customer": "Customer",
19
+    "customer search": "Search customers",
20
+    "new sale": "New Sale",
21
+    "customers": "Customers",
22
+    "actions": "Actions",
23
+    "name": "Name",
24
+    "phone": "Phone",
25
+    "email": "Email",
26
+    "address": "Address",
27
+    "notes": "Notes",
28
+    "edit": "Edit",
29
+    "new customer": "New Customer",
30
+    "adding customer": "Adding Customer",
31
+    "editing customer": "Editing {name}",
32
+    "save": "Save",
33
+    "customer saved": "Customer saved.",
34
+    "invalid customer id": "Invalid customer ID",
35
+    "customer pricing": "Customer Pricing",
36
+    "item": "Item",
37
+    "cost": "Cost",
38
+    "normal price": "Normal Price",
39
+    "customer price": "Customer Price",
40
+    "add price": "Add Price",
41
+    "add customer price": "Add Customer Price",
42
+    "delete": "Delete",
43
+    "cancel": "Cancel",
44
+    "price": "Price",
45
+    "finish": "Finish",
46
+    "registers": "Registers",
47
+    "add register": "Add Register",
48
+    "balance": "Balance",
49
+    "opened": "Opened",
50
+    "closed": "Closed",
51
+    "never": "Never",
52
+    "last opened": "Last Opened",
53
+    "still open": "Still Open",
54
+    "open": "Open",
55
+    "no cash": "No cash",
56
+    "choose register": "Choose a cash register",
57
+    "cash not open": "Cash not open.  Go to Registers to open it.",
58
+    "cash opened": "Cash opened.",
59
+    "cash closed": "Cash closed.",
60
+    "register set": "Register set.",
61
+    "change register": "Change register",
62
+    "reports": "Reports",
63
+    "report type": "Report Type",
64
+    "format": "Format",
65
+    "filter": "Filter",
66
+    "generate report": "Generate Report",
67
+    "cashflow": "Cash Flow",
68
+    "z report": "Z Report",
69
+    "csv file": "CSV text file",
70
+    "ods file": "ODS spreadsheet",
71
+    "html file": "HTML web page",
72
+    "register": "Register",
73
+    "all": "All",
74
+    "date range": "Date Range",
75
+    "start": "Start",
76
+    "end": "End",
77
+    "grid view": "Grid view",
78
+    "edit register": "Edit Register",
79
+    "editing register": "Editing register {name}",
80
+    "adding register": "Adding register",
81
+    "register saved": "Register saved.",
82
+    "register name taken": "Register name already taken.  Use a different name.",
83
+    "no open registers": "No open cash registers.  Go to the Registers page to open one.",
84
+    "register management": "Register Management",
85
+    "manage register": "Manage register",
86
+    "manage": "Manage",
87
+    "x report": "X Report",
88
+    "pick cash": "Choose",
89
+    "cash already closed": "Cash already closed, cannot edit this transaction.  Process a return instead.",
90
+    "update": "Update",
91
+    "transaction search": "Search transactions",
92
+    "return": "Return",
93
+    "enter refund": "Enter Refund",
94
+    "refund": "Refund",
95
+    "cannot edit return transaction": "Cannot edit a return transaction.",
96
+    "gift cards": "Gift Cards",
97
+    "add card": "Add Card",
98
+    "card number": "Card Number",
99
+    "start balance": "Starting Balance",
100
+    "issued": "Issued",
101
+    "editing card x": "Editing card {code}",
102
+    "adding card": "Adding card",
103
+    "card added": "Gift card added.",
104
+    "card saved": "Gift card updated.",
105
+    "card x added": "Gift card #{arg} added.",
106
+    "card x saved": "Gift card #{arg} updated.",
107
+    "open drawer": "Open Drawer",
108
+    "no items": "No items in transaction.",
109
+    "delete transaction": "Delete transaction",
110
+    "transaction discount": "Transaction discount",
111
+    "Online Sales": "Online Sales"
112
+}

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

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

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


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

@@ -0,0 +1,13 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class IncorrectPasswordException extends Exception {
10
+    public function __construct(string $message = "Incorrect password.", int $code = 0, \Throwable $previous = null) {
11
+        parent::__construct($message, $code, $previous);
12
+    }
13
+}

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

@@ -6,8 +6,6 @@
6 6
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 7
  */
8 8
 
9
-require_once __DIR__ . "/receipts.php";
10
-
11 9
 class GenerateReceipt {
12 10
 
13 11
     const RECEIPT_TYPE_TRANSACTION = 1;
@@ -51,7 +49,7 @@ class GenerateReceipt {
51 49
             if ($p['amount'] < 0) {
52 50
                 continue;
53 51
             }
54
-            $paymentlines[] = new ReceiptLine(lang($p['text'], false), "", '$' . number_format($p['amount'] * 1.0, 2));
52
+            $paymentlines[] = new ReceiptLine($Strings->get($p['text'], false), "", '$' . number_format($p['amount'] * 1.0, 2));
55 53
             $paid += $p['amount'] * 1.0;
56 54
         }
57 55
         $change = $paid - $total;
@@ -124,7 +122,7 @@ class GenerateReceipt {
124 122
             'txid' => $txid
125 123
         ]);
126 124
         foreach ($payments as $p) {
127
-            $paymentlines[] = new ReceiptLine(lang($p['text'], false), "", '$' . number_format($p['amount'] * -1.0, 2));
125
+            $paymentlines[] = new ReceiptLine($Strings->get($p['text'], false), "", '$' . number_format($p['amount'] * -1.0, 2));
128 126
             $paid += $p['amount'] * 1.0;
129 127
         }
130 128
 
@@ -191,7 +189,7 @@ class GenerateReceipt {
191 189
             $balance[$p['type']] += $p['amount'];
192 190
         }
193 191
 
194
-        $receipt->appendHeader(new ReceiptLine(lang("x report", false), "", "", ReceiptLine::LINEFORMAT_BOLD | ReceiptLine::LINEFORMAT_CENTER));
192
+        $receipt->appendHeader(new ReceiptLine($Strings->get("x report", false), "", "", ReceiptLine::LINEFORMAT_BOLD | ReceiptLine::LINEFORMAT_CENTER));
195 193
 
196 194
         $receipt->appendLine(new ReceiptLine("Printed:", "", date(DATETIME_FORMAT)));
197 195
         $receipt->appendLine(new ReceiptLine("Register:", "", $registername));
@@ -209,7 +207,7 @@ class GenerateReceipt {
209 207
         $receipt->appendLine(new ReceiptLine("Sales", "", "", ReceiptLine::LINEFORMAT_CENTER));
210 208
         $receipt->appendBreak();
211 209
         foreach ($paymenttypes as $t) {
212
-            $receipt->appendLine(new ReceiptLine(lang($t['text'], false) . ":", "", '$' . number_format($balance[$t['type']], 2)));
210
+            $receipt->appendLine(new ReceiptLine($Strings->get($t['text'], false) . ":", "", '$' . number_format($balance[$t['type']], 2)));
213 211
         }
214 212
 
215 213
         $receipt->appendBlank();
@@ -246,7 +244,7 @@ class GenerateReceipt {
246 244
             $balance[$p['type']] += $p['amount'];
247 245
         }
248 246
 
249
-        $receipt->appendHeader(new ReceiptLine(lang("z report", false), "", "", ReceiptLine::LINEFORMAT_BOLD | ReceiptLine::LINEFORMAT_CENTER));
247
+        $receipt->appendHeader(new ReceiptLine($Strings->get("z report", false), "", "", ReceiptLine::LINEFORMAT_BOLD | ReceiptLine::LINEFORMAT_CENTER));
250 248
 
251 249
         $receipt->appendLine(new ReceiptLine("Printed:", "", date(DATETIME_FORMAT)));
252 250
         $receipt->appendLine(new ReceiptLine("Register:", "", $registername));
@@ -271,7 +269,7 @@ class GenerateReceipt {
271 269
         $receipt->appendLine(new ReceiptLine("Sales", "", "", ReceiptLine::LINEFORMAT_CENTER));
272 270
         $receipt->appendBreak();
273 271
         foreach ($paymenttypes as $t) {
274
-            $receipt->appendLine(new ReceiptLine(lang($t['text'], false) . ":", "", '$' . number_format($balance[$t['type']], 2)));
272
+            $receipt->appendLine(new ReceiptLine($Strings->get($t['text'], false) . ":", "", '$' . number_format($balance[$t['type']], 2)));
275 273
         }
276 274
 
277 275
         return $receipt;

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

@@ -0,0 +1,135 @@
1
+<?php
2
+
3
+/* This Source Code Form is subject to the terms of the Mozilla Public
4
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
5
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
+
7
+class IPUtils {
8
+
9
+    /**
10
+     * Check if a given ipv4 address is in a given cidr
11
+     * @param  string $ip    IP to check in IPV4 format eg. 127.0.0.1
12
+     * @param  string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
13
+     * @return boolean true if the ip is in this range / false if not.
14
+     * @author Thorsten Ott <https://gist.github.com/tott/7684443>
15
+     */
16
+    public static function ip4_in_cidr($ip, $cidr) {
17
+        if (strpos($cidr, '/') == false) {
18
+            $cidr .= '/32';
19
+        }
20
+        // $range is in IP/CIDR format eg 127.0.0.1/24
21
+        list( $cidr, $netmask ) = explode('/', $cidr, 2);
22
+        $range_decimal = ip2long($cidr);
23
+        $ip_decimal = ip2long($ip);
24
+        $wildcard_decimal = pow(2, ( 32 - $netmask)) - 1;
25
+        $netmask_decimal = ~ $wildcard_decimal;
26
+        return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
27
+    }
28
+
29
+    /**
30
+     * Check if a given ipv6 address is in a given cidr
31
+     * @param string $ip IP to check in IPV6 format
32
+     * @param string $cidr CIDR netmask
33
+     * @return boolean true if the IP is in this range, false otherwise.
34
+     * @author MW. <https://stackoverflow.com/a/7952169>
35
+     */
36
+    public static function ip6_in_cidr($ip, $cidr) {
37
+        $address = inet_pton($ip);
38
+        $subnetAddress = inet_pton(explode("/", $cidr)[0]);
39
+        $subnetMask = explode("/", $cidr)[1];
40
+
41
+        $addr = str_repeat("f", $subnetMask / 4);
42
+        switch ($subnetMask % 4) {
43
+            case 0:
44
+                break;
45
+            case 1:
46
+                $addr .= "8";
47
+                break;
48
+            case 2:
49
+                $addr .= "c";
50
+                break;
51
+            case 3:
52
+                $addr .= "e";
53
+                break;
54
+        }
55
+        $addr = str_pad($addr, 32, '0');
56
+        $addr = pack("H*", $addr);
57
+
58
+        $binMask = $addr;
59
+        return ($address & $binMask) == $subnetAddress;
60
+    }
61
+
62
+    /**
63
+     * Check if the REMOTE_ADDR is on Cloudflare's network.
64
+     * @return boolean true if it is, otherwise false
65
+     */
66
+    public static function validateCloudflare() {
67
+        if (filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
68
+            // Using IPv6
69
+            $cloudflare_ips_v6 = [
70
+                "2400:cb00::/32",
71
+                "2405:8100::/32",
72
+                "2405:b500::/32",
73
+                "2606:4700::/32",
74
+                "2803:f800::/32",
75
+                "2c0f:f248::/32",
76
+                "2a06:98c0::/29"
77
+            ];
78
+            $valid = false;
79
+            foreach ($cloudflare_ips_v6 as $cidr) {
80
+                if (ip6_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
81
+                    $valid = true;
82
+                    break;
83
+                }
84
+            }
85
+        } else {
86
+            // Using IPv4
87
+            $cloudflare_ips_v4 = [
88
+                "103.21.244.0/22",
89
+                "103.22.200.0/22",
90
+                "103.31.4.0/22",
91
+                "104.16.0.0/12",
92
+                "108.162.192.0/18",
93
+                "131.0.72.0/22",
94
+                "141.101.64.0/18",
95
+                "162.158.0.0/15",
96
+                "172.64.0.0/13",
97
+                "173.245.48.0/20",
98
+                "188.114.96.0/20",
99
+                "190.93.240.0/20",
100
+                "197.234.240.0/22",
101
+                "198.41.128.0/17"
102
+            ];
103
+            $valid = false;
104
+            foreach ($cloudflare_ips_v4 as $cidr) {
105
+                if (ip4_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
106
+                    $valid = true;
107
+                    break;
108
+                }
109
+            }
110
+        }
111
+        return $valid;
112
+    }
113
+
114
+    /**
115
+     * Makes a good guess at the client's real IP address.
116
+     *
117
+     * @return string Client IP or `0.0.0.0` if we can't find anything
118
+     */
119
+    public static function getClientIP() {
120
+        // If CloudFlare is in the mix, we should use it.
121
+        // Check if the request is actually from CloudFlare before trusting it.
122
+        if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
123
+            if (validateCloudflare()) {
124
+                return $_SERVER["HTTP_CF_CONNECTING_IP"];
125
+            }
126
+        }
127
+
128
+        if (isset($_SERVER["REMOTE_ADDR"])) {
129
+            return $_SERVER["REMOTE_ADDR"];
130
+        }
131
+
132
+        return "0.0.0.0"; // This will not happen unless we aren't a web server
133
+    }
134
+
135
+}

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

@@ -0,0 +1,129 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class Login {
10
+
11
+    const BAD_USERPASS = 1;
12
+    const BAD_2FA = 2;
13
+    const ACCOUNT_DISABLED = 3;
14
+    const LOGIN_OK = 4;
15
+
16
+    public static function auth(string $username, string $password, string $twofa = ""): int {
17
+        global $database;
18
+        $username = strtolower($username);
19
+
20
+        $user = User::byUsername($username);
21
+
22
+        if (!$user->exists()) {
23
+            return Login::BAD_USERPASS;
24
+        }
25
+        if (!$user->checkPassword($password)) {
26
+            return Login::BAD_USERPASS;
27
+        }
28
+
29
+        if ($user->has2fa()) {
30
+            if (!$user->check2fa($twofa)) {
31
+                return Login::BAD_2FA;
32
+            }
33
+        }
34
+
35
+        switch ($user->getStatus()->get()) {
36
+            case AccountStatus::TERMINATED:
37
+                return Login::BAD_USERPASS;
38
+            case AccountStatus::LOCKED_OR_DISABLED:
39
+                return Login::ACCOUNT_DISABLED;
40
+            case AccountStatus::NORMAL:
41
+            default:
42
+                return Login::LOGIN_OK;
43
+        }
44
+
45
+        return Login::LOGIN_OK;
46
+    }
47
+
48
+    public static function verifyCaptcha(string $session, string $answer, string $url): bool {
49
+        $data = [
50
+            'session_id' => $session,
51
+            'answer_id' => $answer,
52
+            'action' => "verify"
53
+        ];
54
+        $options = [
55
+            'http' => [
56
+                'header' => "Content-type: application/x-www-form-urlencoded\r\n",
57
+                'method' => 'POST',
58
+                'content' => http_build_query($data)
59
+            ]
60
+        ];
61
+        $context = stream_context_create($options);
62
+        $result = file_get_contents($url, false, $context);
63
+        $resp = json_decode($result, TRUE);
64
+        if (!$resp['result']) {
65
+            return false;
66
+        } else {
67
+            return true;
68
+        }
69
+    }
70
+
71
+    /**
72
+     * Check the login server API for sanity
73
+     * @return boolean true if OK, else false
74
+     */
75
+    public static function checkLoginServer() {
76
+        try {
77
+            $client = new GuzzleHttp\Client();
78
+
79
+            $response = $client
80
+                    ->request('POST', PORTAL_API, [
81
+                'form_params' => [
82
+                    'key' => PORTAL_KEY,
83
+                    'action' => "ping"
84
+                ]
85
+            ]);
86
+
87
+            if ($response->getStatusCode() != 200) {
88
+                return false;
89
+            }
90
+
91
+            $resp = json_decode($response->getBody(), TRUE);
92
+            if ($resp['status'] == "OK") {
93
+                return true;
94
+            } else {
95
+                return false;
96
+            }
97
+        } catch (Exception $e) {
98
+            return false;
99
+        }
100
+    }
101
+
102
+    /**
103
+     * Checks if the given AccountHub API key is valid by attempting to
104
+     * access the API with it.
105
+     * @param String $key The API key to check
106
+     * @return boolean TRUE if the key is valid, FALSE if invalid or something went wrong
107
+     */
108
+    function checkAPIKey($key) {
109
+        try {
110
+            $client = new GuzzleHttp\Client();
111
+
112
+            $response = $client
113
+                    ->request('POST', PORTAL_API, [
114
+                'form_params' => [
115
+                    'key' => $key,
116
+                    'action' => "ping"
117
+                ]
118
+            ]);
119
+
120
+            if ($response->getStatusCode() === 200) {
121
+                return true;
122
+            }
123
+            return false;
124
+        } catch (Exception $e) {
125
+            return false;
126
+        }
127
+    }
128
+
129
+}

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

@@ -0,0 +1,65 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class Notifications {
10
+
11
+    /**
12
+     * Add a new notification.
13
+     * @global $database
14
+     * @param User $user
15
+     * @param string $title
16
+     * @param string $content
17
+     * @param string $timestamp If left empty, the current date and time will be used.
18
+     * @param string $url
19
+     * @param bool $sensitive If true, the notification is marked as containing sensitive content, and the $content might be hidden on lockscreens and other non-secure places.
20
+     * @return int The newly-created notification ID.
21
+     * @throws Exception
22
+     */
23
+    public static function add(User $user, string $title, string $content, string $timestamp = "", string $url = "", bool $sensitive = false): int {
24
+        global $Strings;
25
+        if ($user->exists()) {
26
+            if (empty($title) || empty($content)) {
27
+                throw new Exception($Strings->get("invalid parameters", false));
28
+            }
29
+
30
+            $timestamp = date("Y-m-d H:i:s");
31
+            if (!empty($timestamp)) {
32
+                $timestamp = date("Y-m-d H:i:s", strtotime($timestamp));
33
+            }
34
+
35
+            $client = new GuzzleHttp\Client();
36
+
37
+            $response = $client
38
+                    ->request('POST', PORTAL_API, [
39
+                'form_params' => [
40
+                    'key' => PORTAL_KEY,
41
+                    'action' => "addnotification",
42
+                    'uid' => $user->getUID(),
43
+                    'title' => $title,
44
+                    'content' => $content,
45
+                    'timestamp' => $timestamp,
46
+                    'url' => $url,
47
+                    'sensitive' => $sensitive
48
+                ]
49
+            ]);
50
+
51
+            if ($response->getStatusCode() > 299) {
52
+                sendError("Login server error: " . $response->getBody());
53
+            }
54
+
55
+            $resp = json_decode($response->getBody(), TRUE);
56
+            if ($resp['status'] == "OK") {
57
+                return $resp['id'] * 1;
58
+            } else {
59
+                return false;
60
+            }
61
+        }
62
+        throw new Exception($Strings->get("user does not exist", false));
63
+    }
64
+
65
+}

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

@@ -0,0 +1,124 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class Receipt {
10
+
11
+    private $lines = [];
12
+    private $header = [];
13
+    private $footer = [];
14
+
15
+    function __construct() {
16
+
17
+    }
18
+
19
+    function appendLine(ReceiptLine $line) {
20
+        $this->lines[] = $line;
21
+    }
22
+
23
+    function appendLines($lines) {
24
+        foreach ($lines as $l) {
25
+            $this->lines[] = $l;
26
+        }
27
+    }
28
+
29
+    function appendHeader(ReceiptLine $line) {
30
+        $this->header[] = $line;
31
+    }
32
+
33
+    function appendFooter(ReceiptLine $line) {
34
+        $this->footer[] = $line;
35
+    }
36
+
37
+    function appendBreak() {
38
+        $this->lines[] = new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR);
39
+    }
40
+
41
+    function appendBlank() {
42
+        $this->lines[] = new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_BLANK);
43
+    }
44
+
45
+    function getHtml($title = "") {
46
+        global $SECURE_NONCE;
47
+        $html = <<<END
48
+<!DOCTYPE html>
49
+<meta charset="UTF-8">
50
+<meta name="viewport" content="width=device-width, initial-scale=1">
51
+<title>$title</title>
52
+<style nonce="$SECURE_NONCE">
53
+.flex {
54
+    display: flex;
55
+    justify-content: space-between;
56
+    margin: 0;
57
+}
58
+.bold {
59
+    font-weight: bold;
60
+}
61
+.centered {
62
+    justify-content: center;
63
+}
64
+</style>
65
+END;
66
+        if (count($this->header) > 0) {
67
+            foreach ($this->header as $line) {
68
+                $html .= $line->getHtml() . "\n";
69
+            }
70
+            $html .= (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getHtml();
71
+        }
72
+        foreach ($this->lines as $line) {
73
+            $html .= $line->getHtml() . "\n";
74
+        }
75
+        if (count($this->footer) > 0) {
76
+            $html .= (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getHtml();
77
+            foreach ($this->footer as $line) {
78
+                $html .= $line->getHtml() . "\n";
79
+            }
80
+        }
81
+        return $html;
82
+    }
83
+
84
+    function getPlainText($width) {
85
+        $lines = [];
86
+        if (count($this->header) > 0) {
87
+            foreach ($this->header as $line) {
88
+                $lines[] = $line->getPlainText($width);
89
+            }
90
+            $lines[] = (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getPlainText($width);
91
+        }
92
+        foreach ($this->lines as $line) {
93
+            $lines[] = $line->getPlainText($width);
94
+        }
95
+        if (count($this->footer) > 0) {
96
+            $lines[] = (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getPlainText($width);
97
+            foreach ($this->footer as $line) {
98
+                $lines[] = $line->getPlainText($width);
99
+            }
100
+        }
101
+        return implode("\n", $lines);
102
+    }
103
+
104
+    function getArray($width = 64) {
105
+        $header = [];
106
+        $lines = [];
107
+        $footer = [];
108
+        foreach ($this->header as $line) {
109
+            $header[] = $line->getArray($width);
110
+        }
111
+        foreach ($this->lines as $line) {
112
+            $lines[] = $line->getArray($width);
113
+        }
114
+        foreach ($this->footer as $line) {
115
+            $footer[] = $line->getArray($width);
116
+        }
117
+        return ["header" => $header, "lines" => $lines, "footer" => $footer];
118
+    }
119
+
120
+    function getJson($width = 64) {
121
+        return json_encode($this->getArray($width));
122
+    }
123
+
124
+}

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

@@ -135,120 +135,3 @@ class ReceiptLine {
135 135
     }
136 136
 
137 137
 }
138
-
139
-class Receipt {
140
-
141
-    private $lines = [];
142
-    private $header = [];
143
-    private $footer = [];
144
-
145
-    function __construct() {
146
-
147
-    }
148
-
149
-    function appendLine(ReceiptLine $line) {
150
-        $this->lines[] = $line;
151
-    }
152
-
153
-    function appendLines($lines) {
154
-        foreach ($lines as $l) {
155
-            $this->lines[] = $l;
156
-        }
157
-    }
158
-
159
-    function appendHeader(ReceiptLine $line) {
160
-        $this->header[] = $line;
161
-    }
162
-
163
-    function appendFooter(ReceiptLine $line) {
164
-        $this->footer[] = $line;
165
-    }
166
-
167
-    function appendBreak() {
168
-        $this->lines[] = new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR);
169
-    }
170
-
171
-    function appendBlank() {
172
-        $this->lines[] = new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_BLANK);
173
-    }
174
-
175
-    function getHtml($title = "") {
176
-        global $SECURE_NONCE;
177
-        $html = <<<END
178
-<!DOCTYPE html>
179
-<meta charset="UTF-8">
180
-<meta name="viewport" content="width=device-width, initial-scale=1">
181
-<title>$title</title>
182
-<style nonce="$SECURE_NONCE">
183
-.flex {
184
-    display: flex;
185
-    justify-content: space-between;
186
-    margin: 0;
187
-}
188
-.bold {
189
-    font-weight: bold;
190
-}
191
-.centered {
192
-    justify-content: center;
193
-}
194
-</style>
195
-END;
196
-        if (count($this->header) > 0) {
197
-            foreach ($this->header as $line) {
198
-                $html .= $line->getHtml() . "\n";
199
-            }
200
-            $html .= (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getHtml();
201
-        }
202
-        foreach ($this->lines as $line) {
203
-            $html .= $line->getHtml() . "\n";
204
-        }
205
-        if (count($this->footer) > 0) {
206
-            $html .= (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getHtml();
207
-            foreach ($this->footer as $line) {
208
-                $html .= $line->getHtml() . "\n";
209
-            }
210
-        }
211
-        return $html;
212
-    }
213
-
214
-    function getPlainText($width) {
215
-        $lines = [];
216
-        if (count($this->header) > 0) {
217
-            foreach ($this->header as $line) {
218
-                $lines[] = $line->getPlainText($width);
219
-            }
220
-            $lines[] = (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getPlainText($width);
221
-        }
222
-        foreach ($this->lines as $line) {
223
-            $lines[] = $line->getPlainText($width);
224
-        }
225
-        if (count($this->footer) > 0) {
226
-            $lines[] = (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getPlainText($width);
227
-            foreach ($this->footer as $line) {
228
-                $lines[] = $line->getPlainText($width);
229
-            }
230
-        }
231
-        return implode("\n", $lines);
232
-    }
233
-
234
-    function getArray($width = 64) {
235
-        $header = [];
236
-        $lines = [];
237
-        $footer = [];
238
-        foreach ($this->header as $line) {
239
-            $header[] = $line->getArray($width);
240
-        }
241
-        foreach ($this->lines as $line) {
242
-            $lines[] = $line->getArray($width);
243
-        }
244
-        foreach ($this->footer as $line) {
245
-            $footer[] = $line->getArray($width);
246
-        }
247
-        return ["header" => $header, "lines" => $lines, "footer" => $footer];
248
-    }
249
-
250
-    function getJson($width = 64) {
251
-        return json_encode($this->getArray($width));
252
-    }
253
-
254
-}

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

@@ -0,0 +1,137 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+use League\Csv\Writer;
10
+use League\Csv\HTMLConverter;
11
+use odsPhpGenerator\ods;
12
+use odsPhpGenerator\odsTable;
13
+use odsPhpGenerator\odsTableRow;
14
+use odsPhpGenerator\odsTableColumn;
15
+use odsPhpGenerator\odsTableCellString;
16
+use odsPhpGenerator\odsStyleTableColumn;
17
+use odsPhpGenerator\odsStyleTableCell;
18
+
19
+class Report {
20
+
21
+    private $title = "";
22
+    private $header = [];
23
+    private $data = [];
24
+
25
+    public function __construct(string $title = "", array $header = [], array $data = []) {
26
+        $this->title = $title;
27
+        $this->header = $header;
28
+        $this->data = $data;
29
+    }
30
+
31
+    public function setHeader(array $header) {
32
+        $this->header = $header;
33
+    }
34
+
35
+    public function addDataRow(array $columns) {
36
+        $this->data[] = $columns;
37
+    }
38
+
39
+    public function getHeader(): array {
40
+        return $this->header;
41
+    }
42
+
43
+    public function getData(): array {
44
+        return $this->data;
45
+    }
46
+
47
+    public function output(string $format) {
48
+        switch ($format) {
49
+            case "ods":
50
+                $this->toODS();
51
+                break;
52
+            case "html":
53
+                $this->toHTML();
54
+                break;
55
+            case "csv":
56
+            default:
57
+                $this->toCSV();
58
+                break;
59
+        }
60
+    }
61
+
62
+    private function toODS() {
63
+        $ods = new ods();
64
+        $styleColumn = new odsStyleTableColumn();
65
+        $styleColumn->setUseOptimalColumnWidth(true);
66
+        $headerstyle = new odsStyleTableCell();
67
+        $headerstyle->setFontWeight("bold");
68
+        $table = new odsTable($this->title);
69
+
70
+        for ($i = 0; $i < count($this->header); $i++) {
71
+            $table->addTableColumn(new odsTableColumn($styleColumn));
72
+        }
73
+
74
+        $row = new odsTableRow();
75
+        foreach ($this->header as $cell) {
76
+            $row->addCell(new odsTableCellString($cell, $headerstyle));
77
+        }
78
+        $table->addRow($row);
79
+
80
+        foreach ($this->data as $cols) {
81
+            $row = new odsTableRow();
82
+            foreach ($cols as $cell) {
83
+                $row->addCell(new odsTableCellString($cell));
84
+            }
85
+            $table->addRow($row);
86
+        }
87
+        $ods->addTable($table);
88
+        // The @ is a workaround to silence the tempnam notice,
89
+        // which breaks the file.  This is apparently the intended behavior:
90
+        // https://bugs.php.net/bug.php?id=69489
91
+        @$ods->downloadOdsFile($this->title . "_" . date("Y-m-d_Hi") . ".ods");
92
+    }
93
+
94
+    private function toHTML() {
95
+        global $SECURE_NONCE;
96
+        $data = array_merge([$this->header], $this->data);
97
+        // HTML exporter doesn't like null values
98
+        for ($i = 0; $i < count($data); $i++) {
99
+            for ($j = 0; $j < count($data[$i]); $j++) {
100
+                if (is_null($data[$i][$j])) {
101
+                    $data[$i][$j] = '';
102
+                }
103
+            }
104
+        }
105
+        header('Content-type: text/html');
106
+        $converter = new HTMLConverter();
107
+        $out = "<!DOCTYPE html>\n"
108
+                . "<meta charset=\"utf-8\">\n"
109
+                . "<meta name=\"viewport\" content=\"width=device-width\">\n"
110
+                . "<title>" . $this->title . "_" . date("Y-m-d_Hi") . "</title>\n"
111
+                . <<<STYLE
112
+<style nonce="$SECURE_NONCE">
113
+    .table-csv-data {
114
+        border-collapse: collapse;
115
+    }
116
+    .table-csv-data tr:first-child {
117
+        font-weight: bold;
118
+    }
119
+    .table-csv-data tr td {
120
+        border: 1px solid black;
121
+    }
122
+</style>
123
+STYLE
124
+                . $converter->convert($data);
125
+        echo $out;
126
+    }
127
+
128
+    private function toCSV() {
129
+        $csv = Writer::createFromString('');
130
+        $data = array_merge([$this->header], $this->data);
131
+        $csv->insertAll($data);
132
+        header('Content-type: text/csv');
133
+        header('Content-Disposition: attachment; filename="' . $this->title . "_" . date("Y-m-d_Hi") . ".csv" . '"');
134
+        echo $csv;
135
+    }
136
+
137
+}

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

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

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

@@ -0,0 +1,118 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+/**
10
+ * Provides translated language strings.
11
+ */
12
+class Strings {
13
+
14
+    private $language = "en";
15
+    private $strings = [];
16
+
17
+    public function __construct($language = "en") {
18
+        if (!preg_match("/[a-zA-Z\_\-]+/", $language)) {
19
+            throw new Exception("Invalid language code $language");
20
+        }
21
+
22
+        $this->load("en");
23
+
24
+        if (file_exists(__DIR__ . "/../langs/$language/")) {
25
+            $this->language = $language;
26
+            $this->load($language);
27
+        } else {
28
+            trigger_error("Language $language could not be found.", E_USER_WARNING);
29
+        }
30
+    }
31
+
32
+    /**
33
+     * Load all JSON files for the specified language.
34
+     * @param string $language
35
+     */
36
+    private function load(string $language) {
37
+        $files = glob(__DIR__ . "/../langs/$language/*.json");
38
+        foreach ($files as $file) {
39
+            $strings = json_decode(file_get_contents($file), true);
40
+            foreach ($strings as $key => $val) {
41
+                if (array_key_exists($key, $this->strings)) {
42
+                    trigger_error("Language key \"$key\" is defined more than once.", E_USER_WARNING);
43
+                }
44
+                $this->strings[$key] = $val;
45
+            }
46
+        }
47
+    }
48
+
49
+    /**
50
+     * Add language strings dynamically.
51
+     * @param array $strings ["key" => "value", ...]
52
+     */
53
+    public function addStrings(array $strings) {
54
+        foreach ($strings as $key => $val) {
55
+            $this->strings[$key] = $val;
56
+        }
57
+    }
58
+
59
+    /**
60
+     * I18N string getter.  If the key isn't found, it outputs the key itself.
61
+     * @param string $key
62
+     * @param bool $echo True to echo the result, false to return it.  Default is true.
63
+     * @return string
64
+     */
65
+    public function get(string $key, bool $echo = true): string {
66
+        $str = $key;
67
+        if (array_key_exists($key, $this->strings)) {
68
+            $str = $this->strings[$key];
69
+        } else {
70
+            trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING);
71
+        }
72
+
73
+        if ($echo) {
74
+            echo $str;
75
+        }
76
+        return $str;
77
+    }
78
+
79
+    /**
80
+     * I18N string getter (with builder).    If the key doesn't exist, outputs the key itself.
81
+     * @param string $key
82
+     * @param array $replace key-value array of replacements.
83
+     * If the string value is "hello {abc}" and you give ["abc" => "123"], the
84
+     * result will be "hello 123".
85
+     * @param bool $echo True to echo the result, false to return it.  Default is true.
86
+     * @return string
87
+     */
88
+    public function build(string $key, array $replace, bool $echo = true): string {
89
+        $str = $key;
90
+        if (array_key_exists($key, $this->strings)) {
91
+            $str = $this->strings[$key];
92
+        } else {
93
+            trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING);
94
+        }
95
+
96
+        foreach ($replace as $find => $repl) {
97
+            $str = str_replace("{" . $find . "}", $repl, $str);
98
+        }
99
+
100
+        if ($echo) {
101
+            echo $str;
102
+        }
103
+        return $str;
104
+    }
105
+
106
+    /**
107
+     * Builds and returns a JSON key:value string for the supplied array of keys.
108
+     * @param array $keys ["key1", "key2", ...]
109
+     */
110
+    public function getJSON(array $keys): string {
111
+        $strings = [];
112
+        foreach ($keys as $k) {
113
+            $strings[$k] = $this->get($k, false);
114
+        }
115
+        return json_encode($strings);
116
+    }
117
+
118
+}

+ 352
- 0
lib/User.lib.php View File

@@ -0,0 +1,352 @@
1
+<?php
2
+
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
5
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+class User {
10
+
11
+    private $uid = null;
12
+    private $username;
13
+    private $email;
14
+    private $realname;
15
+    private $has2fa = false;
16
+    private $exists = false;
17
+
18
+    public function __construct(int $uid, string $username = "") {
19
+        // Check if user exists
20
+        $client = new GuzzleHttp\Client();
21
+
22
+        $response = $client
23
+                ->request('POST', PORTAL_API, [
24
+            'form_params' => [
25
+                'key' => PORTAL_KEY,
26
+                'action' => "userexists",
27
+                'uid' => $uid
28
+            ]
29
+        ]);
30
+
31
+        if ($response->getStatusCode() > 299) {
32
+            sendError("Login server error: " . $response->getBody());
33
+        }
34
+
35
+        $resp = json_decode($response->getBody(), TRUE);
36
+        if ($resp['status'] == "OK" && $resp['exists'] === true) {
37
+            $this->exists = true;
38
+        } else {
39
+            $this->uid = $uid;
40
+            $this->username = $username;
41
+            $this->exists = false;
42
+        }
43
+
44
+        if ($this->exists) {
45
+            // Get user info
46
+            $client = new GuzzleHttp\Client();
47
+
48
+            $response = $client
49
+                    ->request('POST', PORTAL_API, [
50
+                'form_params' => [
51
+                    'key' => PORTAL_KEY,
52
+                    'action' => "userinfo",
53
+                    'uid' => $uid
54
+                ]
55
+            ]);
56
+
57
+            if ($response->getStatusCode() > 299) {
58
+                sendError("Login server error: " . $response->getBody());
59
+            }
60
+
61
+            $resp = json_decode($response->getBody(), TRUE);
62
+            if ($resp['status'] == "OK") {
63
+                $this->uid = $resp['data']['uid'] * 1;
64
+                $this->username = $resp['data']['username'];
65
+                $this->email = $resp['data']['email'];
66
+                $this->realname = $resp['data']['name'];
67
+            } else {
68
+                sendError("Login server error: " . $resp['msg']);
69
+            }
70
+        }
71
+    }
72
+
73
+    public static function byUsername(string $username): User {
74
+        $client = new GuzzleHttp\Client();
75
+
76
+        $response = $client
77
+                ->request('POST', PORTAL_API, [
78
+            'form_params' => [
79
+                'key' => PORTAL_KEY,
80
+                'username' => $username,
81
+                'action' => "userinfo"
82
+            ]
83
+        ]);
84
+
85
+        if ($response->getStatusCode() > 299) {
86
+            sendError("Login server error: " . $response->getBody());
87
+        }
88
+
89
+        $resp = json_decode($response->getBody(), TRUE);
90
+        if (!isset($resp['status'])) {
91
+            sendError("Login server error: " . $resp);
92
+        }
93
+        if ($resp['status'] == "OK") {
94
+            return new self($resp['data']['uid'] * 1);
95
+        } else {
96
+            return new self(-1, $username);
97
+        }
98
+    }
99
+
100
+    public function exists(): bool {
101
+        return $this->exists;
102
+    }
103
+
104
+    public function has2fa(): bool {
105
+        if (!$this->exists) {
106
+            return false;
107
+        }
108
+        $client = new GuzzleHttp\Client();
109
+
110
+        $response = $client
111
+                ->request('POST', PORTAL_API, [
112
+            'form_params' => [
113
+                'key' => PORTAL_KEY,
114
+                'action' => "hastotp",
115
+                'username' => $this->username
116
+            ]
117
+        ]);
118
+
119
+        if ($response->getStatusCode() > 299) {
120
+            sendError("Login server error: " . $response->getBody());
121
+        }
122
+
123
+        $resp = json_decode($response->getBody(), TRUE);
124
+        if ($resp['status'] == "OK") {
125
+            return $resp['otp'] == true;
126
+        } else {
127
+            return false;
128
+        }
129
+    }
130
+
131
+    function getUsername() {
132
+        return $this->username;
133
+    }
134
+
135
+    function getUID() {
136
+        return $this->uid;
137
+    }
138
+
139
+    function getEmail() {
140
+        return $this->email;
141
+    }
142
+
143
+    function getName() {
144
+        return $this->realname;
145
+    }
146
+
147
+    /**
148
+     * Check the given plaintext password against the stored hash.
149
+     * @param string $password
150
+     * @return bool
151
+     */
152
+    function checkPassword(string $password): bool {
153
+        $client = new GuzzleHttp\Client();
154
+
155
+        $response = $client
156
+                ->request('POST', PORTAL_API, [
157
+            'form_params' => [
158
+                'key' => PORTAL_KEY,
159
+                'action' => "auth",
160
+                'username' => $this->username,
161
+                'password' => $password
162
+            ]
163
+        ]);
164
+
165
+        if ($response->getStatusCode() > 299) {
166
+            sendError("Login server error: " . $response->getBody());
167
+        }
168
+
169
+        $resp = json_decode($response->getBody(), TRUE);
170
+        if ($resp['status'] == "OK") {
171
+            return true;
172
+        } else {
173
+            return false;
174
+        }
175
+    }
176
+
177
+    function check2fa(string $code): bool {
178
+        if (!$this->has2fa) {
179
+            return true;
180
+        }
181
+        $client = new GuzzleHttp\Client();
182
+
183
+        $response = $client
184
+                ->request('POST', PORTAL_API, [
185
+            'form_params' => [
186
+                'key' => PORTAL_KEY,
187
+                'action' => "verifytotp",
188
+                'username' => $this->username,
189
+                'code' => $code
190
+            ]
191
+        ]);
192
+
193
+        if ($response->getStatusCode() > 299) {
194
+            sendError("Login server error: " . $response->getBody());
195
+        }
196
+
197
+        $resp = json_decode($response->getBody(), TRUE);
198
+        if ($resp['status'] == "OK") {
199
+            return $resp['valid'];
200
+        } else {
201
+            return false;
202
+        }
203
+    }
204
+
205
+    /**
206
+     * Check if the given username has the given permission (or admin access)
207
+     * @global $database $database
208
+     * @param string $code
209
+     * @return boolean TRUE if the user has the permission (or admin access), else FALSE
210
+     */
211
+    function hasPermission(string $code): bool {
212
+        $client = new GuzzleHttp\Client();
213
+
214
+        $response = $client
215
+                ->request('POST', PORTAL_API, [
216
+            'form_params' => [
217
+                'key' => PORTAL_KEY,
218
+                'action' => "permission",
219
+                'username' => $this->username,
220
+                'code' => $code
221
+            ]
222
+        ]);
223
+
224
+        if ($response->getStatusCode() > 299) {
225
+            sendError("Login server error: " . $response->getBody());
226
+        }
227
+
228
+        $resp = json_decode($response->getBody(), TRUE);
229
+        if ($resp['status'] == "OK") {
230
+            return $resp['has_permission'];
231
+        } else {
232
+            return false;
233
+        }
234
+    }
235
+
236
+    /**
237
+     * Get the account status.
238
+     * @return \AccountStatus
239
+     */
240
+    function getStatus(): AccountStatus {
241
+
242
+        $client = new GuzzleHttp\Client();
243
+
244
+        $response = $client
245
+                ->request('POST', PORTAL_API, [
246
+            'form_params' => [
247
+                'key' => PORTAL_KEY,
248
+                'action' => "acctstatus",
249
+                'username' => $this->username
250
+            ]
251
+        ]);
252
+
253
+        if ($response->getStatusCode() > 299) {
254
+            sendError("Login server error: " . $response->getBody());
255
+        }
256
+
257
+        $resp = json_decode($response->getBody(), TRUE);
258
+        if ($resp['status'] == "OK") {
259
+            return AccountStatus::fromString($resp['account']);
260
+        } else {
261
+            return null;
262
+        }
263
+    }
264
+
265
+    function sendAlertEmail(string $appname = SITE_TITLE) {
266
+        $client = new GuzzleHttp\Client();
267
+
268
+        $response = $client
269
+                ->request('POST', PORTAL_API, [
270
+            'form_params' => [
271
+                'key' => PORTAL_KEY,
272
+                'action' => "alertemail",
273
+                'username' => $this->username,
274
+                'appname' => SITE_TITLE
275
+            ]
276
+        ]);
277
+
278
+        if ($response->getStatusCode() > 299) {
279
+            return "An unknown error occurred.";
280
+        }
281
+
282
+        $resp = json_decode($response->getBody(), TRUE);
283
+        if ($resp['status'] == "OK") {
284
+            return true;
285
+        } else {
286
+            return $resp['msg'];
287
+        }
288
+    }
289
+
290
+}
291
+
292
+class AccountStatus {
293
+
294
+    const NORMAL = 1;
295
+    const LOCKED_OR_DISABLED = 2;
296
+    const CHANGE_PASSWORD = 3;
297
+    const TERMINATED = 4;
298
+    const ALERT_ON_ACCESS = 5;
299
+
300
+    private $status;
301
+
302
+    public function __construct(int $status) {
303
+        $this->status = $status;
304
+    }
305
+
306
+    public static function fromString(string $status): AccountStatus {
307
+        switch ($status) {
308
+            case "NORMAL":
309
+                return new self(self::NORMAL);
310
+            case "LOCKED_OR_DISABLED":
311
+                return new self(self::LOCKED_OR_DISABLED);
312
+            case "CHANGE_PASSWORD":
313
+                return new self(self::CHANGE_PASSWORD);
314
+            case "TERMINATED":
315
+                return new self(self::TERMINATED);
316
+            case "ALERT_ON_ACCESS":
317
+                return new self(self::ALERT_ON_ACCESS);
318
+            default:
319
+                return new self(0);
320
+        }
321
+    }
322
+
323
+    /**
324
+     * Get the account status/state as an integer.
325
+     * @return int
326
+     */
327
+    public function get(): int {
328
+        return $this->status;
329
+    }
330
+
331
+    /**
332
+     * Get the account status/state as a string representation.
333
+     * @return string
334
+     */
335
+    public function getString(): string {
336
+        switch ($this->status) {
337
+            case self::NORMAL:
338
+                return "NORMAL";
339
+            case self::LOCKED_OR_DISABLED:
340
+                return "LOCKED_OR_DISABLED";
341
+            case self::CHANGE_PASSWORD:
342
+                return "CHANGE_PASSWORD";
343
+            case self::TERMINATED:
344
+                return "TERMINATED";
345
+            case self::ALERT_ON_ACCESS:
346
+                return "ALERT_ON_ACCESS";
347
+            default:
348
+                return "OTHER_" . $this->status;
349
+        }
350
+    }
351
+
352
+}

+ 3
- 3
lib/chooseregister.php View File

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

+ 0
- 131
lib/iputils.php View File

@@ -1,131 +0,0 @@
1
-<?php
2
-
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
-
7
-/**
8
- * Check if a given ipv4 address is in a given cidr