Browse Source

Merge BusinessAppTemplate

Skylar Ittner 6 months ago
parent
commit
56a223c430

+ 1
- 0
README.md View File

@@ -39,3 +39,4 @@ Installing
39 39
 8. Set the URL of this app ("URL")
40 40
 9. Copy webroot.htaccess to your webroot and adjust paths if needed
41 41
 10. Run `composer install` (or `composer.phar install`) to install dependency libraries
42
+11. Run `git submodule init` and `git submodule update` to install other dependencies via git.

+ 11
- 10
action.php View File

@@ -9,7 +9,6 @@
9 9
  */
10 10
 require_once __DIR__ . "/required.php";
11 11
 require_once __DIR__ . "/lib/util.php";
12
-require_once __DIR__ . "/lib/login.php";
13 12
 
14 13
 if ($VARS['action'] !== "signout") {
15 14
     dieifnotloggedin();
@@ -37,9 +36,11 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST' && empty($_POST) &&
37 36
     returnToSender("upload_too_big");
38 37
 }
39 38
 
39
+$user = new User($_SESSION['uid']);
40
+
40 41
 switch ($VARS['action']) {
41 42
     case "newpage":
42
-        if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_EDIT")) {
43
+        if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_EDIT")) {
43 44
             returnToSender("no_permission");
44 45
         }
