Browse Source

Merge ../BusinessAppTemplate

# Conflicts:
#	README.md
#	api.php
#	index.php
#	langs/en/core.json
#	langs/en/titles.json
#	mobile/index.php
#	required.php
#	settings.template.php
Skylar Ittner 3 months ago
parent
commit
e1837a22fe

+ 3
- 15
LICENSE.md View File

@@ -1,19 +1,7 @@
1
-Copyright (c) 2018 Netsyms Technologies.
1
+Copyright (c) 2017-2019 Netsyms Technologies.  Some rights reserved.
2 2
 
3
-If you modify and redistribute this project, you must replace the branding
4
-assets with your own.
5
-
6
-The branding assets include:
7
- * the application icon
8
- * the Netsyms N punchcard logo
9
- * the Netsyms for Business graph logo
10
-
11
-If you are unsure if your usage is allowed, please contact us:
12
-https://netsyms.com/contact
13
-legal@netsyms.com
14
-
15
-All other portions of this application,
16
-unless otherwise noted (in comments, headers, etc), are licensed as follows:
3
+Licensed under the Mozilla Public License Version 2.0.  Files without MPL header
4
+comments, including third party code, may be under a different license.
17 5
 
18 6
 Mozilla Public License Version 2.0
19 7
 ==================================

+ 21
- 21
action.php View File

