From 5b7ab6594636d1be9e5e0aec9f7d59a141eacdda Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Fri, 14 Dec 2018 21:16:31 -0700 Subject: [PATCH 1/9] Make better API system, use new AccountHub API --- api.php | 33 +------- api/.htaccess | 5 ++ api/actions/ping.php | 9 +++ api/apisettings.php | 15 ++++ api/functions.php | 123 ++++++++++++++++++++++++++++++ api/index.php | 77 +++++++++++++++++++ lib/AccountHubApi.lib.php | 54 +++++++++++++ lib/Login.lib.php | 30 +------- lib/Notifications.lib.php | 30 +++----- lib/User.lib.php | 155 +++----------------------------------- mobile/index.php | 43 +++-------- settings.template.php | 2 +- 12 files changed, 314 insertions(+), 262 deletions(-) create mode 100644 api/.htaccess create mode 100644 api/actions/ping.php create mode 100644 api/apisettings.php create mode 100644 api/functions.php create mode 100644 api/index.php create mode 100644 lib/AccountHubApi.lib.php diff --git a/api.php b/api.php index 03178ea..b45877d 100644 --- a/api.php +++ b/api.php @@ -4,35 +4,4 @@ * 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/. */ -/** - * Simple JSON API to allow other apps to access data from this app. - * - * Requests can be sent via either GET or POST requests. POST is recommended - * as it has a lower chance of being logged on the server, exposing unencrypted - * user passwords. - */ -require __DIR__ . '/required.php'; -header("Content-Type: application/json"); - -$username = $VARS['username']; -$password = $VARS['password']; -$user = User::byUsername($username); -if ($user->exists() !== true || Login::auth($username, $password) !== Login::LOGIN_OK) { - header("HTTP/1.1 403 Unauthorized"); - die("\"403 Unauthorized\""); -} - -// query max results -$max = 20; -if (preg_match("/^[0-9]+$/", $VARS['max']) === 1 && $VARS['max'] <= 1000) { - $max = (int) $VARS['max']; -} - -switch ($VARS['action']) { - case "ping": - $out = ["status" => "OK", "maxresults" => $max, "pong" => true]; - exit(json_encode($out)); - default: - header("HTTP/1.1 400 Bad Request"); - die("\"400 Bad Request\""); -} \ No newline at end of file +require __DIR__ . "/api/index.php"; \ No newline at end of file diff --git a/api/.htaccess b/api/.htaccess new file mode 100644 index 0000000..9a4efe4 --- /dev/null +++ b/api/.htaccess @@ -0,0 +1,5 @@ +# Rewrite for Nextcloud Notes API + + RewriteEngine on + RewriteRule ([a-zA-Z0-9]+) index.php?action=$1 [PT] + \ No newline at end of file diff --git a/api/actions/ping.php b/api/actions/ping.php new file mode 100644 index 0000000..c764967 --- /dev/null +++ b/api/actions/ping.php @@ -0,0 +1,9 @@ + [ + "load" => "ping.php", + "vars" => [ + ] + ] +]; diff --git a/api/functions.php b/api/functions.php new file mode 100644 index 0000000..78e84c1 --- /dev/null +++ b/api/functions.php @@ -0,0 +1,123 @@ + 5) { + for ($i = 2; $i < strlen($key) - 2; $i++) { + $resp[$i] = "*"; + } + } + return $resp; +} + +/** + * Check if the request is allowed + * @global type $VARS + * @global type $database + * @return bool true if the request should continue, false if the request is bad + */ +function authenticate(): bool { + global $VARS, $database; + if (empty($VARS['key'])) { + return false; + } else { + $key = $VARS['key']; + if ($database->has('apikeys', ['key' => $key]) !== TRUE) { + engageRateLimit(); + http_response_code(403); + Log::insert(LogType::API_BAD_KEY, null, "Key: " . $key); + return false; + } + } + return true; +} + +function checkVars($vars, $or = false) { + global $VARS; + $ok = []; + foreach ($vars as $key => $val) { + if (strpos($key, "OR") === 0) { + checkVars($vars[$key], true); + continue; + } + + // Only check type of optional variables if they're set, and don't + // mark them as bad if they're not set + if (strpos($key, " (optional)") !== false) { + $key = str_replace(" (optional)", "", $key); + if (empty($VARS[$key])) { + continue; + } + } else { + if (empty($VARS[$key])) { + $ok[$key] = false; + continue; + } + } + $checkmethod = "is_$val"; + if ($checkmethod($VARS[$key]) !== true) { + $ok[$key] = false; + } else { + $ok[$key] = true; + } + } + if ($or) { + $success = false; + $bad = ""; + foreach ($ok as $k => $v) { + if ($v) { + $success = true; + break; + } else { + $bad = $k; + } + } + if (!$success) { + http_response_code(400); + die("400 Bad request: variable $bad is missing or invalid"); + } + } else { + foreach ($ok as $key => $bool) { + if (!$bool) { + http_response_code(400); + die("400 Bad request: variable $key is missing or invalid"); + } + } + } +} diff --git a/api/index.php b/api/index.php new file mode 100644 index 0000000..a930798 --- /dev/null +++ b/api/index.php @@ -0,0 +1,77 @@ + 1) { + $VARS["action"] = $route[0]; + } + if (count($route) >= 2 && strpos($route[1], "?") !== 0) { + $VARS["key"] = $route[1]; + + for ($i = 2; $i < count($route); $i++) { + $key = explode("=", $route[$i], 2)[0]; + $val = explode("=", $route[$i], 2)[1]; + $VARS[$key] = $val; + } + } + + if (strpos($route[count($route) - 1], "?") === 0) { + $morevars = explode("&", substr($route[count($route) - 1], 1)); + foreach ($morevars as $var) { + $key = explode("=", $var, 2)[0]; + $val = explode("=", $var, 2)[1]; + $VARS[$key] = $val; + } + } +} + +if (!authenticate()) { + http_response_code(403); + die("403 Unauthorized"); +} + +if (empty($VARS['action'])) { + http_response_code(404); + die("404 No action specified"); +} + +if (!isset($APIS[$VARS['action']])) { + http_response_code(404); + die("404 Action not defined"); +} + +$APIACTION = $APIS[$VARS["action"]]; + +if (!file_exists(__DIR__ . "/actions/" . $APIACTION["load"])) { + http_response_code(404); + die("404 Action not found"); +} + +if (!empty($APIACTION["vars"])) { + checkVars($APIACTION["vars"]); +} + +require_once __DIR__ . "/actions/" . $APIACTION["load"]; diff --git a/lib/AccountHubApi.lib.php b/lib/AccountHubApi.lib.php new file mode 100644 index 0000000..06fae3b --- /dev/null +++ b/lib/AccountHubApi.lib.php @@ -0,0 +1,54 @@ + $action, + "key" => PORTAL_KEY + ]; + if (!is_null($data)) { + $content = array_merge($content, $data); + } + $options = [ + 'http' => [ + 'method' => 'POST', + 'content' => json_encode($content), + 'header' => "Content-Type: application/json\r\n" . + "Accept: application/json\r\n", + "ignore_errors" => true + ] + ]; + + $context = stream_context_create($options); + $result = file_get_contents(PORTAL_API, false, $context); + $response = json_decode($result, true); + if ($result === false || !AccountHubApi::checkHttpRespCode($http_response_header) || json_last_error() != JSON_ERROR_NONE) { + if ($throwex) { + throw new Exception($result); + } else { + sendError($result); + } + } + return $response; + } + + private static function checkHttpRespCode(array $headers): bool { + foreach ($headers as $header) { + if (preg_match("/HTTP\/[0-9]\.[0-9] [0-9]{3}.*/", $header)) { + $respcode = explode(" ", $header)[1] * 1; + if ($respcode >= 200 && $respcode < 300) { + return true; + } + } + } + return false; + } + +} diff --git a/lib/Login.lib.php b/lib/Login.lib.php index fe22a38..b136c6c 100644 --- a/lib/Login.lib.php +++ b/lib/Login.lib.php @@ -74,21 +74,7 @@ class Login { */ 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); + $resp = AccountHubApi::get("ping"); if ($resp['status'] == "OK") { return true; } else { @@ -107,19 +93,7 @@ class Login { */ 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; - } + $resp = AccountHubApi::get("ping", null, true); return false; } catch (Exception $e) { return false; diff --git a/lib/Notifications.lib.php b/lib/Notifications.lib.php index c1d93a9..812af26 100644 --- a/lib/Notifications.lib.php +++ b/lib/Notifications.lib.php @@ -32,27 +32,15 @@ class Notifications { $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); + $resp = AccountHubApi::get("addnotification", [ + 'uid' => $user->getUID(), + 'title' => $title, + 'content' => $content, + 'timestamp' => $timestamp, + 'url' => $url, + 'sensitive' => $sensitive + ] + ); if ($resp['status'] == "OK") { return $resp['id'] * 1; } else { diff --git a/lib/User.lib.php b/lib/User.lib.php index 7852e31..752cc88 100644 --- a/lib/User.lib.php +++ b/lib/User.lib.php @@ -17,22 +17,7 @@ class User { 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); + $resp = AccountHubApi::get("userexists", ["uid" => $uid]); if ($resp['status'] == "OK" && $resp['exists'] === true) { $this->exists = true; } else { @@ -43,22 +28,7 @@ class User { 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); + $resp = AccountHubApi::get("userinfo", ["uid" => $uid]); if ($resp['status'] == "OK") { $this->uid = $resp['data']['uid'] * 1; $this->username = $resp['data']['username']; @@ -71,22 +41,7 @@ class User { } 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); + $resp = AccountHubApi::get("userinfo", ["username" => $username]); if (!isset($resp['status'])) { sendError("Login server error: " . $resp); } @@ -105,22 +60,8 @@ class User { 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); + $resp = AccountHubApi::get("hastotp", ['username' => $this->username]); if ($resp['status'] == "OK") { return $resp['otp'] == true; } else { @@ -150,23 +91,7 @@ class User { * @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); + $resp = AccountHubApi::get("auth", ['username' => $this->username, 'password' => $password]); if ($resp['status'] == "OK") { return true; } else { @@ -178,23 +103,8 @@ class User { 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); + $resp = AccountHubApi::get("verifytotp", ['username' => $this->username, 'code' => $code]); if ($resp['status'] == "OK") { return $resp['valid']; } else { @@ -209,23 +119,7 @@ class User { * @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); + $resp = AccountHubApi::get("permission", ['username' => $this->username, 'code' => $code]); if ($resp['status'] == "OK") { return $resp['has_permission']; } else { @@ -238,23 +132,7 @@ class User { * @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); + $resp = AccountHubApi::get("acctstatus", ['username' => $this->username]); if ($resp['status'] == "OK") { return AccountStatus::fromString($resp['account']); } else { @@ -263,23 +141,8 @@ class User { } 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 = AccountHubApi::get("alertemail", ['username' => $this->username, 'appname' => SITE_TITLE]); - $resp = json_decode($response->getBody(), TRUE); if ($resp['status'] == "OK") { return true; } else { diff --git a/mobile/index.php b/mobile/index.php index de36d52..dbb10f3 100644 --- a/mobile/index.php +++ b/mobile/index.php @@ -23,21 +23,7 @@ if ($VARS['action'] == "ping") { } function mobile_enabled() { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - 'action' => "mobileenabled" - ] - ]); - - if ($response->getStatusCode() > 299) { - return false; - } - - $resp = json_decode($response->getBody(), TRUE); + $resp = AccountHubApi::get("mobileenabled"); if ($resp['status'] == "OK" && $resp['mobile'] === TRUE) { return true; } else { @@ -46,26 +32,15 @@ function mobile_enabled() { } function mobile_valid($username, $code) { - $client = new GuzzleHttp\Client(); - - $response = $client - ->request('POST', PORTAL_API, [ - 'form_params' => [ - 'key' => PORTAL_KEY, - "code" => $code, - "username" => $username, - 'action' => "mobilevalid" - ] - ]); + try { + $resp = AccountHubApi::get("mobilevalid", ["code" => $code, "username" => $username], true); - if ($response->getStatusCode() > 299) { - return false; - } - - $resp = json_decode($response->getBody(), TRUE); - if ($resp['status'] == "OK" && $resp['valid'] === TRUE) { - return true; - } else { + if ($resp['status'] == "OK" && $resp['valid'] === TRUE) { + return true; + } else { + return false; + } + } catch (Exception $ex) { return false; } } diff --git a/settings.template.php b/settings.template.php index 2732b99..8711f01 100644 --- a/settings.template.php +++ b/settings.template.php @@ -22,7 +22,7 @@ define("SITE_TITLE", "Web App Template"); // URL of the AccountHub API endpoint -define("PORTAL_API", "http://localhost/accounthub/api.php"); +define("PORTAL_API", "http://localhost/accounthub/api/"); // URL of the AccountHub home page define("PORTAL_URL", "http://localhost/accounthub/home.php"); // AccountHub API Key From 61d660be69bda5a88f10b58e9d71fca532b442cb Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Thu, 20 Dec 2018 23:24:47 -0700 Subject: [PATCH 2/9] Add FormBuilder --- langs/en/titles.json | 4 +- lib/FormBuilder.lib.php | 257 ++++++++++++++++++++++++++++++++++++++++ pages.php | 10 +- pages/form.php | 24 ++++ static/js/form.js | 16 +++ 5 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 lib/FormBuilder.lib.php create mode 100644 pages/form.php create mode 100644 static/js/form.js diff --git a/langs/en/titles.json b/langs/en/titles.json index 6fbf103..4d745d0 100644 --- a/langs/en/titles.json +++ b/langs/en/titles.json @@ -1,4 +1,4 @@ { - "home": "Home", - "test": "Test" + "Home": "Home", + "Form": "Form" } diff --git a/lib/FormBuilder.lib.php b/lib/FormBuilder.lib.php new file mode 100644 index 0000000..fddbff2 --- /dev/null +++ b/lib/FormBuilder.lib.php @@ -0,0 +1,257 @@ +title = $title; + $this->icon = $icon; + $this->action = $action; + $this->method = $method; + } + + /** + * Set the title of the form. + * @param string $title + */ + public function setTitle(string $title) { + $this->title = $title; + } + + /** + * Set the icon for the form. + * @param string $icon FontAwesome icon (example: "fas fa-toilet-paper") + */ + public function setIcon(string $icon) { + $this->icon = $icon; + } + + /** + * Set the URL the form will submit to. + * @param string $action + */ + public function setAction(string $action) { + $this->action = $action; + } + + /** + * Set the form submission method (GET, POST, etc) + * @param string $method + */ + public function setMethod(string $method = "POST") { + $this->method = $method; + } + + /** + * Set the form ID. + * @param string $id + */ + public function setID(string $id = "editform") { + $this->id = $id; + } + + /** + * Add an input to the form. + * + * @param string $name Element name + * @param string $value Element value + * @param string $type Input type (text, number, date, select, tel...) + * @param bool $required If the element is required for form submission. + * @param string $id Element ID + * @param array $options Array of [value => text] pairs for a select element + * @param string $label Text label to display near the input + * @param string $icon FontAwesome icon (example: "fas fa-toilet-paper") + * @param int $width Bootstrap column width for the input, out of 12. + * @param int $minlength Minimum number of characters for the input. + * @param int $maxlength Maximum number of characters for the input. + * @param string $pattern Regex pattern for custom client-side validation. + * @param string $error Message to show if the input doesn't validate. + */ + public function addInput(string $name, string $value = "", string $type = "text", bool $required = true, string $id = null, array $options = null, string $label = "", string $icon = "", int $width = 4, int $minlength = 1, int $maxlength = 100, string $pattern = "", string $error = "") { + $item = [ + "name" => $name, + "value" => $value, + "type" => $type, + "required" => $required, + "label" => $label, + "icon" => $icon, + "width" => $width, + "minlength" => $minlength, + "maxlength" => $maxlength + ]; + if (!empty($id)) { + $item["id"] = $id; + } + if (!empty($options) && $type == "select") { + $item["options"] = $options; + } + if (!empty($pattern)) { + $item["pattern"] = $pattern; + } + if (!empty($error)) { + $item["error"] = $error; + } + $this->items[] = $item; + } + + /** + * Add a button to the form. + * + * @param string $text Text string to show on the button. + * @param string $icon FontAwesome icon to show next to the text. + * @param string $href If not null, the button will actually be a hyperlink. + * @param string $type Usually "button" or "submit". Ignored if $href is set. + * @param string $id The element ID. + * @param string $name The element name for the button. + * @param string $value The form value for the button. Ignored if $name is null. + * @param string $class The CSS classes for the button, if a standard success-colored one isn't right. + */ + public function addButton(string $text, string $icon = "", string $href = null, string $type = "button", string $id = null, string $name = null, string $value = "", string $class = "btn btn-success") { + $button = [ + "text" => $text, + "icon" => $icon, + "class" => $class, + "type" => $type, + "id" => $id, + "href" => $href, + "name" => $name, + "value" => $value + ]; + $this->buttons[] = $button; + } + + /** + * Add a hidden input. + * @param string $name + * @param string $value + */ + public function addHiddenInput(string $name, string $value) { + $this->hiddenitems[$name] = $value; + } + + /** + * Generate the form HTML. + * @param bool $echo If false, returns HTML string instead of outputting it. + */ + public function generate(bool $echo = true) { + $html = << +
+

