Browse Source

Merge BusinessAppTemplate

master
Skylar Ittner 1 year ago
parent
commit
56a223c430

+ 1
- 0
README.md View File

@@ -39,3 +39,4 @@ Installing
8. Set the URL of this app ("URL")
9. Copy webroot.htaccess to your webroot and adjust paths if needed
10. Run `composer install` (or `composer.phar install`) to install dependency libraries
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 @@
*/
require_once __DIR__ . "/required.php";
require_once __DIR__ . "/lib/util.php";
require_once __DIR__ . "/lib/login.php";

if ($VARS['action'] !== "signout") {
dieifnotloggedin();
@@ -37,9 +36,11 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST' && empty($_POST) &&
returnToSender("upload_too_big");
}

$user = new User($_SESSION['uid']);

switch ($VARS['action']) {
case "newpage":
if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_EDIT")) {
if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_EDIT")) {
returnToSender("no_permission");
}
if (is_empty($VARS['siteid']) || !$database->has("sites", ["siteid" => $VARS['siteid']])) {
@@ -80,7 +81,7 @@ switch ($VARS['action']) {
returnToSender("page_added", $VARS['siteid'] . "|" . $database->id());
break;
case "pagesettings":
if (!account_has_permission($_SESSION['username'], "SITEWRITER")) {
if (!$user->hasPermission("SITEWRITER")) {
returnToSender("no_permission");
}
if (is_empty($VARS['siteid']) || !$database->has("sites", ["siteid" => $VARS['siteid']])) {
@@ -138,7 +139,7 @@ switch ($VARS['action']) {
returnToSender("settings_saved", $VARS['siteid'] . "|" . $VARS['pageid']);
break;
case "sitesettings":
if (!account_has_permission($_SESSION['username'], "SITEWRITER")) {
if (!$user->hasPermission("SITEWRITER")) {
returnToSender("no_permission");
}
if (!is_empty($VARS['siteid'])) {
@@ -198,8 +199,8 @@ switch ($VARS['action']) {
break;
case "saveedits":
header("Content-Type: application/json");
if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_EDIT")) {
exit(json_encode(['status' => "ERROR", 'message' => lang("no permission", false)]));
if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_EDIT")) {
exit(json_encode(['status' => "ERROR", 'message' => $Strings->get("no permission", false)]));
}
$slug = $VARS['slug'];
$site = $VARS['site'];
@@ -228,7 +229,7 @@ switch ($VARS['action']) {
exit(json_encode(["status" => "OK"]));
break;
case "deletemessage":
if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_CONTACT")) {
if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_CONTACT")) {
returnToSender("no_permission");
}
if ($database->count('messages', ["mid" => $VARS['id']]) !== 1) {
@@ -238,7 +239,7 @@ switch ($VARS['action']) {
returnToSender("message_deleted");
break;
case "fileupload":
if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_FILES")) {
if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_FILES")) {
returnToSender("no_permission");
}
$destpath = FILE_UPLOAD_PATH . $VARS['path'];
@@ -310,7 +311,7 @@ switch ($VARS['action']) {
returnToSender("upload_success", "&path=" . $VARS['path']);
break;
case "newfolder":
if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_FILES")) {
if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_FILES")) {
returnToSender("no_permission");
}
$foldername = preg_replace("/[^a-z0-9_\-]/", "_", strtolower($VARS['folder']));
@@ -322,7 +323,7 @@ switch ($VARS['action']) {
returnToSender("folder_not_created", "&path=" . $VARS['path']);
break;
case "filedelete":
if (!account_has_permission($_SESSION['username'], "SITEWRITER") && !account_has_permission($_SESSION['username'], "SITEWRITER_FILES")) {
if (!$user->hasPermission("SITEWRITER") && !$user->hasPermission("SITEWRITER_FILES")) {
returnToSender("no_permission");
}
$file = FILE_UPLOAD_PATH . $VARS['file'];

+ 2
- 4
api.php View File

@@ -12,17 +12,15 @@
* user passwords.
*/
require __DIR__ . '/required.php';
require_once __DIR__ . '/lib/login.php';
require_once __DIR__ . '/lib/userinfo.php';
header("Content-Type: application/json");

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

// query max results
$max = 20;

+ 8
- 8
app.php View File

@@ -28,10 +28,10 @@ header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
header("Link: <static/css/app.css>; rel=preload; as=style", false);
header("Link: <static/css/fa-svg-with-js.css>; rel=preload; as=style", false);
header("Link: <static/css/svg-with-js.min.css>; rel=preload; as=style", false);
header("Link: <static/js/fontawesome-all.min.js>; rel=preload; as=script", false);
header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
@@ -47,7 +47,7 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/material-color/material-color.min.css" rel="stylesheet">
<link href="static/css/app.css" rel="stylesheet">
<link href="static/css/fa-svg-with-js.css" rel="stylesheet">
<link href="static/css/svg-with-js.min.css" rel="stylesheet">
<script nonce="<?php echo $SECURE_NONCE; ?>">
FontAwesomeConfig = {autoAddCss: false}
</script>
@@ -69,9 +69,9 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
if (isset($_GET['msg']) && !is_empty($_GET['msg']) && array_key_exists($_GET['msg'], MESSAGES)) {
// optional string generation argument
if (!isset($_GET['arg']) || is_empty($_GET['arg'])) {
$alertmsg = lang(MESSAGES[$_GET['msg']]['string'], false);
$alertmsg = $Strings->get(MESSAGES[$_GET['msg']]['string'], false);
} else {
$alertmsg = lang2(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
$alertmsg = $Strings->build(MESSAGES[$_GET['msg']]['string'], ["arg" => strip_tags($_GET['arg'])], false);
}
$alerttype = MESSAGES[$_GET['msg']]['type'];
$alerticon = "square-o";
@@ -146,7 +146,7 @@ END;
if (isset($pg['icon'])) {
?><i class="<?php echo $pg['icon']; ?> fa-fw"></i> <?php
}
lang($pg['title']);
$Strings->get($pg['title']);
?>
</a>
</span>
@@ -163,7 +163,7 @@ END;
</span>
<span class="nav-item mr-auto py-<?php echo $navbar_breakpoint; ?>-0">
<a class="nav-link py-<?php echo $navbar_breakpoint; ?>-0" href="action.php?action=signout">
<i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php lang("sign out") ?></span>
<i class="fas fa-sign-out-alt fa-fw"></i><span>&nbsp;<?php $Strings->get("sign out") ?></span>
</a>
</span>
</div>
@@ -182,7 +182,7 @@ END;
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/app.js"></script>
<?php
// custom page scripts

+ 32
- 32
composer.lock View File

@@ -4,21 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "f30e715ebe71e016a347feec9c9dc5bf",
"content-hash": "9efd5e7ff4f253d9ef07ae1535880ffb",
"packages": [
{
"name": "catfan/medoo",
"version": "v1.5.6",
"version": "v1.5.7",
"source": {
"type": "git",
"url": "https://github.com/catfan/Medoo.git",
"reference": "f77a93f72864e892c99d1033b8733e5da8fb0b3b"
"reference": "8d90cba0e8ff176028847527d0ea76fe41a06ecf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/catfan/Medoo/zipball/f77a93f72864e892c99d1033b8733e5da8fb0b3b",
"reference": "f77a93f72864e892c99d1033b8733e5da8fb0b3b",
"url": "https://api.github.com/repos/catfan/Medoo/zipball/8d90cba0e8ff176028847527d0ea76fe41a06ecf",
"reference": "8d90cba0e8ff176028847527d0ea76fe41a06ecf",
"shasum": ""
},
"require": {
@@ -64,20 +63,20 @@
"sql",
"sqlite"
],
"time": "2018-03-26 17:54:24"
"time": "2018-06-14T18:59:08+00:00"
},
{
"name": "composer/ca-bundle",
"version": "1.1.1",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
"reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169"
"reference": "46afded9720f40b9dc63542af4e3e43a1177acb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/d2c0a83b7533d6912e8d516756ebd34f893e9169",
"reference": "d2c0a83b7533d6912e8d516756ebd34f893e9169",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/46afded9720f40b9dc63542af4e3e43a1177acb0",
"reference": "46afded9720f40b9dc63542af4e3e43a1177acb0",
"shasum": ""
},
"require": {
@@ -120,7 +119,7 @@
"ssl",
"tls"
],
"time": "2018-03-29 19:57:20"
"time": "2018-08-08T08:57:40+00:00"
},
{
"name": "geoip2/geoip2",
@@ -172,20 +171,20 @@
"geolocation",
"maxmind"
],
"time": "2018-04-10 15:32:59"
"time": "2018-04-10T15:32:59+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.3.2",
"version": "6.3.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "68d0ea14d5a3f42a20e87632a5f84931e2709c90"
"reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/68d0ea14d5a3f42a20e87632a5f84931e2709c90",
"reference": "68d0ea14d5a3f42a20e87632a5f84931e2709c90",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
"reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
"shasum": ""
},
"require": {
@@ -195,7 +194,7 @@
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
"psr/log": "^1.0"
},
"suggest": {
@@ -237,7 +236,7 @@
"rest",
"web service"
],
"time": "2018-03-26 16:33:04"
"time": "2018-04-22T15:46:56+00:00"
},
{
"name": "guzzlehttp/promises",
@@ -288,7 +287,7 @@
"keywords": [
"promise"
],
"time": "2016-12-20 10:07:11"
"time": "2016-12-20T10:07:11+00:00"
},
{
"name": "guzzlehttp/psr7",
@@ -353,7 +352,7 @@
"uri",
"url"
],
"time": "2017-03-20 17:10:46"
"time": "2017-03-20T17:10:46+00:00"
},
{
"name": "hughbertd/oauth2-unsplash",
@@ -405,7 +404,7 @@
"oauth2",
"single sign on"
],
"time": "2017-12-14 13:08:42"
"time": "2017-12-14T13:08:42+00:00"
},
{
"name": "league/oauth2-client",
@@ -472,7 +471,7 @@
"oauth2",
"single sign on"
],
"time": "2018-01-13 05:27:58"
"time": "2018-01-13T05:27:58+00:00"
},
{
"name": "maxmind-db/reader",
@@ -528,7 +527,7 @@
"geolocation",
"maxmind"
],
"time": "2018-02-21 21:23:33"
"time": "2018-02-21T21:23:33+00:00"
},
{
"name": "maxmind/web-service-common",
@@ -574,20 +573,20 @@
],
"description": "Internal MaxMind Web Service API",
"homepage": "https://github.com/maxmind/web-service-common-php",
"time": "2018-02-12 22:31:54"
"time": "2018-02-12T22:31:54+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v2.0.12",
"version": "v2.0.17",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb"
"reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/258c89a6b97de7dfaf5b8c7607d0478e236b04fb",
"reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d",
"reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d",
"shasum": ""
},
"require": {
@@ -619,10 +618,11 @@
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"time": "2018-04-04 21:24:14"
"time": "2018-07-04T16:31:37+00:00"
},
{
"name": "phpmailer/phpmailer",
@@ -688,7 +688,7 @@
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"time": "2018-03-27 13:49:45"
"time": "2018-03-27T13:49:45+00:00"
},
{
"name": "psr/http-message",
@@ -738,7 +738,7 @@
"request",
"response"
],
"time": "2016-08-06 14:39:51"
"time": "2016-08-06T14:39:51+00:00"
},
{
"name": "unsplash/unsplash",
@@ -797,7 +797,7 @@
}
],
"description": "Wrapper to access the Unsplash API and photo library",
"time": "2018-03-30 17:45:15"
"time": "2018-03-30T17:45:15+00:00"
}
],
"packages-dev": [],

+ 58
- 47
index.php View File

@@ -5,87 +5,98 @@

require_once __DIR__ . "/required.php";

require_once __DIR__ . "/lib/login.php";

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

if (isset($_GET['permissionerror'])) {
$alert = lang("no access permission", false);
$alert = $Strings->get("no access permission", false);
}

/* Authenticate user */
$userpass_ok = false;
$multiauth = false;
if (checkLoginServer()) {
if (!empty($VARS['progress']) && $VARS['progress'] == "1") {
if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && verifyCaptcheck($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
$errmsg = "";
if (authenticate_user($VARS['username'], $VARS['password'], $errmsg)) {
switch (get_account_status($VARS['username'])) {
if (Login::checkLoginServer()) {
if (empty($VARS['progress'])) {
// Easy way to remove "undefined" warnings.
} else if ($VARS['progress'] == "1") {
if (!CAPTCHA_ENABLED || (CAPTCHA_ENABLED && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], CAPTCHA_SERVER . "/api.php"))) {
$autherror = "";
$user = User::byUsername($VARS['username']);
if ($user->exists()) {
$status = $user->getStatus()->getString();
switch ($status) {
case "LOCKED_OR_DISABLED":
$alert = lang("account locked", false);
$alert = $Strings->get("account locked", false);
break;
case "TERMINATED":
$alert = lang("account terminated", false);
$alert = $Strings->get("account terminated", false);
break;
case "CHANGE_PASSWORD":
$alert = lang("password expired", false);
$alert = $Strings->get("password expired", false);
break;
case "NORMAL":
$userpass_ok = true;
$username_ok = true;
break;
case "ALERT_ON_ACCESS":
sendLoginAlertEmail($VARS['username']);
$userpass_ok = true;
$mail_resp = $user->sendAlertEmail();
if (DEBUG) {
var_dump($mail_resp);
}
$username_ok = true;
break;
default:
if (!is_empty($error)) {
$alert = $error;
} else {
$alert = $Strings->get("login error", false);
}
break;
}
if ($userpass_ok) {
$_SESSION['passok'] = true; // stop logins using only username and authcode
if (userHasTOTP($VARS['username'])) {
$multiauth = true;
if ($username_ok) {
if ($user->checkPassword($VARS['password'])) {
$_SESSION['passok'] = true; // stop logins using only username and authcode
if ($user->has2fa()) {
$multiauth = true;
} else {
Session::start($user);
header('Location: app.php');
die("Logged in, go to app.php");
}
} else {
doLoginUser($VARS['username'], $VARS['password']);
header('Location: app.php');
die("Logged in, go to app.php");
$alert = $Strings->get("login incorrect", false);
}
}
} else {
if (!is_empty($errmsg)) {
$alert = lang2("login server error", ['arg' => $errmsg], false);
} else {
$alert = lang("login incorrect", false);
}
} else { // User does not exist anywhere
$alert = $Strings->get("login incorrect", false);
}
} else {
$alert = lang("captcha error", false);
$alert = $Strings->get("captcha error", false);
}
} else if (!empty($VARS['progress']) && $VARS['progress'] == "2") {
} else if ($VARS['progress'] == "2") {
$user = User::byUsername($VARS['username']);
if ($_SESSION['passok'] !== true) {
// stop logins using only username and authcode
sendError("Password integrity check failed!");
}
if (verifyTOTP($VARS['username'], $VARS['authcode'])) {
if (doLoginUser($VARS['username'])) {
header('Location: app.php');
die("Logged in, go to app.php");
} else {
$alert = lang("login server user data error", false);
}
if ($user->check2fa($VARS['authcode'])) {
Session::start($user);
header('Location: app.php');
die("Logged in, go to app.php");
} else {
$alert = lang("2fa incorrect", false);
$alert = $Strings->get("2fa incorrect", false);
}
}
} else {
$alert = lang("login server unavailable", false);
$alert = $Strings->get("login server unavailable", false);
}
header("Link: <static/fonts/Roboto.css>; rel=preload; as=style", false);
header("Link: <static/css/bootstrap.min.css>; rel=preload; as=style", false);
header("Link: <static/css/material-color/material-color.min.css>; rel=preload; as=style", false);
header("Link: <static/css/index.css>; rel=preload; as=style", false);
header("Link: <static/js/jquery-3.3.1.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
header("Link: <static/js/bootstrap.bundle.min.js>; rel=preload; as=script", false);
?>
<!DOCTYPE html>
<html>
@@ -114,7 +125,7 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
<div class="row justify-content-center">
<div class="card col-11 col-xs-11 col-sm-8 col-md-6 col-lg-4">
<div class="card-body">
<h5 class="card-title"><?php lang("sign in"); ?></h5>
<h5 class="card-title"><?php $Strings->get("sign in"); ?></h5>
<form action="" method="POST">
<?php
if (!empty($alert)) {
@@ -127,8 +138,8 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);

if ($multiauth != true) {
?>
<input type="text" class="form-control" name="username" placeholder="<?php lang("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php lang("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<input type="text" class="form-control" name="username" placeholder="<?php $Strings->get("username"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="password" class="form-control" name="password" placeholder="<?php $Strings->get("password"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" /><br />
<?php if (CAPTCHA_ENABLED) { ?>
<div class="captcheck_container" data-stylenonce="<?php echo $SECURE_NONCE; ?>"></div>
<br />
@@ -138,16 +149,16 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
} else if ($multiauth) {
?>
<div class="alert alert-info">
<?php lang("2fa prompt"); ?>
<?php $Strings->get("2fa prompt"); ?>
</div>
<input type="text" class="form-control" name="authcode" placeholder="<?php lang("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="text" class="form-control" name="authcode" placeholder="<?php $Strings->get("authcode"); ?>" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus /><br />
<input type="hidden" name="progress" value="2" />
<input type="hidden" name="username" value="<?php echo $VARS['username']; ?>" />
<?php
}
?>
<button type="submit" class="btn btn-primary">
<?php lang("continue"); ?>
<?php $Strings->get("continue"); ?>
</button>
</form>
</div>
@@ -159,6 +170,6 @@ header("Link: <static/js/bootstrap.min.js>; rel=preload; as=script", false);
</div>
</div>
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/bootstrap.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
</body>
</html>

+ 0
- 146
lang/en_us.php View File

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

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

define("STRINGS", [
"sign in" => "Sign In",
"username" => "Username",
"password" => "Password",
"continue" => "Continue",
"authcode" => "Authentication code",
"2fa prompt" => "Enter the six-digit code from your mobile authenticator app.",
"2fa incorrect" => "Authentication code incorrect.",
"login incorrect" => "Login incorrect.",
"login server unavailable" => "Login server unavailable. Try again later or contact technical support.",
"account locked" => "This account has been disabled. Contact technical support.",
"password expired" => "You must change your password before continuing.",
"account terminated" => "Account terminated. Access denied.",
"account state error" => "Your account state is not stable. Log out, restart your browser, and try again.",
"welcome user" => "Welcome, {user}!",
"sign out" => "Sign out",
"settings" => "Settings",
"options" => "Options",
"404 error" => "404 Error",
"page not found" => "Page not found.",
"invalid parameters" => "Invalid request parameters.",
"login server error" => "The login server returned an error: {arg}",
"login server user data error" => "The login server refused to provide account information. Try again or contact technical support.",
"captcha error" => "There was a problem with the CAPTCHA (robot test). Try again.",
"no access permission" => "You do not have permission to access this system.",
"actions" => "Actions",
"no permission" => "You don't have permission to do that.",
"home" => "Home",
"editor" => "Editor",
"sites" => "Sites",
"theme" => "Theme",
"name" => "Name",
"new site" => "New Site",
"site name" => "Site Name",
"url" => "URL",
"adding site" => "Creating site {site}",
"editing site" => "Editing {site}",
"settings saved" => "Settings saved",
"theme type" => "Theme type",
"single page" => "Single page",
"multiple page" => "Multiple page",
"templates" => "Templates",
"template" => "Template",
"color styles" => "Color styles",
"save" => "Save",
"edit" => "Edit",
"view" => "View",
"preview" => "Preview",
"cancel" => "Cancel",
"save needed" => "Press Save to see recent changes.",
"saved" => "Saved",
"icon" => "Icon",
"image" => "Image",
"link" => "Link",
"text" => "Text",
"select page or enter url" => "Select a page or enter URL",
"edit component" => "Edit component",
"default" => "Default",
"page added" => "Page added.",
"chosen page id slug already taken" => "Chosen page ID (slug) already taken. Choose another.",
"template missing" => "Template missing from theme.",
"new page" => "New Page",
"title" => "Title",
"page id" => "Page ID (slug)",
"add page" => "Add page",
"page settings" => "Page Settings",
"analytics" => "Analytics",
"today" => "Today",
"this week" => "This Week",
"visit" => "visit",
"visits" => "visits",
"page view" => "page view",
"page views" => "page views",
"site" => "Site",
"filter by site" => "Filter by site",
"all sites" => "All Sites",
"filter" => "Filter",
"start date" => "Start date",
"end date" => "End date",
"recent actions" => "Recent Actions",
"overview" => "Overview",
"views per visit" => "views per visit",
"visits over time" => "Visits Over Time",
"page views over time" => "Page Views Over Time",
"page ranking" => "Page Ranking",
"x views" => "{views} views",
"no data" => "No data.",
"visitor map" => "Visitor Map",
"enable built-in analytics" => "Enable built-in analytics",
"disable built-in analytics" => "Disable built-in analytics",
"extra code" => "Extra code (inserted in site head)",
"company info" => "Company Info",
"phone" => "Phone",
"address" => "Address",
"email" => "Email",
"social links" => "Social Links",
"site info" => "Site Info",
"loading" => "Loading...",
"current" => "Current",
"messages" => "Messages",
"message" => "Message",
"date" => "Date",
"message deleted" => "Message deleted.",
"files" => "Files",
"browse" => "Browse",
"upload" => "Upload",
"operation cancelled for security reasons" => "Operation cancelled for security reasons.",
"upload successful" => "Upload successful.",
"upload warning" => "Upload finished with some problems:<br>{arg}",
"destination folder does not exist" => "Destination folder does not exist.",
"destination folder does not allow uploads" => "Destination folder does not allow uploads.",
"uploaded data too large" => "Uploaded data too large.",
"undeletable file" => "The file could not be deleted.",
"folder not empty" => "Folder must be empty to be deleted.",
"file not deleted" => "The file could not be deleted.",
"file deleted" => "File deleted.",
"folder deleted" => "Folder deleted.",
"folder created" => "Folder created.",
"folder not created" => "Folder not created.",
"nothing here" => "There doesn't seem to be anything here...",
"navbar options" => "Site Menu Options",
"in navbar" => "Add page to menu",
"navbar title" => "Page title for menu",
"navbar position" => "Menu position (drag to change position):",
"remove image" => "Remove image",
"site footer links" => "Site Footer Links",
"uploaded files" => "Uploaded Files",
"stock photos" => "Free Stock Photos",
"load more" => "Load more",
"search images" => "Search images",
"x results" => "{results} results",
"reply" => "Reply",
"delete" => "Delete",
"new folder" => "New Folder",
"new" => "New",
"search" => "Search",
"no results" => "No results.",
"contact form" => "Contact Form",
"contact form messages will be forwarded to this email address" => "Contact form messages will be forwarded to this email address, if it is set.",
]);

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

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

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

@@ -0,0 +1,115 @@
{
"actions": "Actions",
"no permission": "You don't have permission to do that.",
"editor": "Editor",
"sites": "Sites",
"theme": "Theme",
"name": "Name",
"new site": "New Site",
"site name": "Site Name",
"url": "URL",
"adding site": "Creating site {site}",
"editing site": "Editing {site}",
"settings saved": "Settings saved",
"theme type": "Theme type",
"single page": "Single page",
"multiple page": "Multiple page",
"templates": "Templates",
"template": "Template",
"color styles": "Color styles",
"save": "Save",
"edit": "Edit",
"view": "View",
"preview": "Preview",
"cancel": "Cancel",
"save needed": "Press Save to see recent changes.",
"saved": "Saved",
"icon": "Icon",
"image": "Image",
"link": "Link",
"text": "Text",
"select page or enter url": "Select a page or enter URL",
"edit component": "Edit component",
"default": "Default",
"page added": "Page added.",
"chosen page id slug already taken": "Chosen page ID (slug) already taken. Choose another.",
"template missing": "Template missing from theme.",
"new page": "New Page",
"title": "Title",
"page id": "Page ID (slug)",
"add page": "Add page",
"page settings": "Page Settings",
"analytics": "Analytics",
"today": "Today",
"this week": "This Week",
"visit": "visit",
"visits": "visits",
"page view": "page view",
"page views": "page views",
"site": "Site",
"filter by site": "Filter by site",
"all sites": "All Sites",
"filter": "Filter",
"start date": "Start date",
"end date": "End date",
"recent actions": "Recent Actions",
"overview": "Overview",
"views per visit": "views per visit",
"visits over time": "Visits Over Time",
"page views over time": "Page Views Over Time",
"page ranking": "Page Ranking",
"x views": "{views} views",
"no data": "No data.",
"visitor map": "Visitor Map",
"enable built-in analytics": "Enable built-in analytics",
"disable built-in analytics": "Disable built-in analytics",
"extra code": "Extra code (inserted in site head)",
"company info": "Company Info",
"phone": "Phone",
"address": "Address",
"email": "Email",
"social links": "Social Links",
"site info": "Site Info",
"loading": "Loading...",
"current": "Current",
"messages": "Messages",
"message": "Message",
"date": "Date",
"message deleted": "Message deleted.",
"files": "Files",
"browse": "Browse",
"upload": "Upload",
"operation cancelled for security reasons": "Operation cancelled for security reasons.",
"upload successful": "Upload successful.",
"upload warning": "Upload finished with some problems:<br>{arg}",
"destination folder does not exist": "Destination folder does not exist.",
"destination folder does not allow uploads": "Destination folder does not allow uploads.",
"uploaded data too large": "Uploaded data too large.",
"undeletable file": "The file could not be deleted.",
"folder not empty": "Folder must be empty to be deleted.",
"file not deleted": "The file could not be deleted.",
"file deleted": "File deleted.",
"folder deleted": "Folder deleted.",
"folder created": "Folder created.",
"folder not created": "Folder not created.",
"nothing here": "There doesn't seem to be anything here...",
"navbar options": "Site Menu Options",
"in navbar": "Add page to menu",
"navbar title": "Page title for menu",
"navbar position": "Menu position (drag to change position):",
"remove image": "Remove image",
"site footer links": "Site Footer Links",
"uploaded files": "Uploaded Files",
"stock photos": "Free Stock Photos",
"load more": "Load more",
"search images": "Search images",
"x results": "{results} results",
"reply": "Reply",
"delete": "Delete",
"new folder": "New Folder",
"new": "New",
"search": "Search",
"no results": "No results.",
"contact form": "Contact Form",
"contact form messages will be forwarded to this email address": "Contact form messages will be forwarded to this email address, if it is set."
}

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

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

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


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

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

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

class IncorrectPasswordException extends Exception {
public function __construct(string $message = "Incorrect password.", int $code = 0, \Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}

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

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

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

class IPUtils {

/**
* Check if a given ipv4 address is in a given cidr
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
* @author Thorsten Ott <https://gist.github.com/tott/7684443>
*/
public static function ip4_in_cidr($ip, $cidr) {
if (strpos($cidr, '/') == false) {
$cidr .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $cidr, $netmask ) = explode('/', $cidr, 2);
$range_decimal = ip2long($cidr);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, ( 32 - $netmask)) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}

/**
* Check if a given ipv6 address is in a given cidr
* @param string $ip IP to check in IPV6 format
* @param string $cidr CIDR netmask
* @return boolean true if the IP is in this range, false otherwise.
* @author MW. <https://stackoverflow.com/a/7952169>
*/
public static function ip6_in_cidr($ip, $cidr) {
$address = inet_pton($ip);
$subnetAddress = inet_pton(explode("/", $cidr)[0]);
$subnetMask = explode("/", $cidr)[1];

$addr = str_repeat("f", $subnetMask / 4);
switch ($subnetMask % 4) {
case 0:
break;
case 1:
$addr .= "8";
break;
case 2:
$addr .= "c";
break;
case 3:
$addr .= "e";
break;
}
$addr = str_pad($addr, 32, '0');
$addr = pack("H*", $addr);

$binMask = $addr;
return ($address & $binMask) == $subnetAddress;
}

/**
* Check if the REMOTE_ADDR is on Cloudflare's network.
* @return boolean true if it is, otherwise false
*/
public static function validateCloudflare() {
if (filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Using IPv6
$cloudflare_ips_v6 = [
"2400:cb00::/32",
"2405:8100::/32",
"2405:b500::/32",
"2606:4700::/32",
"2803:f800::/32",
"2c0f:f248::/32",
"2a06:98c0::/29"
];
$valid = false;
foreach ($cloudflare_ips_v6 as $cidr) {
if (ip6_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
} else {
// Using IPv4
$cloudflare_ips_v4 = [
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"104.16.0.0/12",
"108.162.192.0/18",
"131.0.72.0/22",
"141.101.64.0/18",
"162.158.0.0/15",
"172.64.0.0/13",
"173.245.48.0/20",
"188.114.96.0/20",
"190.93.240.0/20",
"197.234.240.0/22",
"198.41.128.0/17"
];
$valid = false;
foreach ($cloudflare_ips_v4 as $cidr) {
if (ip4_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
}
return $valid;
}

/**
* Makes a good guess at the client's real IP address.
*
* @return string Client IP or `0.0.0.0` if we can't find anything
*/
public static function getClientIP() {
// If CloudFlare is in the mix, we should use it.
// Check if the request is actually from CloudFlare before trusting it.
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
if (validateCloudflare()) {
return $_SERVER["HTTP_CF_CONNECTING_IP"];
}
}

if (isset($_SERVER["REMOTE_ADDR"])) {
return $_SERVER["REMOTE_ADDR"];
}

return "0.0.0.0"; // This will not happen unless we aren't a web server
}

}

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

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

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

class Login {

const BAD_USERPASS = 1;
const BAD_2FA = 2;
const ACCOUNT_DISABLED = 3;
const LOGIN_OK = 4;

public static function auth(string $username, string $password, string $twofa = ""): int {
global $database;
$username = strtolower($username);

$user = User::byUsername($username);

if (!$user->exists()) {
return Login::BAD_USERPASS;
}
if (!$user->checkPassword($password)) {
return Login::BAD_USERPASS;
}

if ($user->has2fa()) {
if (!$user->check2fa($twofa)) {
return Login::BAD_2FA;
}
}

switch ($user->getStatus()->get()) {
case AccountStatus::TERMINATED:
return Login::BAD_USERPASS;
case AccountStatus::LOCKED_OR_DISABLED:
return Login::ACCOUNT_DISABLED;
case AccountStatus::NORMAL:
default:
return Login::LOGIN_OK;
}

return Login::LOGIN_OK;
}

public static function verifyCaptcha(string $session, string $answer, string $url): bool {
$data = [
'session_id' => $session,
'answer_id' => $answer,
'action' => "verify"
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data)
]
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$resp = json_decode($result, TRUE);
if (!$resp['result']) {
return false;
} else {
return true;
}
}

/**
* Check the login server API for sanity
* @return boolean true if OK, else false
*/
public static function checkLoginServer() {
try {
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() != 200) {
return false;
}

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

/**
* Checks if the given AccountHub API key is valid by attempting to
* access the API with it.
* @param String $key The API key to check
* @return boolean TRUE if the key is valid, FALSE if invalid or something went wrong
*/
function checkAPIKey($key) {
try {
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() === 200) {
return true;
}
return false;
} catch (Exception $e) {
return false;
}
}

}

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

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

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

class Notifications {

/**
* Add a new notification.
* @global $database
* @param User $user
* @param string $title
* @param string $content
* @param string $timestamp If left empty, the current date and time will be used.
* @param string $url
* @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.
* @return int The newly-created notification ID.
* @throws Exception
*/
public static function add(User $user, string $title, string $content, string $timestamp = "", string $url = "", bool $sensitive = false): int {
global $Strings;
if ($user->exists()) {
if (empty($title) || empty($content)) {
throw new Exception($Strings->get("invalid parameters", false));
}

$timestamp = date("Y-m-d H:i:s");
if (!empty($timestamp)) {
$timestamp = date("Y-m-d H:i:s", strtotime($timestamp));
}

$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "addnotification",
'uid' => $user->getUID(),
'title' => $title,
'content' => $content,
'timestamp' => $timestamp,
'url' => $url,
'sensitive' => $sensitive
]
]);

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
return $resp['id'] * 1;
} else {
return false;
}
}
throw new Exception($Strings->get("user does not exist", false));
}

}

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

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

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

class Session {

public static function start(User $user) {
$_SESSION['username'] = $user->getUsername();
$_SESSION['uid'] = $user->getUID();
$_SESSION['email'] = $user->getEmail();
$_SESSION['realname'] = $user->getName();
$_SESSION['loggedin'] = true;
}

}

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

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

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

/**
* Provides translated language strings.
*/
class Strings {

private $language = "en";
private $strings = [];

public function __construct($language = "en") {
if (!preg_match("/[a-zA-Z\_\-]+/", $language)) {
throw new Exception("Invalid language code $language");
}

$this->load("en");

if (file_exists(__DIR__ . "/../langs/$language/")) {
$this->language = $language;
$this->load($language);
} else {
trigger_error("Language $language could not be found.", E_USER_WARNING);
}
}

/**
* Load all JSON files for the specified language.
* @param string $language
*/
private function load(string $language) {
$files = glob(__DIR__ . "/../langs/$language/*.json");
foreach ($files as $file) {
$strings = json_decode(file_get_contents($file), true);
foreach ($strings as $key => $val) {
if (array_key_exists($key, $this->strings)) {
trigger_error("Language key \"$key\" is defined more than once.", E_USER_WARNING);
}
$this->strings[$key] = $val;
}
}
}

/**
* Add language strings dynamically.
* @param array $strings ["key" => "value", ...]
*/
public function addStrings(array $strings) {
foreach ($strings as $key => $val) {
$this->strings[$key] = $val;
}
}

/**
* I18N string getter. If the key isn't found, it outputs the key itself.
* @param string $key
* @param bool $echo True to echo the result, false to return it. Default is true.
* @return string
*/
public function get(string $key, bool $echo = true): string {
$str = $key;
if (array_key_exists($key, $this->strings)) {
$str = $this->strings[$key];
} else {
trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING);
}

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

/**
* I18N string getter (with builder). If the key doesn't exist, outputs the key itself.
* @param string $key
* @param array $replace key-value array of replacements.
* If the string value is "hello {abc}" and you give ["abc" => "123"], the
* result will be "hello 123".
* @param bool $echo True to echo the result, false to return it. Default is true.
* @return string
*/
public function build(string $key, array $replace, bool $echo = true): string {
$str = $key;
if (array_key_exists($key, $this->strings)) {
$str = $this->strings[$key];
} else {
trigger_error("Language key \"$key\" does not exist in " . $this->language, E_USER_WARNING);
}

foreach ($replace as $find => $repl) {
$str = str_replace("{" . $find . "}", $repl, $str);
}

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

/**
* Builds and returns a JSON key:value string for the supplied array of keys.
* @param array $keys ["key1", "key2", ...]
*/
public function getJSON(array $keys): string {
$strings = [];
foreach ($keys as $k) {
$strings[$k] = $this->get($k, false);
}
return json_encode($strings);
}

}

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

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

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

class User {

private $uid = null;
private $username;
private $email;
private $realname;
private $has2fa = false;
private $exists = false;

public function __construct(int $uid, string $username = "") {
// Check if user exists
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK" && $resp['exists'] === true) {
$this->exists = true;
} else {
$this->uid = $uid;
$this->username = $username;
$this->exists = false;
}

if ($this->exists) {
// Get user info
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if ($resp['status'] == "OK") {
$this->uid = $resp['data']['uid'] * 1;
$this->username = $resp['data']['username'];
$this->email = $resp['data']['email'];
$this->realname = $resp['data']['name'];
} else {
sendError("Login server error: " . $resp['msg']);
}
}
}

public static function byUsername(string $username): User {
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

$resp = json_decode($response->getBody(), TRUE);
if (!isset($resp['status'])) {
sendError("Login server error: " . $resp);
}
if ($resp['status'] == "OK") {
return new self($resp['data']['uid'] * 1);
} else {
return new self(-1, $username);
}
}

public function exists(): bool {
return $this->exists;
}

public function has2fa(): bool {
if (!$this->exists) {
return false;
}
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

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

function getUsername() {
return $this->username;
}

function getUID() {
return $this->uid;
}

function getEmail() {
return $this->email;
}

function getName() {
return $this->realname;
}

/**
* Check the given plaintext password against the stored hash.
* @param string $password
* @return bool
*/
function checkPassword(string $password): bool {
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

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

function check2fa(string $code): bool {
if (!$this->has2fa) {
return true;
}
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

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

/**
* Check if the given username has the given permission (or admin access)
* @global $database $database
* @param string $code
* @return boolean TRUE if the user has the permission (or admin access), else FALSE
*/
function hasPermission(string $code): bool {
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

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

/**
* Get the account status.
* @return \AccountStatus
*/
function getStatus(): AccountStatus {

$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

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

function sendAlertEmail(string $appname = SITE_TITLE) {
$client = new GuzzleHttp\Client();

$response = $client
->request('POST', PORTAL_API, [
'form_params' => [
'key' => PORTAL_KEY,
'action' => "alertemail",
'username' => $this->username,
'appname' => SITE_TITLE
]
]);

if ($response->getStatusCode() > 299) {
return "An unknown error occurred.";
}

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

}

class AccountStatus {

const NORMAL = 1;
const LOCKED_OR_DISABLED = 2;
const CHANGE_PASSWORD = 3;
const TERMINATED = 4;
const ALERT_ON_ACCESS = 5;

private $status;

public function __construct(int $status) {
$this->status = $status;
}

public static function fromString(string $status): AccountStatus {
switch ($status) {
case "NORMAL":
return new self(self::NORMAL);
case "LOCKED_OR_DISABLED":
return new self(self::LOCKED_OR_DISABLED);
case "CHANGE_PASSWORD":
return new self(self::CHANGE_PASSWORD);
case "TERMINATED":
return new self(self::TERMINATED);
case "ALERT_ON_ACCESS":
return new self(self::ALERT_ON_ACCESS);
default:
return new self(0);
}
}

/**
* Get the account status/state as an integer.
* @return int
*/
public function get(): int {
return $this->status;
}

/**
* Get the account status/state as a string representation.
* @return string
*/
public function getString(): string {
switch ($this->status) {
case self::NORMAL:
return "NORMAL";
case self::LOCKED_OR_DISABLED:
return "LOCKED_OR_DISABLED";
case self::CHANGE_PASSWORD:
return "CHANGE_PASSWORD";
case self::TERMINATED:
return "TERMINATED";
case self::ALERT_ON_ACCESS:
return "ALERT_ON_ACCESS";
default:
return "OTHER_" . $this->status;
}
}

}

+ 5
- 5
lib/filepicker.php View File

@@ -35,12 +35,12 @@ if ($enableunsplash) {
<ul class="nav nav-tabs" id="fileBrowserTabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="uploadedFilesTabBtn" data-toggle="tab" href="#uploadedFilesTab">
<i class="fas fa-folder-open"></i> <?php lang('uploaded files'); ?>
<i class="fas fa-folder-open"></i> <?php $Strings->get('uploaded files'); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="unsplashTabBtn" data-toggle="tab" href="#unsplashTab">
<i class="fas fa-image"></i> <?php lang('stock photos'); ?>
<i class="fas fa-image"></i> <?php $Strings->get('stock photos'); ?>
</a>
</li>
</ul>
@@ -60,10 +60,10 @@ if ($enableunsplash) {
<div class="card">
<div class="card-body">
<div class="input-group">
<input type="text" class="form-control" id="unsplashSearch" placeholder="<?php lang("search images"); ?>" />
<input type="text" class="form-control" id="unsplashSearch" placeholder="<?php $Strings->get("search images"); ?>" />
<div class="input-group-append">
<div class="btn btn-primary" id="unsplashSearchBtn">
<i class="fas fa-search"></i> <?php lang("search"); ?>
<i class="fas fa-search"></i> <?php $Strings->get("search"); ?>
</div>
</div>
</div>
@@ -73,7 +73,7 @@ if ($enableunsplash) {
</div>
<div class="card-body">
<button type="button" class="btn btn-primary btn-block" id="unsplashLoadMoreBtn">
<?php lang("load more"); ?>
<?php $Strings->get("load more"); ?>
</button>
</div>
</div>

+ 1
- 1
lib/filepicker_local.php View File

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

+ 2
- 2
lib/filepicker_unsplash.php View File

@@ -39,7 +39,7 @@ $images->
$htmlout = "";

if (count($images) == 0) {
$htmlout = "<div class=\"card text-center\"><div class=\"card-body\"><i class=\"fas fa-search-minus\"></i> " . lang("no results", false) . "</div></div>";
$htmlout = "<div class=\"card text-center\"><div class=\"card-body\"><i class=\"fas fa-search-minus\"></i> " . $Strings->get("no results", false) . "</div></div>";
}

$htmlout .= '<div class="card-columns">';
@@ -70,7 +70,7 @@ $jsonout = [
];

if (!is_null($results)) {
$jsonout['total'] = lang2("x results", ["results" => $results->getTotal()], false);
$jsonout['total'] = $Strings->build("x results", ["results" => $results->getTotal()], false);
$jsonout['pages'] = $results->getTotalPages();
}


+ 0
- 131
lib/iputils.php View File

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

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

/**
* Check if a given ipv4 address is in a given cidr
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
* @author Thorsten Ott <https://gist.github.com/tott/7684443>
*/
function ip4_in_cidr($ip, $cidr) {
if (strpos($cidr, '/') == false) {
$cidr .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $cidr, $netmask ) = explode('/', $cidr, 2);
$range_decimal = ip2long($cidr);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, ( 32 - $netmask)) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}

/**
* Check if a given ipv6 address is in a given cidr
* @param string $ip IP to check in IPV6 format
* @param string $cidr CIDR netmask
* @return boolean true if the IP is in this range, false otherwise.
* @author MW. <https://stackoverflow.com/a/7952169>
*/
function ip6_in_cidr($ip, $cidr) {
$address = inet_pton($ip);
$subnetAddress = inet_pton(explode("/", $cidr)[0]);
$subnetMask = explode("/", $cidr)[1];

$addr = str_repeat("f", $subnetMask / 4);
switch ($subnetMask % 4) {
case 0:
break;
case 1:
$addr .= "8";
break;
case 2:
$addr .= "c";
break;
case 3:
$addr .= "e";
break;
}
$addr = str_pad($addr, 32, '0');
$addr = pack("H*", $addr);

$binMask = $addr;
return ($address & $binMask) == $subnetAddress;
}

/**
* Check if the REMOTE_ADDR is on Cloudflare's network.
* @return boolean true if it is, otherwise false
*/
function validateCloudflare() {
if (filter_var($_SERVER["REMOTE_ADDR"], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Using IPv6
$cloudflare_ips_v6 = [
"2400:cb00::/32",
"2405:8100::/32",
"2405:b500::/32",
"2606:4700::/32",
"2803:f800::/32",
"2c0f:f248::/32",
"2a06:98c0::/29"
];
$valid = false;
foreach ($cloudflare_ips_v6 as $cidr) {
if (ip6_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
} else {
// Using IPv4
$cloudflare_ips_v4 = [
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"104.16.0.0/12",
"108.162.192.0/18",
"131.0.72.0/22",
"141.101.64.0/18",
"162.158.0.0/15",
"172.64.0.0/13",
"173.245.48.0/20",
"188.114.96.0/20",
"190.93.240.0/20",
"197.234.240.0/22",
"198.41.128.0/17"
];
$valid = false;
foreach ($cloudflare_ips_v4 as $cidr) {
if (ip4_in_cidr($_SERVER["REMOTE_ADDR"], $cidr)) {
$valid = true;
break;
}
}
}
return $valid;
}

/**
* Makes a good guess at the client's real IP address.
*
* @return string Client IP or `0.0.0.0` if we can't find anything
*/
function getClientIP() {
// If CloudFlare is in the mix, we should use it.
// Check if the request is actually from CloudFlare before trusting it.
if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
if (validateCloudflare()) {
return $_SERVER["HTTP_CF_CONNECTING_IP"];
}
}

if (isset($_SERVER["REMOTE_ADDR"])) {
return $_SERVER["REMOTE_ADDR"];
}

return "0.0.0.0"; // This will not happen unless we aren't a web server
}

+ 0
- 402
lib/login.php View File

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

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

/**
* Authentication and account functions. Connects to an AccountHub instance.
*/

/**
* Check the login server API for sanity
* @return boolean true if OK, else false
*/
function checkLoginServer() {
try {
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() != 200) {
return false;
}

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

/**
* Checks if the given AccountHub API key is valid by attempting to
* access the API with it.
* @param String $key The API key to check
* @return boolean TRUE if the key is valid, FALSE if invalid or something went wrong
*/
function checkAPIKey($key) {
try {
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() === 200) {
return true;
}
return false;
} catch (Exception $e) {
return false;
}
}

////////////////////////////////////////////////////////////////////////////////
// Account handling //
////////////////////////////////////////////////////////////////////////////////

/**
* Checks the given credentials against the API.
* @param string $username
* @param string $password
* @return boolean True if OK, else false
*/
function authenticate_user($username, $password, &$errmsg) {
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

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

/**
* Check if a username exists.
* @param String $username
*/
function user_exists($username) {
$client = new GuzzleHttp\Client();

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

if ($response->getStatusCode() > 299) {
sendError("Login server error: " . $response->getBody());
}

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