@@ -36,7 +36,7 @@ if ($VARS['action'] != "signout" && !(new User($_SESSION['uid']))->hasPermission
36 36
 switch ($VARS['action']) {
37 37
     case "edititem":
38 38
         $insert = true;
39
-        if (is_empty($VARS['itemid'])) {
39
+        if (empty($VARS['itemid'])) {
40 40
             $insert = true;
41 41
         } else {
42 42
             if ($database->has('items', ['itemid' => $VARS['itemid']])) {
@@ -45,42 +45,42 @@ switch ($VARS['action']) {
45 45
                 returnToSender("invalid_itemid");
46 46
             }
47 47
         }
48
-        if (is_empty($VARS['name'])) {
48
+        if (empty($VARS['name'])) {
49 49
             returnToSender('missing_name');
50 50
         }
51
-        if (!is_empty($VARS['catstr']) && is_empty($VARS['cat'])) {
51
+        if (!empty($VARS['catstr']) && empty($VARS['cat'])) {
52 52
             if ($database->count("categories", ["catname" => $VARS['catstr']]) == 1) {
53 53
                 $VARS['cat'] = $database->get("categories", 'catid', ["catname" => $VARS['catstr']]);
54 54
             } else {
55 55
                 returnToSender('use_the_drop_luke');
56 56
             }
57 57
         }
58
-        if (!is_empty($VARS['locstr']) && is_empty($VARS['loc'])) {
58
+        if (!empty($VARS['locstr']) && empty($VARS['loc'])) {
59 59
             if ($database->count("locations", ["locname" => $VARS['locstr']]) == 1) {
60 60
                 $VARS['loc'] = $database->get("locations", 'locid', ["locname" => $VARS['locstr']]);
61 61
             } else {
62 62
                 returnToSender('use_the_drop_luke');
63 63
             }
64 64
         }
65
-        if (is_empty($VARS['cat']) || is_empty($VARS['loc'])) {
65
+        if (empty($VARS['cat']) || empty($VARS['loc'])) {
66 66
             returnToSender('invalid_parameters');
67 67
         }
68
-        if (is_empty($VARS['qty'])) {
68
+        if (empty($VARS['qty'])) {
69 69
             $VARS['qty'] = 1;
70 70
         } else if (!is_numeric($VARS['qty'])) {
71 71
             returnToSender('field_nan');
72 72
         }
73
-        if (is_empty($VARS['want'])) {
73
+        if (empty($VARS['want'])) {
74 74
             $VARS['want'] = 0;
75 75
         } else if (!is_numeric($VARS['want'])) {
76 76
             returnToSender('field_nan');
77 77
         }
78
-        if (is_empty($VARS['cost'])) {
78
+        if (empty($VARS['cost'])) {
79 79
             $VARS['cost'] = null;
80 80
         } else if (!is_numeric($VARS['cost'])) {
81 81
             returnToSender('field_nan');
82 82
         }
83
-        if (is_empty($VARS['price'])) {
83
+        if (empty($VARS['price'])) {
84 84
             $VARS['price'] = null;
85 85
         } else if (!is_numeric($VARS['price'])) {
86 86
             returnToSender('field_nan');
@@ -128,7 +128,7 @@ switch ($VARS['action']) {
128 128
         returnToSender("item_saved");
129 129
     case "editcat":
130 130
         $insert = true;
131
-        if (is_empty($VARS['catid'])) {
131
+        if (empty($VARS['catid'])) {
132 132
             $insert = true;
133 133
         } else {
134 134
             if ($database->has('categories', ['catid' => $VARS['catid']])) {
@@ -137,7 +137,7 @@ switch ($VARS['action']) {
137 137
                 returnToSender("invalid_catid");
138 138
             }
139 139
         }
140
-        if (is_empty($VARS['name'])) {
140
+        if (empty($VARS['name'])) {
141 141
             returnToSender('invalid_parameters');
142 142
         }
143 143
 
@@ -154,7 +154,7 @@ switch ($VARS['action']) {
154 154
         returnToSender("category_saved");
155 155
     case "editloc":
156 156
         $insert = true;
157
-        if (is_empty($VARS['locid'])) {
157
+        if (empty($VARS['locid'])) {
158 158
             $insert = true;
159 159
         } else {
160 160
             if ($database->has('locations', ['locid' => $VARS['locid']])) {
@@ -163,7 +163,7 @@ switch ($VARS['action']) {
163 163
                 returnToSender("invalid_locid");
164 164
             }
165 165
         }
166
-        if (is_empty($VARS['name'])) {
166
+        if (empty($VARS['name'])) {
167 167
             returnToSender('invalid_parameters');
168 168
         }
169 169
 
@@ -217,9 +217,9 @@ switch ($VARS['action']) {
217 217
         $client = new GuzzleHttp\Client();
218 218
 
219 219
         $response = $client
220
-                ->request('POST', PORTAL_API, [
220
+                ->request('POST', $SETTINGS['accounthub']['api'], [
221 221
             'form_params' => [
222
-                'key' => PORTAL_KEY,
222
+                'key' => $SETTINGS['accounthub']['key'],
223 223
                 'action' => "usersearch",
224 224
                 'search' => $VARS['q']
225 225
             ]
@@ -237,7 +237,7 @@ switch ($VARS['action']) {
237 237
         }
238 238
         break;
239 239
     case "imageupload":
240
-        $destpath = FILE_UPLOAD_PATH;
240
+        $destpath = $SETTINGS['file_upload_path'];
241 241
         if (!is_writable($destpath)) {
242 242
             returnToSender("unwritable_folder", "&id=$VARS[itemid]");
243 243
         }
@@ -274,7 +274,7 @@ switch ($VARS['action']) {
274 274
                     default:
275 275
                         $err = "could not be uploaded.";
276 276
                 }
277
-                $errors[] = htmlspecialchars($f['name']) . " $err";
277
+                $errors[] = htmlentities($f['name']) . " $err";
278 278
                 continue;
279 279
             }
280 280
 
@@ -296,7 +296,7 @@ switch ($VARS['action']) {
296 296
             }
297 297
 
298 298
             if (!$imagevalid) {
299
-                $errors[] = htmlspecialchars($f['name']) . " is not a supported image type (JPEG, GIF, PNG, WEBP).";
299
+                $errors[] = htmlentities($f['name']) . " is not a supported image type (JPEG, GIF, PNG, WEBP).";
300 300
                 continue;
301 301
             }
302 302
 
@@ -319,7 +319,7 @@ switch ($VARS['action']) {
319 319
                 }
320 320
                 $database->insert('images', ['itemid' => $VARS['itemid'], 'imagename' => $filename, 'primary' => $primary]);
321 321
             } else {
322
-                $errors[] = htmlspecialchars($f['name']) . " could not be uploaded.";
322
+                $errors[] = htmlentities($f['name']) . " could not be uploaded.";
323 323
             }
324 324
         }
325 325
 
@@ -350,7 +350,7 @@ switch ($VARS['action']) {
350 350
 
351 351
         $imagename = $database->get('images', 'imagename', ['imageid' => $VARS['imageid']]);
352 352
         if ($database->count('images', ['imagename' => $imagename]) <= 1) {
353
-            unlink(FILE_UPLOAD_PATH . "/" . $imagename);
353
+            unlink($SETTINGS['file_upload_path'] . "/" . $imagename);
354 354
         }
355 355
         $database->delete('images', ['AND' => ['itemid' => $VARS['itemid'], 'imageid' => $VARS['imageid']]]);
356 356
 
@@ -361,6 +361,6 @@ switch ($VARS['action']) {
361 361
         returnToSender("image_deleted", "&id=$VARS[itemid]");
362 362
     case "signout":
363 363
         session_destroy();
364
-        header('Location: index.php');
364
+        header('Location: index.php?logout=1');
365 365
         die("Logged out.");
366 366
 }

+ 2
- 31
api.php View File

@@ -4,35 +4,6 @@
4 4
  * License, v. 2.0. If a copy of the MPL was not distributed with this
5 5
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 6
 
7
-/**
8
- * Simple JSON API to allow other apps to access data from this app.
9
- *
10
- * Requests can be sent via either GET or POST requests.  POST is recommended
11
- * as it has a lower chance of being logged on the server, exposing unencrypted
12
- * user passwords.
13
- */
14
-require __DIR__ . '/required.php';
15
-header("Content-Type: application/json");
16 7
 
17
-$username = $VARS['username'];
18
-$password = $VARS['password'];
19
-$user = User::byUsername($username);
20
-if ($user->exists() !== true || Login::auth($username, $password) !== Login::LOGIN_OK) {
21
-    header("HTTP/1.1 403 Unauthorized");
22
-    die("\"403 Unauthorized\"");
23
-}
24
-
25
-// query max results
26
-$max = 20;
27
-if (preg_match("/^[0-9]+$/", $VARS['max']) === 1 && $VARS['max'] <= 1000) {
28
-    $max = (int) $VARS['max'];
29
-}
30
-
31
-switch ($VARS['action']) {
32
-    case "ping":
33
-        $out = ["status" => "OK", "maxresults" => $max, "pong" => true];
34
-        exit(json_encode($out));
35
-    default:
36
-        header("HTTP/1.1 400 Bad Request");
37
-        die("\"400 Bad Request\"");
38
-}
8
+// Load in new API from legacy location (a.k.a. here)
9
+require __DIR__ . "/api/index.php";

+ 5
- 0
api/.htaccess View File

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

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

@@ -0,0 +1,9 @@
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
+sendJsonResp();

+ 15
- 0
api/apisettings.php View File

@@ -0,0 +1,15 @@
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
+$APIS = [
10
+    "ping" => [
11
+        "load" => "ping.php",
12
+        "vars" => [
13
+        ]
14
+    ]
15
+];

+ 144
- 0
api/functions.php View File

@@ -0,0 +1,144 @@
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
+ * Build and send a simple JSON response.
11
+ * @param string $msg A message
12
+ * @param string $status "OK" or "ERROR"
13
+ * @param array $data More JSON data
14
+ */
15
+function sendJsonResp(string $msg = null, string $status = "OK", array $data = null) {
16
+    $resp = [];
17
+    if (!is_null($data)) {
18
+        $resp = $data;
19
+    }
20
+    if (!is_null($msg)) {
21
+        $resp["msg"] = $msg;
22
+    }
23
+    $resp["status"] = $status;
24
+    header("Content-Type: application/json");
25
+    exit(json_encode($resp));
26
+}
27
+
28
+function exitWithJson(array $json) {
29
+    header("Content-Type: application/json");
30
+    exit(json_encode($json));
31
+}
32
+
33
+/**
34
+ * Get the API key with most of the characters replaced with *s.
35
+ * @global string $key
36
+ * @return string
37
+ */
38
+function getCensoredKey() {
39
+    global $key;
40
+    $resp = $key;
41
+    if (strlen($key) > 5) {
42
+        for ($i = 2; $i < strlen($key) - 2; $i++) {
43
+            $resp[$i] = "*";
44
+        }
45
+    }
46
+    return $resp;
47
+}
48
+
49
+/**
50
+ * Check if the request is allowed
51
+ * @global array $VARS
52
+ * @return bool true if the request should continue, false if the request is bad
53
+ */
54
+function authenticate(): bool {
55
+    global $VARS;
56
+    // HTTP basic auth
57
+    if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
58
+        $user = User::byUsername($_SERVER['PHP_AUTH_USER']);
59
+        if (!$user->checkPassword($_SERVER['PHP_AUTH_PW'])) {
60
+            return false;
61
+        }
62
+        return true;
63
+    }
64
+    // Form auth
65
+    if (empty($VARS['username']) || empty($VARS['password'])) {
66
+        return false;
67
+    } else {
68
+        $username = $VARS['username'];
69
+        $password = $VARS['password'];
70
+        $user = User::byUsername($username);
71
+        if ($user->exists() !== true || Login::auth($username, $password) !== Login::LOGIN_OK) {
72
+            return false;
73
+        }
74
+    }
75
+    return true;
76
+}
77
+
78
+/**
79
+ * Get the User whose credentials were used to make the request.
80
+ */
81
+function getRequestUser(): User {
82
+    global $VARS;
83
+    if (!empty($_SERVER['PHP_AUTH_USER'])) {
84
+        return User::byUsername($_SERVER['PHP_AUTH_USER']);
85
+    } else {
86
+        return User::byUsername($VARS['username']);
87
+    }
88
+}
89
+
90
+function checkVars($vars, $or = false) {
91
+    global $VARS;
92
+    $ok = [];
93
+    foreach ($vars as $key => $val) {
94
+        if (strpos($key, "OR") === 0) {
95
+            checkVars($vars[$key], true);
96
+            continue;
97
+        }
98
+
99
+        // Only check type of optional variables if they're set, and don't
100
+        // mark them as bad if they're not set
101
+        if (strpos($key, " (optional)") !== false) {
102
+            $key = str_replace(" (optional)", "", $key);
103
+            if (empty($VARS[$key])) {
104
+                continue;
105
+            }
106
+        } else {
107
+            if (empty($VARS[$key])) {
108
+                $ok[$key] = false;
109
+                continue;
110
+            }
111
+        }
112
+
113
+        if (strpos($val, "/") === 0) {
114
+            // regex
115
+            $ok[$key] = preg_match($val, $VARS[$key]) === 1;
116
+        } else {
117
+            $checkmethod = "is_$val";
118
+            $ok[$key] = !($checkmethod($VARS[$key]) !== true);
119
+        }
120
+    }
121
+    if ($or) {
122
+        $success = false;
123
+        $bad = "";
124
+        foreach ($ok as $k => $v) {
125
+            if ($v) {
126
+                $success = true;
127
+                break;
128
+            } else {
129
+                $bad = $k;
130
+            }
131
+        }
132
+        if (!$success) {
133
+            http_response_code(400);
134
+            die("400 Bad request: variable $bad is missing or invalid");
135
+        }
136
+    } else {
137
+        foreach ($ok as $key => $bool) {
138
+            if (!$bool) {
139
+                http_response_code(400);
140
+                die("400 Bad request: variable $key is missing or invalid");
141
+            }
142
+        }
143
+    }
144
+}

+ 79
- 0
api/index.php View File

@@ -0,0 +1,79 @@
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
+require __DIR__ . '/../required.php';
10
+require __DIR__ . '/functions.php';
11
+require __DIR__ . '/apisettings.php';
12
+
13
+$VARS = $_GET;
14
+if ($_SERVER['REQUEST_METHOD'] != "GET") {
15
+    $VARS = array_merge($VARS, $_POST);
16
+}
17
+
18
+$requestbody = file_get_contents('php://input');
19
+$requestjson = json_decode($requestbody, TRUE);
20
+if (json_last_error() == JSON_ERROR_NONE) {
21
+    $VARS = array_merge($VARS, $requestjson);
22
+}
23
+
24
+// If we're not using the old api.php file, allow more flexible requests
25
+if (strpos($_SERVER['REQUEST_URI'], "/api.php") === FALSE) {
26
+    $route = explode("/", substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], "api/") + 4));
27
+
28
+    if (count($route) >= 1) {
29
+        $VARS["action"] = $route[0];
30
+    }
31
+    if (count($route) >= 2 && strpos($route[1], "?") !== 0) {
32
+        for ($i = 1; $i < count($route); $i++) {
33
+            if (empty($route[$i]) || strpos($route[$i], "=") === false) {
34
+                continue;
35
+            }
36
+            $key = explode("=", $route[$i], 2)[0];
37
+            $val = explode("=", $route[$i], 2)[1];
38
+            $VARS[$key] = $val;
39
+        }
40
+    }
41
+
42
+    if (strpos($route[count($route) - 1], "?") === 0) {
43
+        $morevars = explode("&", substr($route[count($route) - 1], 1));
44
+        foreach ($morevars as $var) {
45
+            $key = explode("=", $var, 2)[0];
46
+            $val = explode("=", $var, 2)[1];
47
+            $VARS[$key] = $val;
48
+        }
49
+    }
50
+}
51
+
52
+if (!authenticate()) {
53
+    header('WWW-Authenticate: Basic realm="' . $SETTINGS['site_title'] . '"');
54
+    header('HTTP/1.1 401 Unauthorized');
55
+    die("401 Unauthorized: you need to supply valid credentials.");
56
+}
57
+
58
+if (empty($VARS['action'])) {
59
+    http_response_code(404);
60
+    die("404 No action specified");
61
+}
62
+
63
+if (!isset($APIS[$VARS['action']])) {
64
+    http_response_code(404);
65
+    die("404 Action not defined");
66
+}
67
+
68
+$APIACTION = $APIS[$VARS["action"]];
69
+
70
+if (!file_exists(__DIR__ . "/actions/" . $APIACTION["load"])) {
71
+    http_response_code(404);
72
+    die("404 Action not found");
73
+}
74
+
75
+if (!empty($APIACTION["vars"])) {
76
+    checkVars($APIACTION["vars"]);
77
+}
78
+
79
+require_once __DIR__ . "/actions/" . $APIACTION["load"];

+ 34
- 28
app.php View File

@@ -1,5 +1,4 @@
1 1
 <?php
2
-
3 2
 /* This Source Code Form is subject to the terms of the Mozilla Public
4 3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
5 4
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@@ -14,7 +13,7 @@ if ($_SESSION['loggedin'] != true) {
14 13
 require_once __DIR__ . "/pages.php";
15 14
 
16 15
 $pageid = "home";
17
-if (isset($_GET['page']) && !is_empty($_GET['page'])) {
16
+if (!empty($_GET['page'])) {
18 17
     $pg = strtolower($_GET['page']);
19 18
     $pg = preg_replace('/[^0-9a-z_]/', "", $pg);
20 19
     if (array_key_exists($pg, PAGES) && file_exists(__DIR__ . "/pages/" . $pg . ".php")) {
@@ -40,7 +39,7 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
40 39
         <meta http-equiv="X-UA-Compatible" content="IE=edge">
41 40
         <meta name="viewport" content="width=device-width, initial-scale=1">
42 41
 
43
-        <title><?php echo SITE_TITLE; ?></title>
42
+        <title><?php echo $SETTINGS['site_title']; ?></title>
44 43
 
45 44
         <link rel="icon" href="static/img/logo.svg">
46 45
 
@@ -66,28 +65,35 @@ header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", fals
66 65
 
67 66
         <?php
68 67
 // Alert messages
69
-        if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
70
-            // optional string generation argument
71
-            if (!isset($_GET['arg']) || is_empty($_GET['arg'])) {
72
-                $alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
68
+        if (!empty($_GET['msg'])) {
69
+            if (array_key_exists($_GET['msg'], MESSAGES)) {
70
+                // optional string generation argument
71
+                if (empty($_GET['arg'])) {
72
+                    $alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
73
+                } else {
74
+                    $alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
75
+                }
76
+                $alerttype = MESSAGES[$_GET['msg']]['type'];
77
+                $alerticon = "square-o";
78
+                switch (MESSAGES[$_GET['msg']]['type']) {
79
+                    case "danger":
80
+                        $alerticon = "times";
81
+                        break;
82
+                    case "warning":
83
+                        $alerticon = "exclamation-triangle";
84
+                        break;
85
+                    case "info":
86
+                        $alerticon = "info-circle";
87
+                        break;
88
+                    case "success":
89
+                        $alerticon = "check";
90
+                        break;
91
+                }
73 92
             } else {
74
-                $alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
75
-            }
76
-            $alerttype = MESSAGES[$_GET['msg']]['type'];
77
-            $alerticon = "square-o";
78
-            switch (MESSAGES[$_GET['msg']]['type']) {
79
-                case "danger":
80
-                    $alerticon = "times";
81
-                    break;
82
-                case "warning":
83
-                    $alerticon = "exclamation-triangle";
84
-                    break;
85
-                case "info":
86
-                    $alerticon = "info-circle";
87
-                    break;
88
-                case "success":
89
-                    $alerticon = "check";
90
-                    break;
93
+                // We don't have a message for this, so just assume an error and escape stuff.
94
+                $alertmsg = htmlentities($Strings->get($_GET['msg'], false));
95
+                $alerticon = "times";
96
+                $alerttype = "danger";
91 97
             }
92 98
             echo <<<END
93 99
             <div class="row justify-content-center" id="msg-alert-box">
@@ -121,7 +127,7 @@ END;
121 127
             </button>
122 128
             <a class="navbar-brand py-0 mr-auto" href="app.php">
123 129
                 <img src="static/img/logo.svg" alt="" class="d-none d-<?php echo $navbar_breakpoint; ?>-inline brand-img py-0" />
124
-                <?php echo SITE_TITLE; ?>
130
+                <?php echo $SETTINGS['site_title']; ?>
125 131
             </a>
126 132
 
127 133
             <div class="collapse navbar-collapse py-0" id="navbar-collapse">
@@ -157,7 +163,7 @@ END;
157 163
                 </div>
158 164
                 <div class="navbar-nav ml-auto py-0" id="navbar-right">
159 165
                     <span class="nav-item py-<?php echo $navbar_breakpoint; ?>-0">
160
-                        <a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="<?php echo PORTAL_URL; ?>">
166
+                        <a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="<?php echo $SETTINGS['accounthub']['home']; ?>">
161 167
                             <i class="fas fa-user fa-fw"></i><span>&nbsp;<?php echo $_SESSION['realname'] ?></span>
162 168
                         </a>
163 169
                     </span>
@@ -177,8 +183,8 @@ END;
177 183
                 ?>
178 184
             </div>
179 185
             <div class="footer">
180
-                <?php echo FOOTER_TEXT; ?><br />
181
-                Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
186
+                <?php echo $SETTINGS['footer_text']; ?><br />
187
+                Copyright &copy; <?php echo date('Y'); ?> <?php echo $SETTINGS['copyright']; ?>
182 188
             </div>
183 189
         </div>
184 190
         <script src="static/js/jquery-3.3.1.min.js"></script>

+ 2
- 2
image.php View File

@@ -8,7 +8,7 @@
8 8
 
9 9
 require_once __DIR__ . "/required.php";
10 10
 
11
-$base = FILE_UPLOAD_PATH . "/";
11
+$base = $SETTINGS['file_upload_path'] . "/";
12 12
 if (isset($_GET['i'])) {
13 13
     $file = $_GET['i'];
14 14
     $filepath = $base . $file;
@@ -16,7 +16,7 @@ if (isset($_GET['i'])) {
16 16
         http_response_code(404);
17 17
         die("404 File Not Found");
18 18
     }
19
-    if (strpos(realpath($filepath), FILE_UPLOAD_PATH) !== 0) {
19
+    if (strpos(realpath($filepath), $SETTINGS['file_upload_path']) !== 0) {
20 20
         http_response_code(404);
21 21
         die("404 File Not Found");
22 22
     }

+ 111
- 155
index.php View File

@@ -1,175 +1,131 @@
1 1
 <?php
2
-/* This Source Code Form is subject to the terms of the Mozilla Public
2
+/*
3
+ * This Source Code Form is subject to the terms of the Mozilla Public
3 4
  * License, v. 2.0. If a copy of the MPL was not distributed with this
4
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
+ */
5 7
 
6 8
 require_once __DIR__ . "/required.php";
7 9
 
8 10
 // if we're logged in, we don't need to be here.
9 11
 if (!empty($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && !isset($_GET['permissionerror'])) {
10 12
     header('Location: app.php');
13
+    die();
11 14
 }
12 15
 
13
-if (isset($_GET['permissionerror'])) {
14
-    $alert = $Strings->get("no access permission", false);
16
+/**
17
+ * Show a simple HTML page with a line of text and a button.  Matches the UI of
18
+ * the AccountHub login flow.
19
+ *
20
+ * @global type $SETTINGS
21
+ * @global type $SECURE_NONCE
22
+ * @global type $Strings
23
+ * @param string $title Text to show, passed through i18n
24
+ * @param string $button Button text, passed through i18n
25
+ * @param string $url URL for the button
26
+ */
27
+function showHTML(string $title, string $button, string $url) {
28
+    global $SETTINGS, $SECURE_NONCE, $Strings;
29
+    ?>
30
+    <!DOCTYPE html>
31
+    <meta charset="UTF-8">
32
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
33
+    <meta name="viewport" content="width=device-width, initial-scale=1">
34
+
35
+    <title><?php echo $SETTINGS['site_title']; ?></title>
36
+
37
+    <link rel="icon" href="static/img/logo.svg">
38
+
39
+    <link href="static/css/bootstrap.min.css" rel="stylesheet">
40
+    <style nonce="<?php echo $SECURE_NONCE; ?>">
41
+        .display-5 {
42
+            font-size: 2.5rem;
43
+            font-weight: 300;
44
+            line-height: 1.2;
45
+        }
46
+
47
+        .banner-image {
48
+            max-height: 100px;
49
+            margin: 2em auto;
50
+            border: 1px solid grey;
51
+            border-radius: 15%;
52
+        }
53
+    </style>
54
+
55
+    <div class="container mt-4">
56
+        <div class="row justify-content-center">
57
+            <div class="col-12 text-center">
58
+                <img class="banner-image" src="./static/img/logo.svg" />
59
+            </div>
60
+
61
+            <div class="col-12 text-center">
62
+                <h1 class="display-5 mb-4"><?php $Strings->get($title); ?></h1>
63
+            </div>
64
+
65
+            <div class="col-12 col-sm-8 col-lg-6">
66
+                <div class="card mt-4">
67
+                    <div class="card-body">
68
+                        <a href="<?php echo $url; ?>" class="btn btn-primary btn-block"><?php $Strings->get($button); ?></a>
69
+                    </div>
70
+                </div>
71
+            </div>
72
+        </div>
73
+    </div>
74
+    <?php
15 75
 }
16 76
 
17
-/* Authenticate user */
18
-$userpass_ok = false;
19
-$multiauth = false;
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) {
30
-                    case "LOCKED_OR_DISABLED":
31
-                        $alert = $Strings->get("account locked", false);
32
-                        break;
33
-                    case "TERMINATED":
34
-                        $alert = $Strings->get("account terminated", false);
35
-                        break;
36
-                    case "CHANGE_PASSWORD":
37
-                        $alert = $Strings->get("password expired", false);
38
-                        break;
39
-                    case "NORMAL":
40
-                        $username_ok = true;
41
-                        break;
42
-                    case "ALERT_ON_ACCESS":
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
-                        }
55
-                        break;
56
-                }
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
-                        }
67
-                    } else {
68
-                        $alert = $Strings->get("login incorrect", false);
69
-                    }
77
+if (!empty($_GET['logout'])) {
78
+    showHTML("You have been logged out.", "Log in again", "./index.php");
79
+    die();
80
+}
81
+if (empty($_SESSION["login_code"])) {
82
+    $redirecttologin = true;
83
+} else {
84
+    try {
85
+        $uidinfo = AccountHubApi::get("checkloginkey", ["code" => $_SESSION["login_code"]]);
86
+        if ($uidinfo["status"] == "ERROR") {
87
+            throw new Exception();
88
+        }
89
+        if (is_numeric($uidinfo['uid'])) {
90
+            $user = new User($uidinfo['uid'] * 1);
91
+            foreach ($SETTINGS['permissions'] as $perm) {
92
+                if (!$user->hasPermission($perm)) {
93
+                    showHTML("no access permission", "sign out", "./action.php?action=signout");
94
+                    die();
70 95
                 }
71
-            } else { // User does not exist anywhere
72
-                $alert = $Strings->get("login incorrect", false);
73 96
             }
74
-        } else {
75
-            $alert = $Strings->get("captcha error", false);
76
-        }
77
-    } else if ($VARS['progress'] == "2") {
78
-        $user = User::byUsername($VARS['username']);
79
-        if ($_SESSION['passok'] !== true) {
80
-            // stop logins using only username and authcode
81
-            sendError("Password integrity check failed!");
82
-        }
83
-        if ($user->check2fa($VARS['authcode'])) {
84 97
             Session::start($user);
98
+            $_SESSION["login_code"] = null;
85 99
             header('Location: app.php');
86
-            die("Logged in, go to app.php");
100
+            showHTML("Logged in", "Continue", "./app.php");
101
+            die();
87 102
         } else {
88
-            $alert = $Strings->get("2fa incorrect", false);
103
+            throw new Exception();
89 104
         }
105
+    } catch (Exception $ex) {
106
+        $redirecttologin = true;
107
+    }
108
+}
109
+
110
+if ($redirecttologin) {
111
+    try {
112
+        $urlbase = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "");
113
+        $iconurl = $urlbase . str_replace("index.php", "", $_SERVER["REQUEST_URI"]) . "static/img/logo.svg";
114
+        $codedata = AccountHubApi::get("getloginkey", ["appname" => $SETTINGS["site_title"], "appicon" => $iconurl]);
115
+
116
+        if ($codedata['status'] != "OK") {
117
+            throw new Exception($Strings->get("login server unavailable", false));
118
+        }
119
+
120
+        $redirecturl = $urlbase . $_SERVER['REQUEST_URI'];
121
+
122
+        $_SESSION["login_code"] = $codedata["code"];
123
+
124
+        $locationurl = $codedata["loginurl"] . "?code=" . htmlentities($codedata["code"]) . "&redirect=" . htmlentities($redirecturl);
125
+        header("Location: $locationurl");
126
+        showHTML("Continue", "Continue", $locationurl);
127
+        die();
128
+    } catch (Exception $ex) {
129
+        sendError($ex->getMessage());
90 130
     }
91
-} else {
92
-    $alert = $Strings->get("login server unavailable", false);
93 131
 }
94
-header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
95
-header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
96
-header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
97
-header("Link: <static/css/index.css>; rel=preload; as=style", false);
98
-header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
99
-header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", false);
100
-?>
101
-<!DOCTYPE html>
102
-<html>
103
-    <head>
104
-        <meta charset="UTF-8">
105
-        <meta http-equiv="X-UA-Compatible" content="IE=edge">
106
-        <meta name="viewport" content="width=device-width, initial-scale=1">
107
-
108
-        <title><?php echo SITE_TITLE; ?></title>
109
-
110
-        <link rel="icon" href="static/img/logo.svg">
111
-
112
-        <link href="static/css/bootstrap.min.css" rel="stylesheet">
113
-        <link href="static/css/material-color/material-color.min.css" rel="stylesheet">
114
-        <link href="static/css/index.css" rel="stylesheet">
115
-        <?php if (CAPTCHA_ENABLED) { ?>
116
-            <script src="<?php echo CAPTCHA_SERVER ?>/captcheck.dist.js"></script>
117
-        <?php } ?>
118
-    </head>
119
-    <body>
120
-        <div class="row justify-content-center">
121
-            <div class="col-auto">
122
-                <img class="banner-image" src="static/img/logo.svg" />
123
-            </div>
124
-        </div>
125
-        <div class="row justify-content-center">
126
-            <div class="card col-11 col-xs-11 col-sm-8 col-md-6 col-lg-4">
127
-                <div class="card-body">
128
-                    <h5 class="card-title"><?php $Strings->get("sign in"); ?></h5>
129
-                    <form action="" method="POST">
130
-                        <?php
131
-                        if (!empty($alert)) {
132
-                            ?>
133
-                            <div class="alert alert-danger">
134
-                                <i class="fa fa-fw fa-exclamation-triangle"></i> <?php echo $alert; ?>
135
-                            </div>
136
-                            <?php
137
-                        }
138
-
139
-                        if ($multiauth != true) {
140
-                            ?>
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 />
143
-                            <?php if (CAPTCHA_ENABLED) { ?>
144
-                                <div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
145
-                                <br />
146
-                            <?php } ?>
147
-                            <input type="hidden" name="progress" value="1" />
148
-                            <?php
149
-                        } else if ($multiauth) {
150
-                            ?>
151
-                            <div class="alert alert-info">
152
-                                <?php $Strings->get("2fa prompt"); ?>
153
-                            </div>
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 />
155
-                            <input type="hidden" name="progress" value="2" />
156
-                            <input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
157
-                            <?php
158
-                        }
159
-                        ?>
160
-                        <button type="submit" class="btn btn-primary">
161
-                            <?php $Strings->get("continue"); ?>
162
-                        </button>
163
-                    </form>
164
-                </div>
165
-            </div>
166
-        </div>
167
-        <div class="footer">
168
-            <?php echo FOOTER_TEXT; ?><br />
169
-            Copyright &copy; <?php echo date('Y'); ?> <?php echo COPYRIGHT_NAME; ?>
170
-        </div>
171
-    </div>
172
-    <script src="static/js/jquery-3.3.1.min.js"></script>
173
-    <script src="static/js/bootstrap.bundle.min.js"></script>
174
-</body>
175
-</html>

+ 0
- 114
lang/en_us.php View File

@@ -1,114 +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
-    "no permission" => "You do not have permission to access this system.",
23
-    "sign out" => "Sign out",
24
-    "settings" => "Settings",
25
-    "options" => "Options",
26
-    "404 error" => "404 Error",
27
-    "page not found" => "Page not found.",
28
-    "invalid parameters" => "Invalid request parameters.",
29
-    "login server error" => "The login server returned an error: {arg}",
30
-    "login server user data error" => "The login server refused to provide account information.  Try again or contact technical support.",
31
-    "captcha error" => "There was a problem with the CAPTCHA (robot test).  Try again.",
32
-    "no edit permission" => "You do not have permission to modify records.",
33
-    "no access permission" => "You do not have permission to access this system.",
34
-    "home" => "Home",
35
-    "more" => "More",
36
-    "invalid itemid" => "The item ID is invalid.",
37
-    "invalid category" => "The category is invalid.",
38
-    "invalid location" => "The location does not exist.",
39
-    "item saved" => "Item saved.",
40
-    "item deleted" => "Item deleted.",
41
-    "total items" => "Total Items",
42
-    "items" => "Items",
43
-    "locations" => "Locations",
44
-    "categories" => "Categories",
45
-    "actions" => "Actions",
46
-    "name" => "Name",
47
-    "category" => "Category",
48
-    "location" => "Location",
49
-    "code" => "Code",
50
-    "code 1" => "Code 1",
51
-    "code 2" => "Code 2",
52
-    "qty" => "Qty",
53
-    "quantity" => "Quantity",
54
-    "assigned to" => "Assigned To",
55
-    "view items" => "View Items",
56
-    "nobody" => "Nobody",
57
-    "view categories" => "View Categories",
58
-    "view locations" => "View Locations",
59
-    "edit" => "Edit",
60
-    "clone" => "Copy",
61
-    "item count" => "Items",
62
-    "delete" => "Delete",
63
-    "new item" => "New Item",
64
-    "editing item" => "Editing {item}",
65
-    "editing category" => "Editing {cat}",
66
-    "editing location" => "Editing {loc}",
67
-    "cloning item" => "Copying {oitem} <i class=\"fa fa-angle-right\"></i> {nitem}",
68
-    "adding item" => "Adding new item",
69
-    "adding category" => "Adding new category",
70
-    "invalid catid" => "Invalid category ID.",
71
-    "category deleted" => "Category deleted.",
72
-    "category in use" => "Cannot delete category because there is at least one item still in it.",
73
-    "new category" => "New Category",
74
-    "category saved" => "Category saved.",
75
-    "adding location" => "Adding new location",
76
-    "invalid locid" => "Invalid location ID.",
77
-    "location deleted" => "Location deleted.",
78
-    "location in use" => "Cannot delete location because there is at least one item still in it.",
79
-    "new location" => "New Location",
80
-    "location saved" => "Location saved.",
81
-    "name" => "Name",
82
-    "save" => "Save",
83
-    "placeholder item name" => "Foo Bar",
84
-    "placeholder category name" => "Widgets",
85
-    "placeholder location name" => "Over the Hills",
86
-    "description" => "Description",
87
-    "notes" => "Notes",
88
-    "comments" => "Comments",
89
-    "minwant" => "Minimum On Hand",
90
-    "want" => "Need",
91
-    "field not a number" => "You entered something that isn't a number when a number was expected.",
92
-    "understocked items" => "Understocked Items",
93
-    "view understocked" => "View Understocked",
94
-    "only showing understocked" => "Only showing understocked items.",
95
-    "show all items" => "Show all items",
96
-    "missing name" => "You need to enter a name.",
97
-    "use the dropdowns" => "Whoops, you need to use the category and location autocomplete boxes.",
98
-    "make categories and locations" => "Please create at least one category and location before adding an item.",
99
-    "search" => "Search Items",
100
-    "report export" => "Reports/Export",
101
-    "report type" => "Report type",
102
-    "format" => "Format",
103
-    "generate report" => "Generate report",
104
-    "choose an option" => "Choose an option",
105
-    "csv file" => "CSV text file",
106
-    "ods file" => "ODS spreadsheet",
107
-    "html file" => "HTML web page",
108
-    "itemid" => "Item ID",
109
-    "id" => "ID",
110
-    "item cost" => "Item cost",
111
-    "sale price" => "Sale price",
112
-    "cost" => "Cost",
113
-    "price" => "Price"
114
-]);

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

@@ -1,21 +1,5 @@
1 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 2
     "sign out": "Sign out",
17
-    "settings": "Settings",
18
-    "options": "Options",
19 3
     "404 error": "404 Error",
20 4
     "page not found": "Page not found.",
21 5
     "invalid parameters": "Invalid request parameters.",

+ 3
- 1
langs/en/images.json View File

@@ -6,5 +6,7 @@
6 6
     "Promoted": "Promoted",
7 7
     "Promote": "Promote",
8 8
     "Delete": "Delete",
9
-    "Back": "Back"
9
+    "Back": "Back",
10
+    "Image uploaded.": "Image uploaded.",
11
+    "Upload finished with errors: {arg}": "Upload finished with errors: {arg}"
10 12
 }

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

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

+ 8
- 0
langs/messages.php View File

@@ -88,5 +88,13 @@ define("MESSAGES", [
88 88
     "noloccat" => [
89 89
         "string" => "make categories and locations",
90 90
         "type" => "info"
91
+    ],
92
+    "upload_warning" => [
93
+        "string" => "Upload finished with errors: {arg}",
94
+        "type" => "warning"
95
+    ],
96
+    "upload_success" => [
97
+        "string" => "Image uploaded.",
98
+        "type" => "success"
91 99
     ]
92 100
 ]);

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

@@ -0,0 +1,56 @@
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 AccountHubApi {
10
+
11
+    public static function get(string $action, array $data = null, bool $throwex = false) {
12
+        global $SETTINGS;
13
+
14
+        $content = [
15
+            "action" => $action,
16
+            "key" => $SETTINGS['accounthub']['key']
17
+        ];
18
+        if (!is_null($data)) {
19
+            $content = array_merge($content, $data);
20
+        }
21
+        $options = [
22
+            'http' => [
23
+                'method' => 'POST',
24
+                'content' => json_encode($content),
25
+                'header' => "Content-Type: application/json\r\n" .
26
+                "Accept: application/json\r\n",
27
+                "ignore_errors" => true
28
+            ]
29
+        ];
30
+
31
+        $context = stream_context_create($options);
32
+        $result = file_get_contents($SETTINGS['accounthub']['api'], false, $context);
33
+        $response = json_decode($result, true);
34
+        if ($result === false || !AccountHubApi::checkHttpRespCode($http_response_header) || json_last_error() != JSON_ERROR_NONE) {
35
+            if ($throwex) {
36
+                throw new Exception($result);
37
+            } else {
38
+                sendError($result);
39
+            }
40
+        }
41
+        return $response;
42
+    }
43
+
44
+    private static function checkHttpRespCode(array $headers): bool {
45
+        foreach ($headers as $header) {
46
+            if (preg_match("/HTTP\/[0-9]\.[0-9] [0-9]{3}.*/", $header)) {
47
+                $respcode = explode(" ", $header)[1] * 1;
48
+                if ($respcode >= 200 && $respcode < 300) {
49
+                    return true;
50
+                }
51
+            }
52
+        }
53
+        return false;
54
+    }
55
+
56
+}

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

@@ -0,0 +1,275 @@
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 FormBuilder {
10
+
11
+    private $items = [];
12
+    private $hiddenitems = [];
13
+    private $title = "";
14
+    private $icon = "";
15
+    private $buttons = [];
16
+    private $action = "action.php";
17
+    private $method = "POST";
18
+    private $id = "editform";
19
+
20
+    /**
21
+     * Create a form with autogenerated HTML.
22
+     *
23
+     * @param string $title Form title/heading
24
+     * @param string $icon FontAwesone icon next to the title.
25
+     * @param string $action URL to submit the form to.
26
+     * @param string $method Form submission method (POST, GET, etc.)
27
+     */
28
+    public function __construct(string $title = "Untitled Form", string $icon = "fas fa-file-alt", string $action = "action.php", string $method = "POST") {
29
+        $this->title = $title;
30
+        $this->icon = $icon;
31
+        $this->action = $action;
32
+        $this->method = $method;
33
+    }
34
+
35
+    /**
36
+     * Set the title of the form.
37
+     * @param string $title
38
+     */
39
+    public function setTitle(string $title) {
40
+        $this->title = $title;
41
+    }
42
+
43
+    /**
44
+     * Set the icon for the form.
45
+     * @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
46
+     */
47
+    public function setIcon(string $icon) {
48
+        $this->icon = $icon;
49
+    }
50
+
51
+    /**
52
+     * Set the URL the form will submit to.
53
+     * @param string $action
54
+     */
55
+    public function setAction(string $action) {
56
+        $this->action = $action;
57
+    }
58
+
59
+    /**
60
+     * Set the form submission method (GET, POST, etc)
61
+     * @param string $method
62
+     */
63
+    public function setMethod(string $method = "POST") {
64
+        $this->method = $method;
65
+    }
66
+
67
+    /**
68
+     * Set the form ID.
69
+     * @param string $id
70
+     */
71
+    public function setID(string $id = "editform") {
72
+        $this->id = $id;
73
+    }
74
+
75
+    /**
76
+     * Add an input to the form.
77
+     *
78
+     * @param string $name Element name
79
+     * @param string $value Element value
80
+     * @param string $type Input type (text, number, date, select, tel...)
81
+     * @param bool $required If the element is required for form submission.
82
+     * @param string $id Element ID
83
+     * @param array $options Array of [value => text] pairs for a select element
84
+     * @param string $label Text label to display near the input
85
+     * @param string $icon FontAwesome icon (example: "fas fa-toilet-paper")
86
+     * @param int $width Bootstrap column width for the input, out of 12.
87
+     * @param int $minlength Minimum number of characters for the input.
88
+     * @param int $maxlength Maximum number of characters for the input.
89
+     * @param string $pattern Regex pattern for custom client-side validation.
90
+     * @param string $error Message to show if the input doesn't validate.
91
+     */
92
+    public function addInput(string $name, string $value = "", string $type = "text", bool $required = true, string $id = null, array $options = null, string $label = "", string $icon = "", int $width = 4, int $minlength = 1, int $maxlength = 100, string $pattern = "", string $error = "") {
93
+        $item = [
94
+            "name" => $name,
95
+            "value" => $value,
96
+            "type" => $type,
97
+            "required" => $required,
98
+            "label" => $label,
99
+            "icon" => $icon,
100
+            "width" => $width,
101
+            "minlength" => $minlength,
102
+            "maxlength" => $maxlength
103
+        ];
104
+        if (!empty($id)) {
105
+            $item["id"] = $id;
106
+        }
107
+        if (!empty($options) && $type == "select") {
108
+            $item["options"] = $options;
109
+        }
110
+        if (!empty($pattern)) {
111
+            $item["pattern"] = $pattern;
112
+        }
113
+        if (!empty($error)) {
114
+            $item["error"] = $error;
115
+        }
116
+        $this->items[] = $item;
117
+    }
118
+
119
+    /**
120
+     * Add a button to the form.
121
+     *
122
+     * @param string $text Text string to show on the button.
123
+     * @param string $icon FontAwesome icon to show next to the text.
124
+     * @param string $href If not null, the button will actually be a hyperlink.
125
+     * @param string $type Usually "button" or "submit".  Ignored if $href is set.
126
+     * @param string $id The element ID.
127
+     * @param string $name The element name for the button.
128
+     * @param string $value The form value for the button. Ignored if $name is null.
129
+     * @param string $class The CSS classes for the button, if a standard success-colored one isn't right.
130
+     */
131
+    public function addButton(string $text, string $icon = "", string $href = null, string $type = "button", string $id = null, string $name = null, string $value = "", string $class = "btn btn-success") {
132
+        $button = [
133
+            "text" => $text,
134
+            "icon" => $icon,
135
+            "class" => $class,
136
+            "type" => $type,
137
+            "id" => $id,
138
+            "href" => $href,
139
+            "name" => $name,
140
+            "value" => $value
141
+        ];
142
+        $this->buttons[] = $button;
143
+    }
144
+
145
+    /**
146
+     * Add a hidden input.
147
+     * @param string $name
148
+     * @param string $value
149
+     */
150
+    public function addHiddenInput(string $name, string $value) {
151
+        $this->hiddenitems[$name] = $value;
152
+    }
153
+
154
+    /**
155
+     * Generate the form HTML.
156
+     * @param bool $echo If false, returns HTML string instead of outputting it.
157
+     */
158
+    public function generate(bool $echo = true) {
159
+        $html = <<<HTMLTOP
160
+<form action="$this->action" method="$this->method" id="$this->id">
161
+    <div class="card">
162
+         <h3 class="card-header d-flex">
163
+            <div>
164
+                <i class="$this->icon"></i> $this->title
165
+            </div>
166
+        </h3>
167
+
168
+        <div class="card-body">
169
+            <div class="row">
170
+HTMLTOP;
171
+
172
+        foreach ($this->items as $item) {
173
+            $required = $item["required"] ? "required" : "";
174
+            $id = empty($item["id"]) ? "" : "id=\"$item[id]\"";
175
+            $pattern = empty($item["pattern"]) ? "" : "pattern=\"$item[pattern]\"";
176
+            if (empty($item['type'])) {
177
+                $item['type'] = "text";
178
+            }
179
+            $itemhtml = "";
180
+            $itemlabel = "";
181
+            if ($item['type'] != "checkbox") {
182
+                $itemlabel = "<label class=\"mb-0\">$item[label]:</label>";
183
+            }
184
+            $strippedlabel = strip_tags($item['label']);
185
+            $itemhtml .= <<<ITEMTOP
186
+\n\n                <div class="col-12 col-md-$item[width]">
187
+                    <div class="form-group mb-3">
188
+                        $itemlabel
189
+                        <div class="input-group">
190
+                            <div class="input-group-prepend">
191
+                                <span class="input-group-text"><i class="$item[icon]"></i></span>
192
+                            </div>
193
+ITEMTOP;
194
+            switch ($item['type']) {
195
+                case "select":
196
+                    $itemhtml .= <<<SELECT
197
+\n                            <select class="form-control" name="$item[name]" aria-label="$strippedlabel" $required>
198
+SELECT;
199
+                    foreach ($item['options'] as $value => $label) {
200
+                        $selected = "";
201
+                        if (!empty($item['value']) && $value == $item['value']) {
202
+                            $selected = " selected";
203
+                        }
204
+                        $itemhtml .= "\n                                <option value=\"$value\"$selected>$label</option>";
205
+                    }
206
+                    $itemhtml .= "\n                            </select>";
207
+                    break;
208
+                case "checkbox":
209
+                    $itemhtml .= <<<CHECKBOX
210
+\n                            <div class="form-group form-check">
211
+                                <input type="checkbox" name="$item[name]" $id class="form-check-input" value="$item[value]" $required aria-label="$strippedlabel">
212
+                                <label class="form-check-label">$item[label]</label>
213
+                              </div>
214
+CHECKBOX;
215
+                    break;
216
+                default:
217
+                    $itemhtml .= <<<INPUT
218
+\n                            <input type="$item[type]" name="$item[name]" $id class="form-control" aria-label="$strippedlabel" minlength="$item[minlength]" maxlength="$item[maxlength]" $pattern value="$item[value]" $required />
219
+INPUT;
220
+                    break;
221
+            }
222
+
223
+            if (!empty($item["error"])) {
224
+                $itemhtml .= <<<ERROR
225
+\n                            <div class="invalid-feedback">
226
+                                $item[error]
227
+                            </div>
228
+ERROR;
229
+            }
230
+            $itemhtml .= <<<ITEMBOTTOM
231
+\n                        </div>
232
+                    </div>
233
+                </div>\n
234
+ITEMBOTTOM;
235
+            $html .= $itemhtml;
236
+        }
237
+
238
+        $html .= <<<HTMLBOTTOM
239
+
240
+            </div>
241
+        </div>
242
+HTMLBOTTOM;
243
+
244
+        if (!empty($this->buttons)) {
245
+            $html .= "\n        <div class=\"card-footer\">";
246
+            foreach ($this->buttons as $btn) {
247
+                $btnhtml = "";
248
+                $inner = "<i class=\"$btn[icon]\"></i> $btn[text]";
249
+                $id = empty($btn['id']) ? "" : "id=\"$btn[id]\"";
250
+                if (!empty($btn['href'])) {
251
+                    $btnhtml = "<a href=\"$btn[href]\" class=\"$btn[class]\" $id>$inner</a>";
252
+                } else {
253
+                    $name = empty($btn['name']) ? "" : "name=\"$btn[name]\"";
254
+                    $value = (!empty($btn['name']) && !empty($btn['value'])) ? "value=\"$btn[value]\"" : "";
255
+                    $btnhtml = "<button type=\"$btn[type]\" class=\"$btn[class]\" $id $name $value>$inner</button>";
256
+                }
257
+                $html .= "\n            $btnhtml";
258
+            }
259
+            $html .= "\n        </div>";
260
+        }
261
+
262
+        $html .= "\n    </div>";
263
+        foreach ($this->hiddenitems as $name => $value) {
264
+            $value = htmlentities($value);
265
+            $html .= "\n    <input type=\"hidden\" name=\"$name\" value=\"$value\" />";
266
+        }
267
+        $html .= "\n</form>\n";
268
+
269
+        if ($echo) {
270
+            echo $html;
271
+        }
272
+        return $html;
273
+    }
274
+
275
+}

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

@@ -45,50 +45,13 @@ class Login {
45 45
         return Login::LOGIN_OK;
46 46
     }
47 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 48
     /**
72 49
      * Check the login server API for sanity
73 50
      * @return boolean true if OK, else false
74 51
      */
75 52
     public static function checkLoginServer() {
76 53
         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);
54
+            $resp = AccountHubApi::get("ping");
92 55
             if ($resp['status'] == "OK") {
93 56
                 return true;
94 57
             } else {
@@ -107,19 +70,7 @@ class Login {
107 70
      */
108 71
     function checkAPIKey($key) {
109 72
         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
-            }
73
+            $resp = AccountHubApi::get("ping", null, true);
123 74
             return false;
124 75
         } catch (Exception $e) {
125 76
             return false;

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

@@ -32,27 +32,15 @@ class Notifications {
32 32
                 $timestamp = date("Y-m-d H:i:s", strtotime($timestamp));
33 33
             }
34 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);
35
+            $resp = AccountHubApi::get("addnotification", [
36
+                        'uid' => $user->getUID(),
37
+                        'title' => $title,
38
+                        'content' => $content,
39
+                        'timestamp' => $timestamp,
40
+                        'url' => $url,
41
+                        'sensitive' => $sensitive
42
+                            ]
43
+            );
56 44
             if ($resp['status'] == "OK") {
57 45
                 return $resp['id'] * 1;
58 46
             } else {

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

@@ -21,6 +21,10 @@ class Strings {
21 21
 
22 22
         $this->load("en");
23 23
 
24
+        if ($language == "en") {
25
+            return;
26
+        }
27
+
24 28
         if (file_exists(__DIR__ . "/../langs/$language/")) {
25 29
             $this->language = $language;
26 30
             $this->load($language);

+ 13
- 146
lib/User.lib.php View File

@@ -17,22 +17,7 @@ class User {
17 17
 
18 18
     public function __construct(int $uid, string $username = "") {
19 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);
20
+        $resp = AccountHubApi::get("userexists", ["uid" => $uid]);
36 21
         if ($resp['status'] == "OK" && $resp['exists'] === true) {
37 22
             $this->exists = true;
38 23
         } else {
@@ -43,22 +28,7 @@ class User {
43 28
 
44 29
         if ($this->exists) {
45 30
             // 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);
31
+            $resp = AccountHubApi::get("userinfo", ["uid" => $uid]);
62 32
             if ($resp['status'] == "OK") {
63 33
                 $this->uid = $resp['data']['uid'] * 1;
64 34
                 $this->username = $resp['data']['username'];
@@ -71,22 +41,7 @@ class User {
71 41
     }
72 42
 
73 43
     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);
44
+        $resp = AccountHubApi::get("userinfo", ["username" => $username]);
90 45
         if (!isset($resp['status'])) {
91 46
             sendError("Login server error: " . $resp);
92 47
         }
@@ -105,22 +60,8 @@ class User {
105 60
         if (!$this->exists) {
106 61
             return false;
107 62
         }
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 63
 
123
-        $resp = json_decode($response->getBody(), TRUE);
64
+        $resp = AccountHubApi::get("hastotp", ['username' => $this->username]);
124 65
         if ($resp['status'] == "OK") {
125 66
             return $resp['otp'] == true;
126 67
         } else {
@@ -150,23 +91,7 @@ class User {
150 91
      * @return bool
151 92
      */
152 93
     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);
94
+        $resp = AccountHubApi::get("auth", ['username' => $this->username, 'password' => $password]);
170 95
         if ($resp['status'] == "OK") {
171 96
             return true;
172 97
         } else {
@@ -178,23 +103,8 @@ class User {
178 103
         if (!$this->has2fa) {
179 104
             return true;
180 105
         }
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 106
 
197
-        $resp = json_decode($response->getBody(), TRUE);
107
+        $resp = AccountHubApi::get("verifytotp", ['username' => $this->username, 'code' => $code]);
198 108
         if ($resp['status'] == "OK") {
199 109
             return $resp['valid'];
200 110
         } else {
@@ -209,23 +119,7 @@ class User {
209 119
      * @return boolean TRUE if the user has the permission (or admin access), else FALSE
210 120
      */
211 121
     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);
122
+        $resp = AccountHubApi::get("permission", ['username' => $this->username, 'code' => $code]);
229 123
         if ($resp['status'] == "OK") {
230 124
             return $resp['has_permission'];
231 125
         } else {
@@ -238,23 +132,7 @@ class User {
238 132
      * @return \AccountStatus
239 133
      */
240 134
     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);
135
+        $resp = AccountHubApi::get("acctstatus", ['username' => $this->username]);
258 136
         if ($resp['status'] == "OK") {
259 137
             return AccountStatus::fromString($resp['account']);
260 138
         } else {
@@ -262,24 +140,13 @@ class User {
262 140
         }
263 141
     }
264 142
 
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.";
143
+    function sendAlertEmail(string $appname = null) {
144
+        global $SETTINGS;
145
+        if (is_null($appname)) {
146
+            $appname = $SETTINGS['site_title'];
280 147
         }
148
+        $resp = AccountHubApi::get("alertemail", ['username' => $this->username, 'appname' => $SETTINGS['site_title']]);
281 149
 
282
-        $resp = json_decode($response->getBody(), TRUE);
283 150
         if ($resp['status'] == "OK") {
284 151
             return true;
285 152
         } else {

+ 1
- 1
lib/getitemtable.php View File

@@ -57,7 +57,7 @@ switch ($VARS['order'][0]['column']) {
57 57
 }
58 58
 
59 59
 // search
60
-if (!is_empty($VARS['search']['value'])) {
60
+if (!empty($VARS['search']['value'])) {
61 61
     $filter = true;
62 62
     $wherenolimit = [];
63 63
     if ($showwant) {

+ 17
- 45
mobile/index.php View File

@@ -8,10 +8,6 @@
8 8
  * Mobile app API
9 9
  */
10 10
 
11
-// The name of the permission needed to log in.
12
-// Set to null if you don't need it.
13
-$access_permission = "INV_VIEW";
14
-
15 11
 require __DIR__ . "/../required.php";
16 12
 
17 13
 header('Content-Type: application/json');
@@ -23,21 +19,7 @@ if ($VARS['action'] == "ping") {
23 19
 }
24 20
 
25 21
 function mobile_enabled() {
26
-    $client = new GuzzleHttp\Client();
27
-
28
-    $response = $client
29
-            ->request('POST', PORTAL_API, [
30
-        'form_params' => [
31
-            'key' => PORTAL_KEY,
32
-            'action' => "mobileenabled"
33
-        ]
34
-    ]);
35
-
36
-    if ($response->getStatusCode() > 299) {
37
-        return false;
38
-    }
39
-
40
-    $resp = json_decode($response->getBody(), TRUE);
22
+    $resp = AccountHubApi::get("mobileenabled");
41 23
     if ($resp['status'] == "OK" && $resp['mobile'] === TRUE) {
42 24
         return true;
43 25
     } else {
@@ -46,26 +28,15 @@ function mobile_enabled() {
46 28
 }
47 29
 
48 30
 function mobile_valid($username, $code) {
49
-    $client = new GuzzleHttp\Client();
50
-
51
-    $response = $client
52
-            ->request('POST', PORTAL_API, [
53
-        'form_params' => [
54
-            'key' => PORTAL_KEY,
55
-            "code" => $code,
56
-            "username" => $username,
57
-            'action' => "mobilevalid"
58
-        ]
59
-    ]);
31
+    try {
32
+        $resp = AccountHubApi::get("mobilevalid", ["code" => $code, "username" => $username], true);
60 33
 
61
-    if ($response->getStatusCode() > 299) {
62
-        return false;
63
-    }
64
-
65
-    $resp = json_decode($response->getBody(), TRUE);
66
-    if ($resp['status'] == "OK" && $resp['valid'] === TRUE) {
67
-        return true;
68
-    } else {
34
+        if ($resp['status'] == "OK" && $resp['valid'] === TRUE) {
35
+            return true;
36
+        } else {
37
+            return false;
38
+        }
39
+    } catch (Exception $ex) {
69 40
         return false;
70 41
     }
71 42
 }
@@ -75,7 +46,7 @@ if (mobile_enabled() !== TRUE) {
75 46
 }
76 47
 
77 48
 // Make sure we have a username and access key
78
-if (is_empty($VARS['username']) || is_empty($VARS['key'])) {
49
+if (empty($VARS['username']) || empty($VARS['key'])) {
79 50
     http_response_code(401);
80 51
     die(json_encode(["status" => "ERROR", "msg" => "Missing username and/or access key."]));
81 52
 }
@@ -95,13 +66,14 @@ switch ($VARS['action']) {
95 66
         if ($user->exists()) {
96 67
             if ($user->getStatus()->getString() == "NORMAL") {
97 68
                 if ($user->checkPassword($VARS['password'])) {
98
-                    if (is_null($access_permission) || $user->hasPermission($access_permission)) {
99
-                        Session::start($user);
100
-                        $_SESSION['mobile'] = true;
101
-                        exit(json_encode(["status" => "OK"]));
102
-                    } else {
103
-                        exit(json_encode(["status" => "ERROR", "msg" => lang("no permission", false)]));
69
+                    foreach ($SETTINGS['permissions'] as $perm) {
70
+                        if (!$user->hasPermission($perm)) {
71
+                            exit(json_encode(["status" => "ERROR", "msg" => $Strings->get("no permission", false)]));
72
+                        }
104 73
                     }
74
+                    Session::start($user);
75
+                    $_SESSION['mobile'] = true;
76
+                    exit(json_encode(["status" => "OK"]));
105 77
                 }
106 78
             }
107 79
         }

+ 8
- 0
pages.php View File

@@ -99,5 +99,13 @@ define("PAGES", [
99 99
     ],
100 100
     "404" => [
101 101
         "title" => "404 error"
102
+    ],
103
+    "form" => [
104
+        "title" => "Form",
105
+        "navbar" => true,
106
+        "icon" => "fas fa-file-alt",
107
+        "scripts" => [
108
+            "static/js/form.js"
109
+        ]
102 110
     ]
103 111
 ]);

+ 18
- 37
pages/editcat.php View File

@@ -28,43 +28,24 @@ if (!empty($VARS['id'])) {
28 28
         header('Location: app.php?page=editcat');
29 29
     }
30 30
 }
31
-?>
32 31
 
33
-<form role="form" action="action.php" method="POST">
34
-    <div class="card border-green">
35
-        <h3 class="card-header text-green">
36
-            <?php
37
-            if ($editing) {
38
-                ?>
39
-                <i class="fas fa-edit"></i> <?php $Strings->build("editing category", ['cat' => "<span id=\"name_title\">" . htmlspecialchars($catdata['catname']) . "</span>"]); ?>
40
-                <?php
41
-            } else {
42
-                ?>
43
-                <i class="fas fa-edit"></i> <?php $Strings->get("Adding new category"); ?>
44
-                <?php
45
-            }
46
-            ?>
47
-        </h3>
48
-        <div class="card-body">
49
-            <div class="form-group">
50
-                <label for="name"><i class="fas fa-archive"></i> <?php $Strings->get("name"); ?></label>
51
-                <input type="text" class="form-control" id="name" name="name" placeholder="Foo Bar" required="required" value="<?php echo htmlspecialchars($catdata['catname']); ?>" />
52
-            </div>
53
-        </div>
32
+$form = new FormBuilder("", "fas fa-edit");
54 33
 
55
-        <input type="hidden" name="catid" value="<?php echo isset($VARS['id']) ? htmlspecialchars($VARS['id']) : ""; ?>" />
56
-        <input type="hidden" name="action" value="editcat" />
57
-        <input type="hidden" name="source" value="categories" />
34
+if ($editing) {
35
+    $form->setTitle($Strings->build("editing category", ['cat' => "<span id=\"name_title\">" . htmlentities($catdata['catname']) . "</span>"], false));
36
+} else {
37
+    $form->setTitle($Strings->get("Adding new category", false));
38
+}
39
+$form->addInput("name", htmlentities($catdata['catname']), "text", true, "name", null, $Strings->get("name", false), "fas fa-archive", 12);
40
+
41
+$form->addHiddenInput("catid", isset($VARS['id']) ? htmlspecialchars($VARS['id']) : "");
42
+$form->addHiddenInput("action", "editcat");
43
+$form->addHiddenInput("source", "categories");
44
+
45
+$form->addButton($Strings->get("save", false), "fas fa-save", null, "submit");
46
+
47
+if ($editing) {
48
+    $form->addButton($Strings->get("delete", false), "fas fa-times", "action.php?action=deletecat&source=categories&catid=" . htmlspecialchars($VARS['id']), "", null, null, "", "btn btn-danger ml-auto");
49
+}
58 50
 
59
-        <div class="card-footer d-flex">
60
-            <button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button>
61
-            <?php
62
-            if ($editing) {
63
-                ?>
64
-                <a href="action.php?action=deletecat&source=categories&catid=<?php echo htmlspecialchars($VARS['id']); ?>" class="btn btn-danger ml-auto"><i class="fas fa-times"></i> <?php $Strings->get('delete'); ?></a>
65
-                <?php
66
-            }
67
-            ?>
68
-        </div>
69
-    </div>
70
-</form>
51
+$form->generate();

+ 1
- 1
pages/editimages.php View File

@@ -84,7 +84,7 @@ if (!empty($VARS['id']) && $database->has('items', ['itemid' => $VARS['id']])) {
84 84
     <div class="card-footer d-flex">
85 85
         <?php
86 86
         $source = "edititem";
87
-        if ($_GET['source'] === "item") {
87
+        if (!empty($_GET['source']) && $_GET['source'] === "item") {
88 88
             $source = "item";
89 89
         }
90 90
         ?>

+ 19
- 51
pages/editloc.php View File

@@ -34,58 +34,26 @@ if (!empty($VARS['id'])) {
34 34
         header('Location: app.php?page=editloc');
35 35
     }
36 36
 }
37
-?>
38 37
 
39
-<form role="form" action="action.php" method="POST">
40
-    <div class="card border-green">
41
-            <h3 class="card-header text-green">
42
-                <?php
43
-                if ($editing) {
44
-                    ?>
45
-                    <i class="fas fa-edit"></i> <?php $Strings->build("editing location", ['loc' => "<span id=\"name_title\">" . htmlspecialchars($locdata['locname']) . "</span>"]); ?>
46
-                    <?php
47
-                } else {
48
-                    ?>
49
-                    <i class="fas fa-edit"></i> <?php $Strings->get("Adding new location"); ?>
50
-                    <?php
51
-                }
52
-                ?>
53
-            </h3>
54
-        <div class="card-body">
55
-            <div class="row">
56
-                <div class="col-12 col-md-6">
57
-                    <div class="form-group">
58
-                        <label for="name"><i class="fas fa-map-marker"></i> <?php $Strings->get("name"); ?></label>
59
-                        <input type="text" class="form-control" id="name" name="name" placeholder="<?php $Strings->get("placeholder location name"); ?>" required="required" value="<?php echo htmlspecialchars($locdata['locname']); ?>" />
60
-                    </div>
61
-                </div>
62
-                <div class="col-12 col-md-6">
63
-                    <div class="form-group">
64
-                        <label for="code"><i class="fas fa-barcode"></i> <?php $Strings->get("code"); ?></label>
65
-                        <input type="text" class="form-control" id="code" name="code" placeholder="123456789" value="<?php echo htmlspecialchars($locdata['loccode']); ?>" />
66
-                    </div>
67
-                </div>
68
-            </div>
38
+$form = new FormBuilder("", "fas fa-edit");
69 39
 
70
-            <div class="form-group">
71
-                <label for="info"><i class="fas fa-info"></i> <?php $Strings->get("Description"); ?></label>
72
-                <textarea class="form-control" id="info" name="info"><?php echo htmlspecialchars($locdata['locinfo']); ?></textarea>
73
-            </div>
74
-        </div>
40
+if ($editing) {
41
+    $form->setTitle($Strings->build("editing location", ['loc' => "<span id=\"name_title\">" . htmlentities($locdata['locname']) . "</span>"], false));
42
+} else {
43
+    $form->setTitle($Strings->get("Adding new location", false));
44
+}
45
+$form->addInput("name", htmlentities($locdata['locname']), "text", true, "name", null, $Strings->get("name", false), "fas fa-map-marker", 6);
46
+$form->addInput("code", htmlentities($locdata['loccode']), "text", false, "code", null, $Strings->get("code", false), "fas fa-barcode", 6);
47
+$form->addInput("info", htmlentities($locdata['locinfo']), "textarea", false, "info", null, $Strings->get("Description", false), "fas fa-info", 12);
48
+
49
+$form->addHiddenInput("locid", isset($VARS['id']) ? htmlspecialchars($VARS['id']) : "");
50
+$form->addHiddenInput("action", "editloc");
51
+$form->addHiddenInput("source", "locations");
75 52
 
76
-        <input type="hidden" name="locid" value="<?php echo isset($VARS['id']) ? htmlspecialchars($VARS['id']) : ""; ?>" />
77
-        <input type="hidden" name="action" value="editloc" />
78
-        <input type="hidden" name="source" value="locations" />
53
+$form->addButton($Strings->get("save", false), "fas fa-save", null, "submit");
54
+
55
+if ($editing) {
56
+    $form->addButton($Strings->get("delete", false), "fas fa-times", "action.php?action=deleteloc&source=locations&locid=" . htmlspecialchars($VARS['id']), "", null, null, "", "btn btn-danger ml-auto");
57
+}
79 58
 
80
-        <div class="card-footer d-flex">
81
-            <button type="submit" class="btn btn-success mr-auto"><i class="fas fa-save"></i> <?php $Strings->get("save"); ?></button>
82
-            <?php
83
-            if ($editing) {
84
-                ?>
85
-                <a href="action.php?action=deleteloc&source=locations&locid=<?php echo htmlspecialchars($VARS['id']); ?>" class="btn btn-danger ml-auto"><i class="fas fa-times"></i> <?php $Strings->get('delete'); ?></a>
86
-                <?php
87
-            }
88
-            ?>
89
-        </div>
90
-    </div>
91
-</form>
59
+$form->generate();

+ 30
- 60
required.php View File

@@ -32,7 +32,6 @@ session_start(); // stick some cookies in it
32 32
 // renew session cookie
33 33
 setcookie(session_name(), session_id(), time() + $session_length, "/", false, false);
34 34
 
35
-$captcha_server = (CAPTCHA_ENABLED === true ? preg_replace("/http(s)?:\/\//", "", CAPTCHA_SERVER) : "");
36 35
 if ($_SESSION['mobile'] === TRUE) {
37 36
     header("Content-Security-Policy: "
38 37
             . "default-src 'self';"
@@ -42,8 +41,8 @@ if ($_SESSION['mobile'] === TRUE) {
42 41
             . "frame-src 'none'; "
43 42
             . "font-src 'self'; "
44 43
             . "connect-src *; "
45
-            . "style-src 'self' 'unsafe-inline' $captcha_server; "
46
-            . "script-src 'self' 'unsafe-inline' $captcha_server");
44
+            . "style-src 'self' 'unsafe-inline'; "
45
+            . "script-src 'self' 'unsafe-inline'");
47 46
 } else {
48 47
     header("Content-Security-Policy: "
49 48
             . "default-src 'self';"
@@ -53,8 +52,8 @@ if ($_SESSION['mobile'] === TRUE) {
53 52
             . "frame-src 'none'; "
54 53
             . "font-src 'self'; "
55 54
             . "connect-src *; "
56
-            . "style-src 'self' 'nonce-$SECURE_NONCE' $captcha_server; "
57
-            . "script-src 'self' 'nonce-$SECURE_NONCE' $captcha_server");
55
+            . "style-src 'self' 'nonce-$SECURE_NONCE'; "
56
+            . "script-src 'self' 'nonce-$SECURE_NONCE'");
58 57
 }
59 58
 
60 59
 //
@@ -69,7 +68,7 @@ foreach ($libs as $lib) {
69 68
     require_once $lib;
70 69
 }
71 70
 
72
-$Strings = new Strings(LANGUAGE);
71
+$Strings = new Strings($SETTINGS['language']);
73 72
 
74 73
 /**
75 74
  * Kill off the running process and spit out an error message
@@ -93,7 +92,7 @@ function sendError($error) {
93 92
             . "<p>" . htmlspecialchars($error) . "</p>");
94 93
 }
95 94
 
96
-date_default_timezone_set(TIMEZONE);
95
+date_default_timezone_set($SETTINGS['timezone']);
97 96
 
98 97
 // Database settings
99 98
 // Also inits database and stuff
@@ -102,12 +101,12 @@ use Medoo\Medoo;
102 101
 $database;
103 102
 try {
104 103
     $database = new Medoo([
105
-        'database_type' => DB_TYPE,
106
-        'database_name' => DB_NAME,
107
-        'server' => DB_SERVER,
108
-        'username' => DB_USER,
109
-        'password' => DB_PASS,
110
-        'charset' => DB_CHARSET
104
+        'database_type' => $SETTINGS['database']['type'],
105
+        'database_name' => $SETTINGS['database']['name'],
106
+        'server' => $SETTINGS['database']['server'],
107
+        'username' => $SETTINGS['database']['user'],
108
+        'password' => $SETTINGS['database']['password'],
109
+        'charset' => $SETTINGS['database']['charset']
111 110
     ]);
112 111
 } catch (Exception $ex) {
113 112
     //header('HTTP/1.1 500 Internal Server Error');
@@ -115,7 +114,7 @@ try {
115 114
 }
116 115
 
117 116
 
118
-if (!DEBUG) {
117
+if (!$SETTINGS['debug']) {
119 118
     error_reporting(0);
120 119
 } else {
121 120
     error_reporting(E_ALL);
@@ -132,17 +131,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
132 131
     define("GET", true);
133 132
 }
134 133
 
135
-/**
136
- * Checks if a string or whatever is empty.
137
- * @param $str The thingy to check
138
- * @return boolean True if it's empty or whatever.
139
- */
140
-function is_empty($str) {
141
-    return (is_null($str) || !isset($str) || $str == '');
142
-}
143
-
144
-
145 134
 function dieifnotloggedin() {
135
+    global $SETTINGS;
146 136
     if ($_SESSION['loggedin'] != true) {
147 137
         sendError("Session expired.  Please log out and log in again.");
148 138
         die();
@@ -150,6 +140,13 @@ function dieifnotloggedin() {
150 140
     if ((new User($_SESSION['uid']))->hasPermission("INV_VIEW") == FALSE) {
151 141
         die("You don't have permission to be here.");
152 142
     }
143
+    $user = new User($_SESSION['uid']);
144
+    foreach ($SETTINGS['permissions'] as $perm) {
145
+        if (!$user->hasPermission($perm)) {
146
+            session_destroy();
147
+            die("You don't have permission to be here.");
148
+        }
149
+    }
153 150
 }
154 151
 
155 152
 /**
@@ -169,45 +166,18 @@ function checkDBError($specials = []) {
169 166
     }
170 167
 }
171 168
 
172
-/*
173
- * http://stackoverflow.com/a/20075147
174
- */
175
-if (!function_exists('base_url')) {
176
-
177
-    function base_url($atRoot = FALSE, $atCore = FALSE, $parse = FALSE) {
178
-        if (isset($_SERVER['HTTP_HOST'])) {
179
-            $http = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off' ? 'https' : 'http';
180
-            $hostname = $_SERVER['HTTP_HOST'];
181
-            $dir = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);
182
-
183
-            $core = preg_split('@/@', str_replace($_SERVER['DOCUMENT_ROOT'], '', realpath(dirname(__FILE__))), NULL, PREG_SPLIT_NO_EMPTY);
184
-            $core = $core[0];
185
-
186
-            $tmplt = $atRoot ? ($atCore ? "%s://%s/%s/" : "%s://%s/") : ($atCore ? "%s://%s/%s/" : "%s://%s%s");
187
-            $end = $atRoot ? ($atCore ? $core : $hostname) : ($atCore ? $core : $dir);
188
-            $base_url = sprintf($tmplt, $http, $hostname, $end);
189
-        } else
190
-            $base_url = 'http://localhost/';
191
-
192
-        if ($parse) {
193
-            $base_url = parse_url($base_url);
194
-            if (isset($base_url['path']))
195
-                if ($base_url['path'] == '/')
196
-                    $base_url['path'] = '';
197
-        }
198
-
199
-        return $base_url;
200
-    }
201
-
202
-}
203
-
204 169
 function redirectIfNotLoggedIn() {
170
+    global $SETTINGS;
205 171
     if ($_SESSION['loggedin'] !== TRUE) {
206
-        header('Location: ./index.php');
172
+        header('Location: ' . $SETTINGS['url'] . '/index.php');
207 173
         die();
208 174
     }
209
-    if ((new User($_SESSION['uid']))->hasPermission("INV_VIEW") == FALSE) {
210
-        header('Location: ./index.php?permissionerror');
211
-        die("You don't have permission to be here.");
175
+    $user = new User($_SESSION['uid']);
176
+    foreach ($SETTINGS['permissions'] as $perm) {
177
+        if (!$user->hasPermission($perm)) {
178
+            session_destroy();
179
+            header('Location: ./index.php');
180
+            die("You don't have permission to be here.");
181
+        }
212 182
     }
213 183
 }

+ 53
- 49
settings.template.php View File

@@ -1,52 +1,56 @@
1 1
 <?php
2 2
 
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
3
+/*
4
+ * This Source Code Form is subject to the terms of the Mozilla Public
4 5
  * 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
-// Whether to show debugging data in output.
8
-// DO NOT SET TO TRUE IN PRODUCTION!!!
9
-define("DEBUG", false);
10
-
11
-// Database connection settings
12
-// See http://medoo.in/api/new for info
13
-define("DB_TYPE", "mysql");
14
-define("DB_NAME", "inventory");
15
-define("DB_SERVER", "localhost");
16
-define("DB_USER", "inventory");
17
-define("DB_PASS", "");
18
-define("DB_CHARSET", "utf8");
19
-
20
-// Name of the app.
21
-define("SITE_TITLE", "BinStack");
22
-
23
-
24
-// URL of the AccountHub API endpoint
25
-define("PORTAL_API", "http://localhost/accounthub/api.php");
26
-// URL of the AccountHub home page
27
-define("PORTAL_URL", "http://localhost/accounthub/home.php");
28
-// AccountHub API Key
29
-define("PORTAL_KEY", "123");
30
-
31
-// For supported values, see http://php.net/manual/en/timezones.php
32
-define("TIMEZONE", "America/Denver");
33
-
34
-// Base URL for site links.
35
-define('URL', '.');
36
-
37
-// Folder for item images
38
-// If in the webroot, verify that the contents of the folder are not accessible
39
-// from a client (web browser).
40
-define('FILE_UPLOAD_PATH', __DIR__ . '/images');
41
-
42
-// Use Captcheck on login screen
43
-// https://captcheck.netsyms.com
44
-define("CAPTCHA_ENABLED", FALSE);
45
-define('CAPTCHA_SERVER', 'https://captcheck.netsyms.com');
46
-
47
-// See lang folder for language options
48
-define('LANGUAGE', "en_us");
49
-
50
-
51
-define("FOOTER_TEXT", "");
52
-define("COPYRIGHT_NAME", "Netsyms Technologies");
6
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+// Settings for the app.
10
+// Copy to settings.php and customize.
11
+
12
+$SETTINGS = [
13
+    // Whether to output debugging info like PHP notices, warnings,
14
+    // and stacktraces.
15
+    // Turning this on in production is a security risk and can sometimes break
16
+    // things, such as JSON output where extra content is not expected.
17
+    "debug" => false,
18
+    // Database connection settings
19
+    // See http://medoo.in/api/new for info
20
+    "database" => [
21
+        "type" => "mysql",
22
+        "name" => "binstack",
23
+        "server" => "localhost",
24
+        "user" => "app",
25
+        "password" => "",
26
+        "charset" => "utf8"
27
+    ],
28
+    // Name of the app.
29
+    "site_title" => "BinStack",
30
+    // Settings for connecting to the AccountHub server.
31
+    "accounthub" => [
32
+        // URL for the API endpoint
33
+        "api" => "http://localhost/accounthub/api/",
34
+        // URL of the home page
35
+        "home" => "http://localhost/accounthub/home.php",
36
+        // API key
37
+        "key" => "123"
38
+    ],
39
+    "file_upload_path" => __DIR__ . '/images',
40
+    // List of required user permissions to access this app.
41
+    "permissions" => [
42
+        "INV_VIEW"
43
+    ],
44
+    // For supported values, see http://php.net/manual/en/timezones.php
45
+    "timezone" => "America/Denver",
46
+    // Language to use for localization. See langs folder to add a language.
47
+    "language" => "en",
48
+    // Shown in the footer of all the pages.
49
+    "footer_text" => "",
50
+    // Also shown in the footer, but with "Copyright <current_year>" in front.
51
+    "copyright" => "Netsyms Technologies",
52
+    // Base URL for building links relative to the location of the app.
53
+    // Only used when there's no good context for the path.
54
+    // The default is almost definitely fine.
55
+    "url" => "."
56
+];

+ 0
- 15
static/css/index.css View File

@@ -1,15 +0,0 @@
1
-/* This Source Code Form is subject to the terms of the Mozilla Public
2
- * License, v. 2.0. If a copy of the MPL was not distributed with this
3
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
-
5
-.banner-image {
6
-    max-height: 100px;
7
-    margin: 2em auto;
8
-    border: 1px solid grey;
9
-    border-radius: 15%;
10
-}
11
-
12
-.footer {
13
-    margin-top: 10em;
14
-    text-align: center;
15
-}

+ 2
- 2
static/css/svg-with-js.min.css View File

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

+ 2
- 2
static/js/fontawesome-all.min.js
File diff suppressed because it is too large
View File


+ 16
- 0
static/js/form.js View File

@@ -0,0 +1,16 @@
1
+/*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+
8
+$("#savebtn").click(function (event) {
9
+    var form = $("#sampleform");
10
+
11
+    if (form[0].checkValidity() === false) {
12
+        event.preventDefault();
13
+        event.stopPropagation();
14
+    }
15
+    form.addClass('was-validated');
16
+});

Loading…
Cancel
Save