45 46
         if (is_empty($VARS['siteid']) || !$database->has("sites", ["siteid" => $VARS['siteid']])) {
@@ -80,7 +81,7 @@ switch ($VARS['action']) {
80 81
         returnToSender("page_added", $VARS['siteid'] . "|" . $database->id());
81 82
         break;
82 83
     case "pagesettings":
83
-        if (!account_has_permission($_SESSION['username'], "SITEWRITER")) {
84
+        if (!$user->hasPermission("SITEWRITER")) {
84 85
             returnToSender("no_permission");
85 86
         }
86 87
         if (is_empty($VARS['siteid']) || !$database->has("sites", ["siteid" => $VARS['siteid']])) {
@@ -138,7 +139,7 @@ switch ($VARS['action']) {
138 139
         returnToSender("settings_saved", $VARS['siteid'] . "|" . $VARS['pageid']);
139 140
         break;
140 141
     case "sitesettings":
141
-        if (!account_has_permission($_SESSION['username'], "SITEWRITER")) {
142
+        if (!$user->hasPermission("SITEWRITER")) {
142 143
             returnToSender("no_permission");
143 144
         }
144 145
         if (!is_empty($VARS['siteid'])) {
@@ -198,8 +199,8 @@ switch ($VARS['action']) {
198 199
         break;
199 200
     case "saveedits":
200 201
         header("Content-Type: application/json");
201
-        if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_EDIT")) {
202
-            exit(json_encode(['status' => "ERROR", 'message' => lang("no permission", false)]));
202
+        if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_EDIT")) {
203
+            exit(json_encode(['status' => "ERROR", 'message' => $Strings->get("no permission", false)]));
203 204
         }
204 205
         $slug = $VARS['slug'];
205 206
         $site = $VARS['site'];
@@ -228,7 +229,7 @@ switch ($VARS['action']) {
228 229
         exit(json_encode(["status" => "OK"]));
229 230
         break;
230 231
     case "deletemessage":
231
-        if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_CONTACT")) {
232
+        if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_CONTACT")) {
232 233
             returnToSender("no_permission");
233 234
         }
234 235
         if ($database->count('messages', ["mid" => $VARS['id']]) !== 1) {
@@ -238,7 +239,7 @@ switch ($VARS['action']) {
238 239
         returnToSender("message_deleted");
239 240
         break;
240 241
     case "fileupload":
241
-        if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_FILES")) {
242
+        if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_FILES")) {
242 243
             returnToSender("no_permission");
243 244
         }
244 245
         $destpath = FILE_UPLOAD_PATH . $VARS['path'];
@@ -310,7 +311,7 @@ switch ($VARS['action']) {
310 311
         returnToSender("upload_success", "&path=" . $VARS['path']);
311 312
         break;
312 313
     case "newfolder":
313
-        if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_FILES")) {
314
+        if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_FILES")) {
314 315
             returnToSender("no_permission");
315 316
         }
316 317
         $foldername = preg_replace("/[^a-z0-9_\-]/", "_", strtolower($VARS['folder']));
@@ -322,7 +323,7 @@ switch ($VARS['action']) {
322 323
         returnToSender("folder_not_created", "&path=" . $VARS['path']);
323 324
         break;
324 325
     case "filedelete":
325
-        if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_FILES")) {
326
+        if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_FILES")) {
326 327
             returnToSender("no_permission");
327 328
         }
328 329
         $file = FILE_UPLOAD_PATH . $VARS['file'];

+ 2
- 4
api.php View File

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

+ 8
- 8
app.php View File

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

+ 32
- 32
composer.lock View File

@@ -4,21 +4,20 @@
4 4
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5 5
         "This file is @generated automatically"
6 6
     ],
7
-    "hash": "f30e715ebe71e016a347feec9c9dc5bf",
8 7
     "content-hash": "9efd5e7ff4f253d9ef07ae1535880ffb",
9 8
     "packages": [
10 9
         {
11 10
             "name": "catfan/medoo",
12
-            "version": "v1.5.6",
11
+            "version": "v1.5.7",
13 12
             "source": {
14 13
                 "type": "git",
15 14
                 "url": "https://github.com/catfan/Medoo.git",
16
-                "reference": "f77a93f72864e892c99d1033b8733e5da8fb0b3b"
15
+                "reference": "8d90cba0e8ff176028847527d0ea76fe41a06ecf"
17 16
             },
18 17
             "dist": {
19 18
                 "type": "zip",
20
-                "url": "https://api.github.com/repos/catfan/Medoo/zipball/f77a93f72864e892c99d1033b8733e5da8fb0b3b",
21
-                "reference": "f77a93f72864e892c99d1033b8733e5da8fb0b3b",
19
+                "url": "https://api.github.com/repos/catfan/Medoo/zipball/8d90cba0e8ff176028847527d0ea76fe41a06ecf",
20
+                "reference": "8d90cba0e8ff176028847527d0ea76fe41a06ecf",
22 21
                 "shasum": ""
23 22
             },
24 23
             "require": {
@@ -64,20 +63,20 @@
64 63
                 "sql",
65 64
                 "sqlite"
66 65
             ],
67
-            "time": "2018-03-26 17:54:24"
66
+            "time": "2018-06-14T18:59:08+00:00"
68 67
         },
69 68
         {
70 69
             "name": "composer/ca-bundle",
71
-            "version": "1.1.1",
70
+            "version": "1.1.2",
72 71
             "source": {
73 72
                 "type": "git",
74 73
                 "url": "https://github.com/composer/ca-bundle.git",
75
-                "reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169"
74
+                "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0"
76 75
             },
77 76
             "dist": {
78 77
                 "type": "zip",
79
-                "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d2c0a83b7533d6912e8d516756ebd34f893e9169",
80
-                "reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169",
78
+                "url": "https://api.github.com/repos/composer/ca-bundle/zipball/46afded9720f40b9dc63542af4e3e43a1177acb0",
79
+                "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0",
81 80
                 "shasum": ""
82 81
             },
83 82
             "require": {
@@ -120,7 +119,7 @@
120 119
                 "ssl",
121 120
                 "tls"
122 121
             ],
123
-            "time": "2018-03-29 19:57:20"
122
+            "time": "2018-08-08T08:57:40+00:00"
124 123
         },
125 124
         {
126 125
             "name": "geoip2/geoip2",
@@ -172,20 +171,20 @@
172 171
                 "geolocation",
173 172
                 "maxmind"
174 173
             ],
175
-            "time": "2018-04-10 15:32:59"
174
+            "time": "2018-04-10T15:32:59+00:00"
176 175
         },
177 176
         {
178 177
             "name": "guzzlehttp/guzzle",
179
-            "version": "6.3.2",
178
+            "version": "6.3.3",
180 179
             "source": {
181 180
                 "type": "git",
182 181
                 "url": "https://github.com/guzzle/guzzle.git",
183
-                "reference": "68d0ea14d5a3f42a20e87632a5f84931e2709c90"
182
+                "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
184 183
             },
185 184
             "dist": {
186 185
                 "type": "zip",
187
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/68d0ea14d5a3f42a20e87632a5f84931e2709c90",
188
-                "reference": "68d0ea14d5a3f42a20e87632a5f84931e2709c90",
186
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
187
+                "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
189 188
                 "shasum": ""
190 189
             },
191 190
             "require": {
@@ -195,7 +194,7 @@
195 194
             },
196 195
             "require-dev": {
197 196
                 "ext-curl": "*",
198
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4",
197
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
199 198
                 "psr/log": "^1.0"
200 199
             },
201 200
             "suggest": {
@@ -237,7 +236,7 @@
237 236
                 "rest",
238 237
                 "web service"
239 238
             ],
240
-            "time": "2018-03-26 16:33:04"
239
+            "time": "2018-04-22T15:46:56+00:00"
241 240
         },
242 241
         {
243 242
             "name": "guzzlehttp/promises",
@@ -288,7 +287,7 @@
288 287
             "keywords": [
289 288
                 "promise"
290 289
             ],
291
-            "time": "2016-12-20 10:07:11"
290
+            "time": "2016-12-20T10:07:11+00:00"
292 291
         },
293 292
         {
294 293
             "name": "guzzlehttp/psr7",
@@ -353,7 +352,7 @@
353 352
                 "uri",
354 353
                 "url"
355 354
             ],
356
-            "time": "2017-03-20 17:10:46"
355
+            "time": "2017-03-20T17:10:46+00:00"
357 356
         },
358 357
         {
359 358
             "name": "hughbertd/oauth2-unsplash",
@@ -405,7 +404,7 @@
405 404
                 "oauth2",
406 405
                 "single sign on"
407 406
             ],
408
-            "time": "2017-12-14 13:08:42"
407
+            "time": "2017-12-14T13:08:42+00:00"
409 408
         },
410 409
         {
411 410
             "name": "league/oauth2-client",
@@ -472,7 +471,7 @@
472 471
                 "oauth2",
473 472
                 "single sign on"
474 473
             ],
475
-            "time": "2018-01-13 05:27:58"
474
+            "time": "2018-01-13T05:27:58+00:00"
476 475
         },
477 476
         {
478 477
             "name": "maxmind-db/reader",
@@ -528,7 +527,7 @@
528 527
                 "geolocation",
529 528
                 "maxmind"
530 529
             ],
531
-            "time": "2018-02-21 21:23:33"
530
+            "time": "2018-02-21T21:23:33+00:00"
532 531
         },
533 532
         {
534 533
             "name": "maxmind/web-service-common",
@@ -574,20 +573,20 @@
574 573
             ],
575 574
             "description": "Internal MaxMind Web Service API",
576 575
             "homepage": "https://github.com/maxmind/web-service-common-php",
577
-            "time": "2018-02-12 22:31:54"
576
+            "time": "2018-02-12T22:31:54+00:00"
578 577
         },
579 578
         {
580 579
             "name": "paragonie/random_compat",
581
-            "version": "v2.0.12",
580
+            "version": "v2.0.17",
582 581
             "source": {
583 582
                 "type": "git",
584 583
                 "url": "https://github.com/paragonie/random_compat.git",
585
-                "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb"
584
+                "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d"
586 585
             },
587 586
             "dist": {
588 587
                 "type": "zip",
589
-                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/258c89a6b97de7dfaf5b8c7607d0478e236b04fb",
590
-                "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb",
588
+                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d",
589
+                "reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d",
591 590
                 "shasum": ""
592 591
             },
593 592
             "require": {
@@ -619,10 +618,11 @@
619 618
             "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
620 619
             "keywords": [
621 620
                 "csprng",
621
+                "polyfill",
622 622
                 "pseudorandom",
623 623
                 "random"
624 624
             ],
625
-            "time": "2018-04-04 21:24:14"
625
+            "time": "2018-07-04T16:31:37+00:00"
626 626
         },
627 627
         {
628 628
             "name": "phpmailer/phpmailer",
@@ -688,7 +688,7 @@
688 688
                 }
689 689
             ],
690 690
             "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
691
-            "time": "2018-03-27 13:49:45"
691
+            "time": "2018-03-27T13:49:45+00:00"
692 692
         },
693 693
         {
694 694
             "name": "psr/http-message",
@@ -738,7 +738,7 @@
738 738
                 "request",
739 739
                 "response"
740 740
             ],
741
-            "time": "2016-08-06 14:39:51"
741
+            "time": "2016-08-06T14:39:51+00:00"
742 742
         },