+
+ $this->title +
+

+ +
+
+HTMLTOP; + + foreach ($this->items as $item) { + $required = $item["required"] ? "required" : ""; + $id = empty($item["id"]) ? "" : "id=\"$item[id]\""; + $pattern = empty($item["pattern"]) ? "" : "pattern=\"$item[pattern]\""; + + $itemhtml = ""; + $itemhtml .= << +
+ +
+
+ +
+ITEMTOP; + if (empty($item['type']) || $item['type'] != "select") { + $itemhtml .= << +INPUT; + } else { + $itemhtml .= <<"; + } + + if (!empty($item["error"])) { + $itemhtml .= << + $item[error] +
+ERROR; + } + $itemhtml .= << +
+
\n +ITEMBOTTOM; + $html .= $itemhtml; + } + + $html .= << +
+HTMLBOTTOM; + + if (!empty($this->buttons)) { + $html .= "\n
"; + foreach ($this->buttons as $btn) { + $btnhtml = ""; + $inner = " $btn[text]"; + $id = empty($btn['id']) ? "" : "id=\"$btn[id]\""; + if (!empty($btn['href'])) { + $btnhtml = "$inner"; + } else { + $name = empty($btn['name']) ? "" : "name=\"$btn[name]\""; + $value = (!empty($btn['name']) && !empty($btn['value'])) ? "value=\"$btn[value]\"" : ""; + $btnhtml = ""; + } + $html .= "\n $btnhtml"; + } + $html .= "\n
"; + } + + $html .= "\n
"; + foreach ($this->hiddenitems as $name => $value) { + $value = htmlentities($value); + $html .= "\n "; + } + $html .= "\n\n"; + + if ($echo) { + echo $html; + } + return $html; + } + +} diff --git a/pages.php b/pages.php index ad2a84f..fe7cc1c 100644 --- a/pages.php +++ b/pages.php @@ -7,11 +7,19 @@ // List of pages and metadata define("PAGES", [ "home" => [ - "title" => "home", + "title" => "Home", "navbar" => true, "icon" => "fas fa-home" ], "404" => [ "title" => "404 error" + ], + "form" => [ + "title" => "Form", + "navbar" => true, + "icon" => "fas fa-file-alt", + "scripts" => [ + "static/js/form.js" + ] ] ]); \ No newline at end of file diff --git a/pages/form.php b/pages/form.php new file mode 100644 index 0000000..7cd7fdd --- /dev/null +++ b/pages/form.php @@ -0,0 +1,24 @@ +setID("sampleform"); + +$form->addHiddenInput("page", "form"); + +$form->addInput("name", "John", "text", true, null, null, "Your name", "fas fa-user", 6, 5, 20, "John(ny)?|Steve", "Invalid name, please enter John, Johnny, or Steve."); +$form->addInput("location", "", "select", true, null, ["1" => "Here", "2" => "There"], "Location", "fas fa-map-marker"); + +$form->addButton("Submit", "fas fa-save", null, "submit", "savebtn"); + +$form->generate(); \ No newline at end of file diff --git a/static/js/form.js b/static/js/form.js new file mode 100644 index 0000000..21c9f53 --- /dev/null +++ b/static/js/form.js @@ -0,0 +1,16 @@ +/* + * 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/. + */ + + +$("#savebtn").click(function (event) { + var form = $("#sampleform"); + + if (form[0].checkValidity() === false) { + event.preventDefault(); + event.stopPropagation(); + } + form.addClass('was-validated'); +}); \ No newline at end of file From f1a85f47fd6b7e8caef810d7f77db377ba50b29c Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Thu, 20 Dec 2018 23:25:34 -0700 Subject: [PATCH 3/9] Add comment --- api.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api.php b/api.php index b45877d..d68b923 100644 --- a/api.php +++ b/api.php @@ -4,4 +4,6 @@ * 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/. */ + +// Load in new API from legacy location (a.k.a. here) require __DIR__ . "/api/index.php"; \ No newline at end of file From c179ed7ebbbe04670c0146889ac1d026e8770976 Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Thu, 20 Dec 2018 23:45:45 -0700 Subject: [PATCH 4/9] Make settings.php an array, not a bunch of defines --- app.php | 10 +++--- index.php | 16 ++++----- lib/AccountHubApi.lib.php | 6 ++-- lib/User.lib.php | 8 +++-- required.php | 22 ++++++------ settings.template.php | 75 ++++++++++++++++----------------------- 6 files changed, 65 insertions(+), 72 deletions(-) diff --git a/app.php b/app.php index b74d422..607437e 100644 --- a/app.php +++ b/app.php @@ -39,7 +39,7 @@ header("Link: ; rel=preload; as=script", fals - <?php echo SITE_TITLE; ?> + <?php echo $SETTINGS['site_title']; ?> @@ -127,7 +127,7 @@ END; - + diff --git a/index.php b/index.php index 1f8f76f..f3a816c 100644 --- a/index.php +++ b/index.php @@ -21,7 +21,7 @@ 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"))) { + if (!$SETTINGS['captcha']['enabled'] || ($SETTINGS['captcha']['enabled'] && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], $SETTINGS['captcha']['server'] . "/api.php"))) { $autherror = ""; $user = User::byUsername($VARS['username']); if ($user->exists()) { @@ -41,7 +41,7 @@ if (Login::checkLoginServer()) { break; case "ALERT_ON_ACCESS": $mail_resp = $user->sendAlertEmail(); - if (DEBUG) { + if ($SETTINGS['debug']) { var_dump($mail_resp); } $username_ok = true; @@ -105,15 +105,15 @@ header("Link: ; rel=preload; as=script", fals - <?php echo SITE_TITLE; ?> + <?php echo $SETTINGS['site_title']; ?> - - + + @@ -140,7 +140,7 @@ header("Link: ; rel=preload; as=script", fals ?> " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus />
" required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
- +