743 743
         {
744 744
             "name": "unsplash/unsplash",
@@ -797,7 +797,7 @@
797 797
                 }
798 798
             ],
799 799
             "description": "Wrapper to access the Unsplash API and photo library",
800
-            "time": "2018-03-30 17:45:15"
800
+            "time": "2018-03-30T17:45:15+00:00"
801 801
         }
802 802
     ],
803 803
     "packages-dev": [],

+ 58
- 47
index.php View File

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

+ 0
- 146
lang/en_us.php View File

@@ -1,146 +0,0 @@
1
-<?php
2
-
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
-
7
-define("STRINGS", [
8
-    "sign in" => "Sign In",
9
-    "username" => "Username",
10
-    "password" => "Password",
11
-    "continue" => "Continue",
12
-    "authcode" => "Authentication code",
13
-    "2fa prompt" => "Enter the six-digit code from your mobile authenticator app.",
14
-    "2fa incorrect" => "Authentication code incorrect.",
15
-    "login incorrect" => "Login incorrect.",
16
-    "login server unavailable" => "Login server unavailable.  Try again later or contact technical support.",
17
-    "account locked" => "This account has been disabled. Contact technical support.",
18
-    "password expired" => "You must change your password before continuing.",
19
-    "account terminated" => "Account terminated.  Access denied.",
20
-    "account state error" => "Your account state is not stable.  Log out, restart your browser, and try again.",
21
-    "welcome user" => "Welcome, {user}!",
22
-    "sign out" => "Sign out",
23
-    "settings" => "Settings",
24
-    "options" => "Options",
25
-    "404 error" => "404 Error",
26
-    "page not found" => "Page not found.",
27
-    "invalid parameters" => "Invalid request parameters.",
28
-    "login server error" => "The login server returned an error: {arg}",
29
-    "login server user data error" => "The login server refused to provide account information.  Try again or contact technical support.",
30
-    "captcha error" => "There was a problem with the CAPTCHA (robot test).  Try again.",
31
-    "no access permission" => "You do not have permission to access this system.",
32
-    "actions" => "Actions",
33
-    "no permission" => "You don't have permission to do that.",
34
-    "home" => "Home",
35
-    "editor" => "Editor",
36
-    "sites" => "Sites",
37
-    "theme" => "Theme",
38
-    "name" => "Name",
39
-    "new site" => "New Site",
40
-    "site name" => "Site Name",
41
-    "url" => "URL",
42
-    "adding site" => "Creating site {site}",
43
-    "editing site" => "Editing {site}",
44
-    "settings saved" => "Settings saved",
45
-    "theme type" => "Theme type",
46
-    "single page" => "Single page",
47
-    "multiple page" => "Multiple page",
48
-    "templates" => "Templates",
49
-    "template" => "Template",
50
-    "color styles" => "Color styles",
51
-    "save" => "Save",
52
-    "edit" => "Edit",
53
-    "view" => "View",
54
-    "preview" => "Preview",
55
-    "cancel" => "Cancel",
56
-    "save needed" => "Press Save to see recent changes.",
57
-    "saved" => "Saved",
58
-    "icon" => "Icon",
59
-    "image" => "Image",
60
-    "link" => "Link",
61
-    "text" => "Text",
62
-    "select page or enter url" => "Select a page or enter URL",
63
-    "edit component" => "Edit component",
64
-    "default" => "Default",
65
-    "page added" => "Page added.",
66
-    "chosen page id slug already taken" => "Chosen page ID (slug) already taken.  Choose another.",
67
-    "template missing" => "Template missing from theme.",
68
-    "new page" => "New Page",
69
-    "title" => "Title",
70
-    "page id" => "Page ID (slug)",
71
-    "add page" => "Add page",
72
-    "page settings" => "Page Settings",
73
-    "analytics" => "Analytics",
74
-    "today" => "Today",
75
-    "this week" => "This Week",
76
-    "visit" => "visit",
77
-    "visits" => "visits",
78
-    "page view" => "page view",
79
-    "page views" => "page views",
80
-    "site" => "Site",
81
-    "filter by site" => "Filter by site",
82
-    "all sites" => "All Sites",
83
-    "filter" => "Filter",
84
-    "start date" => "Start date",
85
-    "end date" => "End date",
86
-    "recent actions" => "Recent Actions",
87
-    "overview" => "Overview",
88
-    "views per visit" => "views per visit",
89
-    "visits over time" => "Visits Over Time",
90
-    "page views over time" => "Page Views Over Time",
91
-    "page ranking" => "Page Ranking",
92
-    "x views" => "{views} views",
93
-    "no data" => "No data.",
94
-    "visitor map" => "Visitor Map",
95
-    "enable built-in analytics" => "Enable built-in analytics",
96
-    "disable built-in analytics" => "Disable built-in analytics",
97
-    "extra code" => "Extra code (inserted in site head)",
98
-    "company info" => "Company Info",
99
-    "phone" => "Phone",
100
-    "address" => "Address",
101
-    "email" => "Email",
102
-    "social links" => "Social Links",
103
-    "site info" => "Site Info",
104
-    "loading" => "Loading...",
105
-    "current" => "Current",
106
-    "messages" => "Messages",
107
-    "message" => "Message",
108
-    "date" => "Date",
109
-    "message deleted" => "Message deleted.",
110
-    "files" => "Files",
111
-    "browse" => "Browse",
112
-    "upload" => "Upload",
113
-    "operation cancelled for security reasons" => "Operation cancelled for security reasons.",
114
-    "upload successful" => "Upload successful.",
115
-    "upload warning" => "Upload finished with some problems:<br>{arg}",
116
-    "destination folder does not exist" => "Destination folder does not exist.",
117
-    "destination folder does not allow uploads" => "Destination folder does not allow uploads.",
118
-    "uploaded data too large" => "Uploaded data too large.",
119
-    "undeletable file" => "The file could not be deleted.",
120
-    "folder not empty" => "Folder must be empty to be deleted.",
121
-    "file not deleted" => "The file could not be deleted.",
122
-    "file deleted" => "File deleted.",
123
-    "folder deleted" => "Folder deleted.",
124
-    "folder created" => "Folder created.",
125
-    "folder not created" => "Folder not created.",
126
-    "nothing here" => "There doesn't seem to be anything here...",
127
-    "navbar options" => "Site Menu Options",
128
-    "in navbar" => "Add page to menu",
129
-    "navbar title" => "Page title for menu",
130
-    "navbar position" => "Menu position (drag to change position):",
131
-    "remove image" => "Remove image",
132
-    "site footer links" => "Site Footer Links",
133
-    "uploaded files" => "Uploaded Files",
134
-    "stock photos" => "Free Stock Photos",
135
-    "load more" => "Load more",
136
-    "search images" => "Search images",
137
-    "x results" => "{results} results",
138
-    "reply" => "Reply",
139
-    "delete" => "Delete",
140
-    "new folder" => "New Folder",
141
-    "new" => "New",
142
-    "search" => "Search",
143
-    "no results" => "No results.",
144
-    "contact form" => "Contact Form",
145
-    "contact form messages will be forwarded to this email address" => "Contact form messages will be forwarded to this email address, if it is set.",
146
-]);

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

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

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

@@ -0,0 +1,115 @@
1
+{
2
+    "actions": "Actions",
3
+    "no permission": "You don't have permission to do that.",
4
+    "editor": "Editor",
5
+    "sites": "Sites",
6
+    "theme": "Theme",
7
+    "name": "Name",
8
+    "new site": "New Site",
9
+    "site name": "Site Name",
10
+    "url": "URL",
11
+    "adding site": "Creating site {site}",
12
+    "editing site": "Editing {site}",
13
+    "settings saved": "Settings saved",
14
+    "theme type": "Theme type",
15
+    "single page": "Single page",
16
+    "multiple page": "Multiple page",
17
+    "templates": "Templates",
18
+    "template": "Template",
19
+    "color styles": "Color styles",
20
+    "save": "Save",
21
+    "edit": "Edit",
22
+    "view": "View",
23
+    "preview": "Preview",
24
+    "cancel": "Cancel",
25
+    "save needed": "Press Save to see recent changes.",
26
+    "saved": "Saved",
27
+    "icon": "Icon",
28
+    "image": "Image",
29
+    "link": "Link",
30
+    "text": "Text",
31
+    "select page or enter url": "Select a page or enter URL",
32
+    "edit component": "Edit component",
33
+    "default": "Default",
34
+    "page added": "Page added.",
35
+    "chosen page id slug already taken": "Chosen page ID (slug) already taken.  Choose another.",
36
+    "template missing": "Template missing from theme.",
37
+    "new page": "New Page",
38
+    "title": "Title",
39
+    "page id": "Page ID (slug)",
40
+    "add page": "Add page",
41
+    "page settings": "Page Settings",
42
+    "analytics": "Analytics",
43
+    "today": "Today",
44
+    "this week": "This Week",
45
+    "visit": "visit",
46
+    "visits": "visits",
47
+    "page view": "page view",
48
+    "page views": "page views",
49
+    "site": "Site",
50
+    "filter by site": "Filter by site",
51
+    "all sites": "All Sites",
52
+    "filter": "Filter",
53
+    "start date": "Start date",
54
+    "end date": "End date",
55
+    "recent actions": "Recent Actions",
56
+    "overview": "Overview",
57
+    "views per visit": "views per visit",
58
+    "visits over time": "Visits Over Time",
59
+    "page views over time": "Page Views Over Time",
60
+    "page ranking": "Page Ranking",
61
+    "x views": "{views} views",
62
+    "no data": "No data.",
63
+    "visitor map": "Visitor Map",
64
+    "enable built-in analytics": "Enable built-in analytics",
65
+    "disable built-in analytics": "Disable built-in analytics",
66
+    "extra code": "Extra code (inserted in site head)",
67
+    "company info": "Company Info",
68
+    "phone": "Phone",
69
+    "address": "Address",
70
+    "email": "Email",
71
+    "social links": "Social Links",
72
+    "site info": "Site Info",
73
+    "loading": "Loading...",
74
+    "current": "Current",
75
+    "messages": "Messages",
76
+    "message": "Message",
77
+    "date": "Date",
78
+    "message deleted": "Message deleted.",
79
+    "files": "Files",
80
+    "browse": "Browse",
81
+    "upload": "Upload",
82
+    "operation cancelled for security reasons": "Operation cancelled for security reasons.",
83
+    "upload successful": "Upload successful.",
84
+    "upload warning": "Upload finished with some problems:<br>{arg}",
85
+    "destination folder does not exist": "Destination folder does not exist.",
86
+    "destination folder does not allow uploads": "Destination folder does not allow uploads.",
87
+    "uploaded data too large": "Uploaded data too large.",
88
+    "undeletable file": "The file could not be deleted.",
89
+    "folder not empty": "Folder must be empty to be deleted.",
90
+    "file not deleted": "The file could not be deleted.",
91
+    "file deleted": "File deleted.",
92
+    "folder deleted": "Folder deleted.",
93
+    "folder created": "Folder created.",
94
+    "folder not created": "Folder not created.",
95
+    "nothing here": "There doesn't seem to be anything here...",
96
+    "navbar options": "Site Menu Options",
97
+    "in navbar": "Add page to menu",
98
+    "navbar title": "Page title for menu",
99
+    "navbar position": "Menu position (drag to change position):",
100
+    "remove image": "Remove image",
101
+    "site footer links": "Site Footer Links",
102
+    "uploaded files": "Uploaded Files",
103
+    "stock photos": "Free Stock Photos",
104
+    "load more": "Load more",
105
+    "search images": "Search images",
106
+    "x results": "{results} results",
107
+    "reply": "Reply",
108
+    "delete": "Delete",
109
+    "new folder": "New Folder",
110
+    "new": "New",
111
+    "search": "Search",
112
+    "no results": "No results.",
113
+    "contact form": "Contact Form",
114
+    "contact form messages will be forwarded to this email address": "Contact form messages will be forwarded to this email address, if it is set."
115
+}

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 5
- 5
lib/filepicker.php View File