@@ -165,8 +165,8 @@ header("Link: ; rel=preload; as=script", fals diff --git a/lib/AccountHubApi.lib.php b/lib/AccountHubApi.lib.php index 06fae3b..4d23f9e 100644 --- a/lib/AccountHubApi.lib.php +++ b/lib/AccountHubApi.lib.php @@ -9,9 +9,11 @@ class AccountHubApi { public static function get(string $action, array $data = null, bool $throwex = false) { + global $SETTINGS; + $content = [ "action" => $action, - "key" => PORTAL_KEY + "key" => $SETTINGS['accounthub']['key'] ]; if (!is_null($data)) { $content = array_merge($content, $data); @@ -27,7 +29,7 @@ class AccountHubApi { ]; $context = stream_context_create($options); - $result = file_get_contents(PORTAL_API, false, $context); + $result = file_get_contents($SETTINGS['accounthub']['api'], false, $context); $response = json_decode($result, true); if ($result === false || !AccountHubApi::checkHttpRespCode($http_response_header) || json_last_error() != JSON_ERROR_NONE) { if ($throwex) { diff --git a/lib/User.lib.php b/lib/User.lib.php index 752cc88..763acc5 100644 --- a/lib/User.lib.php +++ b/lib/User.lib.php @@ -140,8 +140,12 @@ class User { } } - function sendAlertEmail(string $appname = SITE_TITLE) { - $resp = AccountHubApi::get("alertemail", ['username' => $this->username, 'appname' => SITE_TITLE]); + function sendAlertEmail(string $appname = null) { + global $SETTINGS; + if (is_null($appname)) { + $appname = $SETTINGS['site_title']; + } + $resp = AccountHubApi::get("alertemail", ['username' => $this->username, 'appname' => $SETTINGS['site_title']]); if ($resp['status'] == "OK") { return true; diff --git a/required.php b/required.php index 6753401..3fe1060 100644 --- a/required.php +++ b/required.php @@ -32,7 +32,7 @@ session_start(); // stick some cookies in it // renew session cookie setcookie(session_name(), session_id(), time() + $session_length, "/", false, false); -$captcha_server = (CAPTCHA_ENABLED === true ? preg_replace("/http(s)?:\/\//", "", CAPTCHA_SERVER) : ""); +$captcha_server = ($SETTINGS['captcha']['enabled'] === true ? preg_replace("/http(s)?:\/\//", "", $SETTINGS['captcha']['server']) : ""); if ($_SESSION['mobile'] === TRUE) { header("Content-Security-Policy: " . "default-src 'self';" @@ -69,7 +69,7 @@ foreach ($libs as $lib) { require_once $lib; } -$Strings = new Strings(LANGUAGE); +$Strings = new Strings($SETTINGS['language']); /** * Kill off the running process and spit out an error message @@ -93,7 +93,7 @@ function sendError($error) { . "

" . htmlspecialchars($error) . "

"); } -date_default_timezone_set(TIMEZONE); +date_default_timezone_set($SETTINGS['timezone']); // Database settings // Also inits database and stuff @@ -102,12 +102,12 @@ use Medoo\Medoo; $database; try { $database = new Medoo([ - 'database_type' => DB_TYPE, - 'database_name' => DB_NAME, - 'server' => DB_SERVER, - 'username' => DB_USER, - 'password' => DB_PASS, - 'charset' => DB_CHARSET + 'database_type' => $SETTINGS['database']['type'], + 'database_name' => $SETTINGS['database']['name'], + 'server' => $SETTINGS['database']['server'], + 'username' => $SETTINGS['database']['user'], + 'password' => $SETTINGS['database']['password'], + 'charset' => $SETTINGS['database']['charset'] ]); } catch (Exception $ex) { //header('HTTP/1.1 500 Internal Server Error'); @@ -115,7 +115,7 @@ try { } -if (!DEBUG) { +if (!$SETTINGS['debug']) { error_reporting(0); } else { error_reporting(E_ALL); @@ -158,7 +158,7 @@ function checkDBError($specials = []) { function redirectIfNotLoggedIn() { if ($_SESSION['loggedin'] !== TRUE) { - header('Location: ' . URL . '/index.php'); + header('Location: ' . $SETTINGS['url'] . '/index.php'); die(); } } diff --git a/settings.template.php b/settings.template.php index 8711f01..2e346e3 100644 --- a/settings.template.php +++ b/settings.template.php @@ -1,47 +1,34 @@ false, + "database" => [ + "type" => "mysql", + "name" => "app", + "server" => "localhost", + "user" => "app", + "password" => "", + "charset" => "utf8" + ], + "site_title" => "Web App Template", + "accounthub" => [ + "api" => "http://localhost/accounthub/api/", + "home" => "http://localhost/accounthub/home.php", + "key" => "123" + ], + "timezone" => "America/Denver", + "captcha" => [ + "enabled" => false, + "server" => "https://captcheck.netsyms.com" + ], + "language" => "en", + "footer_text" => "", + "copyright" => "Netsyms Technologies", + "url" => "." +]; \ No newline at end of file From 129efd13c737ed714e1f5a83b4cb2e122a3569f3 Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Thu, 20 Dec 2018 23:54:25 -0700 Subject: [PATCH 5/9] Add documentation comments to settings --- settings.template.php | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/settings.template.php b/settings.template.php index 2e346e3..75a1896 100644 --- a/settings.template.php +++ b/settings.template.php @@ -6,8 +6,18 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// Settings for the app. +// Copy to settings.php and customize. + $SETTINGS = [ + // Whether to output debugging info like PHP notices, warnings, + // and stacktraces. + // Turning this on in production is a security risk and can sometimes break + // things, such as JSON output where extra content is not expected. "debug" => false, + + // Database connection settings + // See http://medoo.in/api/new for info "database" => [ "type" => "mysql", "name" => "app", @@ -16,19 +26,41 @@ $SETTINGS = [ "password" => "", "charset" => "utf8" ], + + // Name of the app. "site_title" => "Web App Template", + + // Settings for connecting to the AccountHub server. "accounthub" => [ + // URL for the API endpoint "api" => "http://localhost/accounthub/api/", + // URL of the home page "home" => "http://localhost/accounthub/home.php", + // API key "key" => "123" ], + + // For supported values, see http://php.net/manual/en/timezones.php "timezone" => "America/Denver", + + // Use Captcheck on login screen to slow down bots + // https://captcheck.netsyms.com "captcha" => [ "enabled" => false, "server" => "https://captcheck.netsyms.com" ], + + // Language to use for localization. See langs folder to add a language. "language" => "en", + + // Shown in the footer of all the pages. "footer_text" => "", + + // Also shown in the footer, but with "Copyright " in front. "copyright" => "Netsyms Technologies", + + // Base URL for building links relative to the location of the app. + // Only used when there's no good context for the path. + // The default is almost definitely fine. "url" => "." -]; \ No newline at end of file +]; From 3f32258ba0d77fa20073a1499f14dee357a09c09 Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Thu, 20 Dec 2018 23:58:35 -0700 Subject: [PATCH 6/9] Fix bug --- api/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/index.php b/api/index.php index a930798..59d0c2a 100644 --- a/api/index.php +++ b/api/index.php @@ -18,7 +18,7 @@ if ($_SERVER['REQUEST_METHOD'] != "GET") { $requestbody = file_get_contents('php://input'); $requestjson = json_decode($requestbody, TRUE); if (json_last_error() == JSON_ERROR_NONE) { - $requestdata = array_merge($requestdata, $requestjson); + $VARS = array_merge($VARS, $requestjson); } // If we're not using the old api.php file, allow more flexible requests From a559901ac04f320d0e75370638692de466175228 Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Sat, 22 Dec 2018 16:57:45 -0700 Subject: [PATCH 7/9] Redirect to AccountHub for user login --- action.php | 2 +- index.php | 223 +++++++++++++++------------------------------ langs/en/core.json | 14 +-- 3 files changed, 75 insertions(+), 164 deletions(-) diff --git a/action.php b/action.php index d1ea966..67b230b 100644 --- a/action.php +++ b/action.php @@ -33,6 +33,6 @@ function returnToSender($msg, $arg = "") { switch ($VARS['action']) { case "signout": session_destroy(); - header('Location: index.php'); + header('Location: index.php?logout=1'); die("Logged out."); } \ No newline at end of file diff --git a/index.php b/index.php index f3a816c..9e9468e 100644 --- a/index.php +++ b/index.php @@ -1,7 +1,9 @@ get("no access permission", false); -} +if (!empty($_GET['logout'])) { + // Show a logout message instead of immediately redirecting to login flow + ?> + + + + -/* Authenticate user */ -$userpass_ok = false; -$multiauth = false; -if (Login::checkLoginServer()) { - if (empty($VARS['progress'])) { - // Easy way to remove "undefined" warnings. - } else if ($VARS['progress'] == "1") { - if (!$SETTINGS['captcha']['enabled'] || ($SETTINGS['captcha']['enabled'] && Login::verifyCaptcha($VARS['captcheck_session_code'], $VARS['captcheck_selected_answer'], $SETTINGS['captcha']['server'] . "/api.php"))) { - $autherror = ""; - $user = User::byUsername($VARS['username']); - if ($user->exists()) { - $status = $user->getStatus()->getString(); - switch ($status) { - case "LOCKED_OR_DISABLED": - $alert = $Strings->get("account locked", false); - break; - case "TERMINATED": - $alert = $Strings->get("account terminated", false); - break; - case "CHANGE_PASSWORD": - $alert = $Strings->get("password expired", false); - break; - case "NORMAL": - $username_ok = true; - break; - case "ALERT_ON_ACCESS": - $mail_resp = $user->sendAlertEmail(); - if ($SETTINGS['debug']) { - var_dump($mail_resp); - } - $username_ok = true; - break; - default: - if (!empty($error)) { - $alert = $error; - } else { - $alert = $Strings->get("login error", false); - } - break; - } - 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 { - $alert = $Strings->get("login incorrect", false); - } - } - } else { // User does not exist anywhere - $alert = $Strings->get("login incorrect", false); - } - } else { - $alert = $Strings->get("captcha error", false); + <?php echo $SETTINGS['site_title']; ?> + + + + + + + +
+
+
+

get("You have been logged out.") ?>

+
+ + +
+
+ + + $_SESSION["login_code"]]); + if ($uidinfo["status"] == "ERROR") { + throw new Exception(); } - if ($user->check2fa($VARS['authcode'])) { + if (is_numeric($uidinfo['uid'])) { + $user = new User($uidinfo['uid'] * 1); Session::start($user); + $_SESSION["login_code"] = null; header('Location: app.php'); die("Logged in, go to app.php"); } else { - $alert = $Strings->get("2fa incorrect", false); + throw new Exception(); } + } catch (Exception $ex) { + $redirecttologin = true; } -} else { - $alert = $Strings->get("login server unavailable", false); } -header("Link: ; rel=preload; as=style", false); -header("Link: ; rel=preload; as=style", false); -header("Link: ; rel=preload; as=style", false); -header("Link: ; rel=preload; as=style", false); -header("Link: ; rel=preload; as=script", false); -header("Link: ; rel=preload; as=script", false); -?> - - - - - - - <?php echo $SETTINGS['site_title']; ?> +if ($redirecttologin) { + try { + $codedata = AccountHubApi::get("getloginkey", ["appname" => $SETTINGS["site_title"]]); - + if ($codedata['status'] != "OK") { + throw new Exception($Strings->get("login server unavailable", false)); + } - - - - - - - - -
-
- -
-
-
-
-
-
get("sign in"); ?>
-
- -
- -
- - " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus />
- " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
- -
-
- - - -
- get("2fa prompt"); ?> -
- " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus />
- - - - -
-
-
-
- - - - - - \ No newline at end of file + $_SESSION["login_code"] = $codedata["code"]; + + header("Location: " . $codedata["loginurl"] . "?code=" . htmlentities($codedata["code"]) . "&redirect=" . htmlentities($redirecturl)); + } catch (Exception $ex) { + sendError($ex->getMessage()); + } +} \ No newline at end of file diff --git a/langs/en/core.json b/langs/en/core.json index 5e55996..20eac0a 100644 --- a/langs/en/core.json +++ b/langs/en/core.json @@ -1,17 +1,7 @@ { - "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.", + "You have been logged out.": "You have been logged out.", + "Log in again": "Log in again", "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", From ba1369d842bbc4d2fe00bce552ba9da2cb37aabe Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Sat, 22 Dec 2018 21:26:57 -0700 Subject: [PATCH 8/9] Add app icon to login flow --- index.php | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/index.php b/index.php index 9e9468e..8f3b4b9 100644 --- a/index.php +++ b/index.php @@ -28,14 +28,30 @@ if (!empty($_GET['logout'])) {
+
+ +
+

get("You have been logged out.") ?>

@@ -79,13 +95,15 @@ if (empty($_SESSION["login_code"])) { if ($redirecttologin) { try { - $codedata = AccountHubApi::get("getloginkey", ["appname" => $SETTINGS["site_title"]]); + $urlbase = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : ""); + $iconurl = $urlbase . str_replace("index.php", "", $_SERVER["REQUEST_URI"]) . "static/img/logo.svg"; + $codedata = AccountHubApi::get("getloginkey", ["appname" => $SETTINGS["site_title"], "appicon" => $iconurl]); if ($codedata['status'] != "OK") { throw new Exception($Strings->get("login server unavailable", false)); } - $redirecturl = $url = (isset($_SERVER['HTTPS']) ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (($_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) ? ":" . $_SERVER['SERVER_PORT'] : "") . $_SERVER['REQUEST_URI']; + $redirecturl = $urlbase . $_SERVER['REQUEST_URI']; $_SESSION["login_code"] = $codedata["code"]; From 016c71d30de7a246874e3483320e411ab1c7b244 Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Sat, 22 Dec 2018 22:38:50 -0700 Subject: [PATCH 9/9] Fix index.php not redirecting to app.php when already logged in --- index.php | 1 + 1 file changed, 1 insertion(+) diff --git a/index.php b/index.php index 8f3b4b9..e4cafd8 100644 --- a/index.php +++ b/index.php @@ -10,6 +10,7 @@ require_once __DIR__ . "/required.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'); + die(); } if (!empty($_GET['logout'])) {