@@ -35,12 +35,12 @@ if ($enableunsplash) {
35 35
     <ul class="nav nav-tabs" id="fileBrowserTabs" role="tablist">
36 36
         <li class="nav-item">
37 37
             <a class="nav-link active" id="uploadedFilesTabBtn" data-toggle="tab" href="#uploadedFilesTab">
38
-                <i class="fas fa-folder-open"></i> <?php lang('uploaded files'); ?>
38
+                <i class="fas fa-folder-open"></i> <?php $Strings->get('uploaded files'); ?>
39 39
             </a>
40 40
         </li>
41 41
         <li class="nav-item">
42 42
             <a class="nav-link" id="unsplashTabBtn" data-toggle="tab" href="#unsplashTab">
43
-                <i class="fas fa-image"></i> <?php lang('stock photos'); ?>
43
+                <i class="fas fa-image"></i> <?php $Strings->get('stock photos'); ?>
44 44
             </a>
45 45
         </li>
46 46
     </ul>
@@ -60,10 +60,10 @@ if ($enableunsplash) {
60 60
             <div class="card">
61 61
                 <div class="card-body">
62 62
                     <div class="input-group">
63
-                        <input type="text" class="form-control" id="unsplashSearch" placeholder="<?php lang("search images"); ?>" />
63
+                        <input type="text" class="form-control" id="unsplashSearch" placeholder="<?php $Strings->get("search images"); ?>" />
64 64
                         <div class="input-group-append">
65 65
                             <div class="btn btn-primary" id="unsplashSearchBtn">
66
-                                <i class="fas fa-search"></i> <?php lang("search"); ?>
66
+                                <i class="fas fa-search"></i> <?php $Strings->get("search"); ?>
67 67
                             </div>
68 68
                         </div>
69 69
                     </div>
@@ -73,7 +73,7 @@ if ($enableunsplash) {
73 73
                 </div>
74 74
                 <div class="card-body">
75 75
                     <button type="button" class="btn btn-primary btn-block" id="unsplashLoadMoreBtn">
76
-                        <?php lang("load more"); ?>
76
+                        <?php $Strings->get("load more"); ?>
77 77
                     </button>
78 78
                 </div>
79 79
             </div>

+ 1
- 1
lib/filepicker_local.php View File

@@ -129,7 +129,7 @@ $fullpath = $base . $folder;
129 129
                 <i class="far fa-folder-open fa-5x fa-fw"></i>
130 130
             </p>
131 131
             <p class="h5 text-muted">
132
-                <?php lang("nothing here"); ?>
132
+                <?php $Strings->get("nothing here"); ?>
133 133
             </p>
134 134
         </div>
135 135
         <?php

+ 2
- 2
lib/filepicker_unsplash.php View File

@@ -39,7 +39,7 @@ $images->
39 39
 $htmlout = "";
40 40
 
41 41
 if (count($images) == 0) {
42
-    $htmlout = "<div class=\"card text-center\"><div class=\"card-body\"><i class=\"fas fa-search-minus\"></i> " . lang("no results", false) . "</div></div>";
42
+    $htmlout = "<div class=\"card text-center\"><div class=\"card-body\"><i class=\"fas fa-search-minus\"></i> " . $Strings->get("no results", false) . "</div></div>";
43 43
 }
44 44
 
45 45
 $htmlout .= '<div class="card-columns">';
@@ -70,7 +70,7 @@ $jsonout = [
70 70
 ];
71 71
 
72 72
 if (!is_null($results)) {
73
-    $jsonout['total'] = lang2("x results", ["results" => $results->getTotal()], false);
73
+    $jsonout['total'] = $Strings->build("x results", ["results" => $results->getTotal()], false);
74 74
     $jsonout['pages'] = $results->getTotalPages();
75 75
 }
76 76
 

+ 0
- 131
lib/iputils.php View File

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

+ 0
- 402
lib/login.php View File

@@ -1,402 +0,0 @@
1
-<?php
2
-
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
-
7
-/**
8
- * Authentication and account functions.  Connects to an AccountHub instance.
9
- */
10
-
11
-/**
12
- * Check the login server API for sanity
13
- * @return boolean true if OK, else false
14
- */
15
-function checkLoginServer() {
16
-    try {
17
-        $client = new GuzzleHttp\Client();
18
-
19
-        $response = $client
20
-                ->request('POST', PORTAL_API, [
21
-            'form_params' => [
22
-                'key' => PORTAL_KEY,
23
-                'action' => "ping"
24
-            ]
25
-        ]);
26
-
27
-        if ($response->getStatusCode() != 200) {
28
-            return false;
29
-        }
30
-
31
-        $resp = json_decode($response->getBody(), TRUE);
32
-        if ($resp['status'] == "OK") {
33
-            return true;
34
-        } else {
35
-            return false;
36
-        }
37
-    } catch (Exception $e) {
38
-        return false;
39
-    }
40
-}
41
-
42
-/**
43
- * Checks if the given AccountHub API key is valid by attempting to
44
- * access the API with it.
45
- * @param String $key The API key to check
46
- * @return boolean TRUE if the key is valid, FALSE if invalid or something went wrong
47
- */
48
-function checkAPIKey($key) {
49
-    try {
50
-        $client = new GuzzleHttp\Client();
51
-
52
-        $response = $client
53
-                ->request('POST', PORTAL_API, [
54
-            'form_params' => [
55
-                'key' => $key,
56
-                'action' => "ping"
57
-            ]
58
-        ]);
59
-
60
-        if ($response->getStatusCode() === 200) {
61
-            return true;
62
-        }
63
-        return false;
64
-    } catch (Exception $e) {
65
-        return false;
66
-    }
67
-}
68
-
69
-////////////////////////////////////////////////////////////////////////////////
70
-//                           Account handling                                 //
71
-////////////////////////////////////////////////////////////////////////////////
72
-
73
-/**
74
- * Checks the given credentials against the API.
75
- * @param string $username
76
- * @param string $password
77
- * @return boolean True if OK, else false
78
- */
79
-function authenticate_user($username, $password, &$errmsg) {
80
-    $client = new GuzzleHttp\Client();
81
-
82
-    $response = $client
83
-            ->request('POST', PORTAL_API, [
84
-        'form_params' => [
85
-            'key' => PORTAL_KEY,
86
-            'action' => "auth",
87
-            'username' => $username,
88
-            'password' => $password
89
-        ]
90
-    ]);
91
-
92
-    if ($response->getStatusCode() > 299) {
93
-        sendError("Login server error: " . $response->getBody());
94
-    }
95
-
96
-    $resp = json_decode($response->getBody(), TRUE);
97
-    if ($resp['status'] == "OK") {
98
-        return true;
99
-    } else {
100
-        $errmsg = $resp['msg'];
101
-        return false;
102
-    }
103
-}
104
-
105
-/**
106
- * Check if a username exists.
107
- * @param String $username
108
- */
109
-function user_exists($username) {
110
-    $client = new GuzzleHttp\Client();
111
-
112
-    $response = $client
113
-            ->request('POST', PORTAL_API, [
114
-        'form_params' => [
115
-            'key' => PORTAL_KEY,
116
-            'action' => "userexists",
117
-            'username' => $username
118
-        ]
119
-    ]);
120
-
121
-    if ($response->getStatusCode() > 299) {
122
-        sendError("Login server error: " . $response->getBody());
123
-    }
124
-
125
-    $resp = json_decode($response->getBody(), TRUE);
126
-    if ($resp['status'] == "OK" && $resp['exists'] === true) {
127
-        return true;
128
-    } else {
129
-        return false;
130
-    }
131
-}
132
-
133
-/**
134
- * Check if a UID exists.
135
- * @param String $uid
136
- */
137
-function uid_exists($uid) {
138
-    $client = new GuzzleHttp\Client();
139
-
140
-    $response = $client
141
-            ->request('POST', PORTAL_API, [
142
-        'form_params' => [
143
-            'key' => PORTAL_KEY,
144
-            'action' => "userexists",
145
-            'uid' => $uid
146
-        ]
147
-    ]);
148
-
149
-    if ($response->getStatusCode() > 299) {
150
-        sendError("Login server error: " . $response->getBody());
151
-    }
152
-
153
-    $resp = json_decode($response->getBody(), TRUE);
154
-    if ($resp['status'] == "OK" && $resp['exists'] === true) {
155
-        return true;
156
-    } else {
157
-        return false;
158
-    }
159
-}
160
-
161
-/**
162
- * Get the account status: NORMAL, TERMINATED, LOCKED_OR_DISABLED,
163
- * CHANGE_PASSWORD, or ALERT_ON_ACCESS
164
- * @param string $username
165
- * @return string
166
- */
167
-function get_account_status($username) {
168
-    $client = new GuzzleHttp\Client();
169
-
170
-    $response = $client
171
-            ->request('POST', PORTAL_API, [
172
-        'form_params' => [
173
-            'key' => PORTAL_KEY,
174
-            'action' => "acctstatus",
175
-            'username' => $username
176
-        ]
177
-    ]);
178
-
179
-    if ($response->getStatusCode() > 299) {
180
-        sendError("Login server error: " . $response->getBody());
181
-    }
182
-
183
-    $resp = json_decode($response->getBody(), TRUE);
184
-    if ($resp['status'] == "OK") {
185
-        return $resp['account'];
186
-    } else {
187
-        return false;
188
-    }
189
-}
190
-
191
-/**
192
- * Check if the given username has the given permission (or admin access)
193
- * @param string $username
194
- * @param string $permcode
195
- * @return boolean TRUE if the user has the permission (or admin access), else FALSE
196
- */
197
-function account_has_permission($username, $permcode) {
198
-    $client = new GuzzleHttp\Client();
199
-
200
-    $response = $client
201
-            ->request('POST', PORTAL_API, [
202
-        'form_params' => [
203
-            'key' => PORTAL_KEY,
204
-            'action' => "permission",
205
-            'username' => $username,
206
-            'code' => $permcode
207
-        ]
208
-    ]);
209
-
210
-    if ($response->getStatusCode() > 299) {
211
-        sendError("Login server error: " . $response->getBody());
212
-    }
213
-
214
-    $resp = json_decode($response->getBody(), TRUE);
215
-    if ($resp['status'] == "OK") {
216
-        return $resp['has_permission'];
217
-    } else {
218
-        return false;
219
-    }
220
-}
221
-
222
-////////////////////////////////////////////////////////////////////////////////
223
-//                              Login handling                                //
224
-////////////////////////////////////////////////////////////////////////////////
225
-
226
-/**
227
- * Setup $_SESSION values with user data and set loggedin flag to true
228
- * @param string $username
229
- */
230
-function doLoginUser($username) {
231
-    $client = new GuzzleHttp\Client();
232
-
233
-    $response = $client
234
-            ->request('POST', PORTAL_API, [
235
-        'form_params' => [
236
-            'key' => PORTAL_KEY,
237
-            'action' => "userinfo",
238
-            'username' => $username
239
-        ]
240
-    ]);
241
-
242
-    if ($response->getStatusCode() > 299) {
243
-        sendError("Login server error: " . $response->getBody());
244
-    }
245
-
246
-    $resp = json_decode($response->getBody(), TRUE);
247
-
248
-    if ($resp['status'] == "OK") {
249
-        $userinfo = $resp['data'];
250
-        session_regenerate_id(true);
251
-        $newSession = session_id();
252
-        session_write_close();
253
-        session_id($newSession);
254
-        session_start();
255
-        $_SESSION['username'] = $username;
256
-        $_SESSION['uid'] = $userinfo['uid'];
257
-        $_SESSION['email'] = $userinfo['email'];
258
-        $_SESSION['realname'] = $userinfo['name'];
259
-        $_SESSION['loggedin'] = true;
260
-        return true;
261
-    } else {
262
-        return false;
263
-    }
264
-}
265
-
266
-function sendLoginAlertEmail($username) {
267
-    $client = new GuzzleHttp\Client();
268
-
269
-    $response = $client
270
-            ->request('POST', PORTAL_API, [
271
-        'form_params' => [
272
-            'key' => PORTAL_KEY,
273
-            'action' => "alertemail",
274
-            'username' => $username,
275
-            'appname' => SITE_TITLE
276
-        ]
277
-    ]);
278
-
279
-    if ($response->getStatusCode() > 299) {
280
-        return "An unknown error occurred.";
281
-    }
282
-
283
-    $resp = json_decode($response->getBody(), TRUE);
284
-    if ($resp['status'] == "OK") {
285
-        return true;
286
-    } else {
287
-        return $resp['msg'];
288
-    }
289
-}
290
-
291
-function simLogin($username, $password) {
292
-    $client = new GuzzleHttp\Client();
293
-
294
-    $response = $client
295
-            ->request('POST', PORTAL_API, [
296
-        'form_params' => [
297
-            'key' => PORTAL_KEY,
298
-            'action' => "login",
299
-            'username' => $username,
300
-            'password' => $password
301
-        ]
302
-    ]);
303
-
304
-    if ($response->getStatusCode() > 299) {
305
-        sendError("Login server error: " . $response->getBody());
306
-    }
307
-
308
-    $resp = json_decode($response->getBody(), TRUE);
309
-    if ($resp['status'] == "OK") {
310
-        return true;
311
-    } else {
312
-        return $resp['msg'];
313
-    }
314
-}
315
-
316
-function verifyCaptcheck($session, $answer, $url) {
317
-    $data = [
318
-        'session_id' => $session,
319
-        'answer_id' => $answer,
320
-        'action' => "verify"
321
-    ];
322
-    $options = [
323
-        'http' => [
324
-            'header' => "Content-type: application/x-www-form-urlencoded\r\n",
325
-            'method' => 'POST',
326
-            'content' => http_build_query($data)
327
-        ]
328
-    ];
329
-    $context = stream_context_create($options);
330
-    $result = file_get_contents($url, false, $context);
331
-    $resp = json_decode($result, TRUE);
332
-    if (!$resp['result']) {
333
-        return false;
334
-    } else {
335
-        return true;
336
-    }
337
-}
338
-
339
-////////////////////////////////////////////////////////////////////////////////
340
-//                          2-factor authentication                           //
341
-////////////////////////////////////////////////////////////////////////////////
342
-
343
-/**
344
- * Check if a user has TOTP setup
345
- * @param string $username
346
- * @return boolean true if TOTP secret exists, else false
347
- */
348
-function userHasTOTP($username) {
349
-    $client = new GuzzleHttp\Client();
350
-
351
-    $response = $client
352
-            ->request('POST', PORTAL_API, [
353
-        'form_params' => [
354
-            'key' => PORTAL_KEY,
355
-            'action' => "hastotp",
356
-            'username' => $username
357
-        ]
358
-    ]);
359
-
360
-    if ($response->getStatusCode() > 299) {
361
-        sendError("Login server error: " . $response->getBody());
362
-    }
363
-
364
-    $resp = json_decode($response->getBody(), TRUE);
365
-    if ($resp['status'] == "OK") {
366
-        return $resp['otp'];
367
-    } else {
368
-        return false;
369
-    }
370
-}
371
-
372
-/**
373
- * Verify a TOTP multiauth code
374
- * @global $database
375
- * @param string $username
376
- * @param int $code
377
- * @return boolean true if it's legit, else false
378
- */
379
-function verifyTOTP($username, $code) {
380
-    $client = new GuzzleHttp\Client();
381
-
382
-    $response = $client
383
-            ->request('POST', PORTAL_API, [
384
-        'form_params' => [
385
-            'key' => PORTAL_KEY,
386
-            'action' => "verifytotp",
387
-            'username' => $username,
388
-            'code' => $code
389
-        ]
390
-    ]);
391
-
392
-    if ($response->getStatusCode() > 299) {
393
-        sendError("Login server error: " . $response->getBody());
394
-    }
395
-
396
-    $resp = json_decode($response->getBody(), TRUE);
397
-    if ($resp['status'] == "OK") {
398
-        return $resp['valid'];
399
-    } else {
400
-        return false;
401
-    }
402
-}

+ 0
- 127
lib/userinfo.php View File

@@ -1,127 +0,0 @@
1
-<?php
2
-
3
-/* This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
-
7
-/**
8
- * Get user info for the given username.
9
- * @param int $u username
10
- * @return [string] Array of [uid, username, name]
11
- */
12
-function getUserByUsername($u) {
13
-    $client = new GuzzleHttp\Client();
14
-
15
-    $response = $client
16
-            ->request('POST', PORTAL_API, [
17
-        'form_params' => [
18
-            'key' => PORTAL_KEY,
19
-            'action' => "userinfo",
20
-            'username' => $u
21
-        ]
22
-    ]);
23
-
24
-    if ($response->getStatusCode() > 299) {
25
-        sendError("Login server error: " . $response->getBody());
26
-    }
27
-
28
-    $resp = json_decode($response->getBody(), TRUE);
29
-    if ($resp['status'] == "OK") {
30
-        return $resp['data'];
31
-    } else {
32
-        // this shouldn't happen, but in case it does just fake it.
33
-        return ["name" => $u, "username" => $u, "uid" => $u];
34
-    }
35
-}
36
-
37
-/**
38
- * Get user info for the given UID.
39
- * @param int $u user ID
40
- * @return [string] Array of [uid, username, name]
41
- */
42
-function getUserByID($u) {
43
-    $client = new GuzzleHttp\Client();
44
-
45
-    $response = $client
46
-            ->request('POST', PORTAL_API, [
47
-        'form_params' => [
48
-            'key' => PORTAL_KEY,
49
-            'action' => "userinfo",
50
-            'uid' => $u
51
-        ]
52
-    ]);
53
-
54
-    if ($response->getStatusCode() > 299) {
55
-        sendError("Login server error: " . $response->getBody());
56
-    }
57
-
58
-    $resp = json_decode($response->getBody(), TRUE);
59
-    if ($resp['status'] == "OK") {
60
-        return $resp['data'];
61
-    } else {
62
-        // this shouldn't happen, but in case it does just fake it.
63
-        return ["name" => $u, "username" => $u, "uid" => $u];
64
-    }
65
-}
66
-
67
-/**
68
- * Check if the first UID is a manager of the second UID.
69
- * @param int $m Manager UID
70
- * @param int $e Employee UID
71
- * @return boolean
72
- */
73
-function isManagerOf($m, $e) {
74
-    $client = new GuzzleHttp\Client();
75
-
76
-    $response = $client
77
-            ->request('POST', PORTAL_API, [
78
-        'form_params' => [
79
-            'key' => PORTAL_KEY,
80
-            'action' => "ismanagerof",
81
-            'manager' => $m,
82
-            'employee' => $e,
83
-            'uid' => 1
84
-        ]
85
-    ]);
86
-
87
-    if ($response->getStatusCode() > 299) {
88
-        sendError("Login server error: " . $response->getBody());
89
-    }
90
-
91
-    $resp = json_decode($response->getBody(), TRUE);
92
-    if ($resp['status'] == "OK") {
93
-        return $resp['managerof'] === true;
94
-    } else {
95
-        // this shouldn't happen, but in case it does just fake it.
96
-        return false;
97
-    }
98
-}
99
-
100
-/**
101
- * Get an array of UIDs the given UID is a manager of.
102
- * @param int $manageruid The UID of the manager to find employees for.
103
- * @return [int]
104
- */
105
-function getManagedUIDs($manageruid) {
106
-    $client = new GuzzleHttp\Client();
107
-
108
-    $response = $client
109
-            ->request('POST', PORTAL_API, [
110
-        'form_params' => [
111
-            'key' => PORTAL_KEY,
112
-            'action' => "getmanaged",
113
-            'uid' => $manageruid
114
-        ]
115
-    ]);
116
-
117
-    if ($response->getStatusCode() > 299) {
118
-        sendError("Login server error: " . $response->getBody());
119
-    }
120
-
121
-    $resp = json_decode($response->getBody(), TRUE);
122
-    if ($resp['status'] == "OK") {
123
-        return $resp['employees'];
124
-    } else {
125
-        return [];
126
-    }
127
-}

+ 9
- 10
mobile/index.php View File

@@ -14,8 +14,6 @@ $access_permission = null;
14 14
 
15 15
 require __DIR__ . "/../required.php";
16 16
 
17
-require __DIR__ . "/../lib/login.php";
18
-
19 17
 header('Content-Type: application/json');
20 18
 header('Access-Control-Allow-Origin: *');
21 19
 
@@ -73,7 +71,7 @@ function mobile_valid($username, $code) {
73 71
 }