Forking from BootstrapSQLiteBlog

This commit is contained in:
Conner Harkness 2025-07-24 08:37:22 -06:00
parent bf40752d9e
commit eafc2588dc
39 changed files with 4088 additions and 1 deletions

View File

@ -1,2 +1,3 @@
# PlainSQLiteBlog # BootstrapSQLiteBlog
A blogging plugin for the php-webapp-framwork. Clone to the "plugins/" directory to use.

View File

@ -0,0 +1,8 @@
CREATE table if not exists links (
id integer primary key auto_increment,
label text not null,
url text not null,
icon text not null,
position text not null,
visibility text null,
sort integer not null default 0)

View File

@ -0,0 +1,9 @@
CREATE table if not exists posts (
id integer primary key auto_increment,
username text not null,
content text not null,
location text not null,
visibility text null,
created timestamp not null default current_timestamp,
updated timestamp not null default current_timestamp,
sort integer not null default 0)

View File

@ -0,0 +1,5 @@
CREATE table if not exists sessions (
id integer primary key auto_increment,
username text not null,
token text not null,
expires timestamp null)

View File

@ -0,0 +1,4 @@
CREATE table if not exists settings (
id integer primary key auto_increment,
setting text not null,
value text null)

View File

@ -0,0 +1,7 @@
CREATE table if not exists users (
id integer primary key auto_increment,
username text not null,
hash text not null,
can_post integer not null default 0,
is_admin integer not null default 0,
created timestamp not null default current_timestamp)

View File

@ -0,0 +1,4 @@
SELECT *
from posts
where
id = last_insert_id()

View File

@ -0,0 +1,8 @@
CREATE table if not exists links (
id integer primary key autoincrement,
label text not null,
url text not null,
icon text not null,
position text not null,
visibility text null,
sort integer not null default 0)

View File

@ -0,0 +1,9 @@
CREATE table if not exists posts (
id integer primary key autoincrement,
username text not null,
content text not null,
location text not null,
visibility text null,
created timestamp not null default current_timestamp,
updated timestamp not null default current_timestamp,
sort integer not null default 0)

View File

@ -0,0 +1,5 @@
CREATE table if not exists sessions (
id integer primary key autoincrement,
username text not null,
token text not null,
expires timestamp null)

View File

@ -0,0 +1,4 @@
CREATE table if not exists settings (
id integer primary key autoincrement,
setting text not null,
value text null)

View File

@ -0,0 +1,7 @@
CREATE table if not exists users (
id integer primary key autoincrement,
username text not null,
hash text not null,
can_post integer not null default 0,
is_admin integer not null default 0,
created timestamp not null default current_timestamp)

View File

@ -0,0 +1,4 @@
SELECT *
from posts
where
rowid = last_insert_rowid()

View File

@ -0,0 +1,46 @@
$(function() {
FormValidator = {};
FormValidator.onError = function(str)
{
alert(str);
console.log(str);
};
FormValidator.validate = function(callback)
{
var pass = 1;
$("[fv-regex]").each(function(i, x) {
if (pass == 0)
return;
x = $(x);
let input = x.val();
let regex = new RegExp(x.attr("fv-regex"), "g");
if (!input.match(regex))
{
x.addClass("border-danger");
x.focus();
x.select();
let warning = x.attr("fv-warning") ?? "Please correct the highlighted input and try again";
FormValidator.onError(warning)
pass = 0;
return;
}
if (pass == 1)
if (typeof callback === "function")
callback();
x.removeClass("border-danger");
});
};
});

View File

@ -0,0 +1,37 @@
div.searchable-input input
{
width: 100%;
box-sizing: border-box;
}
div.searchable-input div.items
{
padding-top: 0.5em;
padding-bottom: 0.5em;
border: 1px solid #aaa;
/*border-top: 0;*/
max-width: 100%;
max-height: 10em;
overflow-x: scroll;
overflow-y: scroll;
position: absolute;
background: #fff;
box-sizing: border-box;
}
div.searchable-input div.item
{
white-space: normal;
padding-left: 0.5em;
padding-right: 0.5em;
user-select: none;
}
div.searchable-input div.item:hover
{
/*background: #0af;
color: #fff;*/
background: highlight;
color: highlighttext;
cursor: pointer;
}

View File

@ -0,0 +1,206 @@
$(function() {
SearchableInput = {};
SearchableInput.initItems = function()
{
var items = $("div.searchable-input div.item");
items.each(function(i, x) {
x = $(x);
var parent = x.parents("div.searchable-input").first();
var input = parent.find("input").first();
var item = x;
var itemContainer = parent.find("div.items").first();
if (item.is("[onclick]"))
return;
if (!item.is("[data-value]"))
return;
item.unbind("mousedown");
item.bind("mousedown", function() {
input.attr("data-value", item.attr("data-value"));
input.val(item.text().trim());
itemContainer.hide();
});
});
};
SearchableInput.init = function()
{
$("div.searchable-input input").each(function(i, x) {
x = $(x);
var parent = x.parents("div.searchable-input").first();
var backgroundColor = null;
var selectColor = null;
var input = x;
var itemContainer = parent.find("div.items").first();
var items = itemContainer.find("div.item");
var strictSearch = parent.attr("strict-search");
// Get first opaque body color:
parent.parents("*").each(function(i, x) {
if (backgroundColor !== null)
return;
x = $(x);
var color = x.css("backgroundColor");
if (/^rgba/.test(color))
{
var alphaPart = parseFloat(color.split(",")[3]);
if (isNaN(alphaPart))
return;
if (alphaPart > 0.9)
backgroundColor = color;
return;
}
if (/^rgb/.test(color))
backgroundColor = color;
});
itemContainer.hide();
x.unbind("blur");
x.bind("blur", function() {
setTimeout(function() {
itemContainer.hide();
if (input.val().trim().length < 1)
input.attr("data-value", null);
}, 10);
});
x.unbind("input");
x.bind("input", function() {
var fnOnInputCooldown = function(itemContainer)
{
var parent = itemContainer.parents("div.searchable-input").first();
var input = parent.find("input").first();
var items = itemContainer.find("div.item");
var searchTermsString = input.val().toLowerCase().trim();
var searchTerms = searchTermsString.split(" ");
items.hide();
items.each(function(i, x) {
x = $(x);
var item = x;
var pass = 1;
var itemText = item.text().toLowerCase().trim();
if (strictSearch)
{
if (itemText.includes(searchTermsString))
item.show();
return;
}
for (var i = 0; i < searchTerms.length; i++)
{
var searchTerm = searchTerms[i];
if (searchTerm.length < 1)
continue;
if (!itemText.includes(searchTerm))
pass = 0;
}
//item.hide();
if (pass == 1)
item.show();
});
};
var dataSourceApi = itemContainer.attr("data-source-api");
if (dataSourceApi)
{
if (typeof(SearchableInput.timeout) !== "undefined")
clearTimeout(SearchableInput.timeout);
SearchableInput.timeout = setTimeout(function() {
var searchTermsString = input.val().toLowerCase().trim();
console.log(dataSourceApi);
$.get({
url: `${dataSourceApi}?q=${searchTermsString}`,
//url: `${dataSourceApi}`,
method: "get",
success: function(data)
{
console.log(data);
itemContainer.html("");
for (var i = 0; i < data.length; i++)
{
var item = data[i];
var itemElement = $("<div></div>");
itemElement.addClass("item");
itemElement.attr("data-value", item.value);
itemElement.html(item.name);
itemContainer.append(itemElement);
}
SearchableInput.initItems();
fnOnInputCooldown(itemContainer);
},
error: function(xhr, e, m)
{
console.log(e);
console.log(m);
}
});
}, 1000);
}
fnOnInputCooldown(itemContainer);
});
x.unbind("focus");
x.bind("focus", function() {
//x.trigger("input");
itemContainer.css("width", parent.width() + "px");
itemContainer.css("background", backgroundColor);
// If data-source is a valid jQuery selector, use that container's innerHTML:
var dataSource = itemContainer.attr("data-source");
if (dataSource)
{
var dataSourceObject = $(dataSource);
if (dataSourceObject.length > 0)
{
itemContainer.html(dataSourceObject.first().html());
//SearchableInput.init();
SearchableInput.initItems();
}
}
itemContainer.show();
items.show();
input.select();
});
SearchableInput.initItems();
});
};
SearchableInput.init();
});

61
footer.php Normal file
View File

@ -0,0 +1,61 @@
<?php
global $c;
$varUser = UserAuth::getUser();
$varFooterLinks2 = $c->query("SELECT * from links where position like 'footer' order by sort");
$varFooterLinks = [];
foreach ($varFooterLinks2 as $varLink)
{
if (UserAuth::visible($varLink["visibility"]))
$varFooterLinks[] = $varLink;
}
?>
<div class="mb-5">
<hr />
<?php
$strSidebarContent = Settings::get("footer_content", "Copyright ©", true);
?>
<?php if (strlen($strSidebarContent) > 0): ?>
<div class="container">
<div class="row">
<div class="col-lg-8">
<div>
<?php
$varParsedown = new Parsedown();
echo $varParsedown->text($strSidebarContent);
?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php if (count($varFooterLinks) > 0): ?>
<div class="container">
<div class="row">
<div class="col-lg-4">
<?php foreach ($varFooterLinks as $varLink): ?>
<div>
<a class="link-underline link-underline-opacity-0" href="<?= $varLink["url"]; ?>"><i class="fa fa-fw fa-<?= $varLink["icon"]; ?> pe-2"></i> <?= $varLink["label"]; ?></a>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<script>
$(function() {
$("input, textarea").each(function(i, x) {
x = $(x);
x.attr("autocomplete", "0");
x.attr("autocorrect", "0");
x.attr("autocapitalize", "0");
x.attr("spellcheck", "false");
});
});
</script>

26
head.php Normal file
View File

@ -0,0 +1,26 @@
<meta name="viewport" content="initial-scale=1, width=device-width" />
<!-- Bootstrap -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" ></script>
<!-- JQuery -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- FontAwesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/js/all.min.js"></script>
<!-- SearchableInput -->
<link rel="stylesheet" href="/files/SearchableInput/SearchableInput.css" />
<script src="/files/SearchableInput/SearchableInput.js"></script>
<!-- FormValidator -->
<script src="/files/FormValidator/FormValidator.js"></script>
<style>
/* https://github.com/twbs/bootstrap/issues/37184 */
.dropdown-menu {
z-index: 1040 !important;
}
</style>

92
header.php Normal file
View File

@ -0,0 +1,92 @@
<?php
global $c;
$varUser = UserAuth::getUser();
$varNavbarLinks = $c->query("SELECT * from links where position like 'navbar' order by sort");
$varSidebarLinks = $c->query("SELECT * from links where position like 'sidebar' order by sort");
$varFirstNavbarLink = array_shift($varNavbarLinks);
?>
<script>
// Make the page's theme dark:
$("body").first().attr("data-bs-theme", "dark");
</script>
<div class="offcanvas offcanvas-start" id="sidebar">
<div class="offcanvas-body">
<?php
$strSidebarContent = Settings::get("sidebar_content", "#### Sidebar Navigation", true);
?>
<?php if (strlen($strSidebarContent) > 0): ?>
<div class="mt-5">
<?php
$varParsedown = new Parsedown();
echo $varParsedown->text($strSidebarContent);
?>
</div>
<?php endif; ?>
<?php foreach ($varSidebarLinks as $varLink): ?>
<?php if (!UserAuth::visible($varLink["visibility"])) continue; ?>
<div class="mb-2">
<a class="btn btn-outline-secondary w-100" href="<?= $varLink["url"]; ?>"><i class="fa fa-fw fa-<?= $varLink["icon"]; ?> pe-2"></i> <?= $varLink["label"]; ?></a>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="navbar navbar-expand bg-secondary d-flex px-3">
<div class="container justify-content-between">
<div class="navbar-nav d-inline-flex align-items-center">
<div class="navbar-nav d-inline-flex">
<a class="btn btn-secondary me-2" data-bs-toggle="offcanvas" data-bs-target="#sidebar">&nbsp; <i class="fa fa-fw fa-bars"></i> &nbsp;</a>
</div>
<a class="navbar-brand" href="<?= $varFirstNavbarLink["url"]; ?>"><?= $varFirstNavbarLink["label"]; ?></a>
<div class="dropdown d-lg-none">
<a class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown">...</a>
<div class="dropdown-menu">
<?php foreach ($varNavbarLinks as $varLink): ?>
<?php if (!UserAuth::visible($varLink["visibility"])) continue; ?>
<a class="dropdown-item" href="<?= $varLink["url"]; ?>"><i class="fa fa-fw fa-<?= $varLink["icon"]; ?> pe-2"></i> <?= $varLink["label"]; ?></a>
<?php endforeach; ?>
</div>
</div>
<?php foreach ($varNavbarLinks as $varLink): ?>
<?php if (!UserAuth::visible($varLink["visibility"])) continue; ?>
<a class="nav-link d-none d-lg-inline" href="<?= $varLink["url"]; ?>"><?= $varLink["label"]; ?></a>
<?php endforeach; ?>
</div>
<div class="navbar-nav d-inline-flex align-items-center">
<div class="dropdown">
<a class="btn btn-secondary dropdown-toggle h-100" data-bs-toggle="dropdown"><i class="fa fa-fw fa-user"></i> &nbsp;</a>
<div class="dropdown-menu dropdown-menu-end">
<?php if ($varUser !== null): ?>
<a class="dropdown-item" href="/user/info"><i class="fa fa-fw fa-user pe-2"></i> <?= $varUser["username"]; ?></a>
<a class="dropdown-item" href="/user/signout"><i class="fa fa-fw fa-right-from-bracket pe-2"></i> Sign Out</a>
<?php else: ?>
<a class="dropdown-item" href="/user/signin"><i class="fa fa-fw fa-right-to-bracket pe-2"></i> Sign In</a>
<a class="dropdown-item" href="/user/register"><i class="fa fa-fw fa-user-plus pe-2"></i> Register</a>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>

47
init.php Normal file
View File

@ -0,0 +1,47 @@
<?php
global $c;
$strDBCSFile = "dbcs.txt";
$strDBCS = "sqlite:sqlite.db";
if (!file_exists($strDBCSFile))
file_put_contents($strDBCSFile, $strDBCS);
$strDBCS = trim(file_get_contents($strDBCSFile));
$c = new DatabaseConnection($strDBCS);
$intInitialize = 1;
if ($intInitialize == 1)
{
$c->query([
"create_users_table.sql",
"create_sessions_table.sql",
"create_links_table.sql",
"create_posts_table.sql",
"create_settings_table.sql"]);
$varLinks = $c->query("SELECT * from links");
if (count($varLinks) < 1)
{
$c->query(
"INSERT into links (label, url, icon, position, visibility)
values
('Home', '/', 'home', 'navbar', ''),
('Post', '/post', 'edit', 'navbar', 'user'),
('Home', '/', 'home', 'sidebar', ''),
('Edit Links', '/edit/links', 'link', 'sidebar', 'admin'),
('Edit CSS', '/settings/css', 'code', 'sidebar', 'admin'),
('Edit JS', '/settings/js', 'code', 'sidebar', 'admin'),
('Edit sidebar content', '/settings/sidebar_content', 'comment', 'sidebar', 'admin'),
('Edit footer content', '/settings/footer_content', 'comment', 'sidebar', 'admin'),
('Go home', '/', 'home', 'footer', ''),
('Search', '/search', 'search', 'footer', '')"
);
}
}
?>

140
lib/BootstrapRender.php Normal file
View File

@ -0,0 +1,140 @@
<?php
class BootstrapRender
{
public static function message()
{
if (func_num_args() > 0)
{
Cookie::set("message", func_get_arg(0));
if (func_num_args() > 1)
Cookie::set("messageClass", func_get_arg(1));
return;
}
$strMessage = Cookie::get("message");
$strMessageClass = Cookie::get("messageClass");
if (!isset($strMessageClass) || $strMessageClass == null || strlen($strMessageClass) < 1)
$strMessageClass = "info";
?>
<div id="page-message-container">
<?php if (isset($strMessage) && $strMessage !== null && strlen($strMessage) > 0): ?>
<div class="alert alert-<?= $strMessageClass; ?> d-none" id="page-message">
<?= $strMessage; ?>
</div>
<script>
$(function() {
$("#page-message")
.hide()
.removeClass("d-none")
.fadeIn();
});
</script>
<?php endif; ?>
</div>
<script>
$(function() {
BootstrapRender = {};
BootstrapRender.message = function(message, messageClass = "info") {
var messageElem = $("<div></div>");
messageElem.addClass(`alert alert-${messageClass} d-none`);
messageElem.attr("id", "page-message");
messageElem.html(message);
$("#page-message-container")
.empty()
.append(messageElem);
messageElem
.hide()
.removeClass("d-none")
.fadeIn();
return messageElem;
};
});
</script>
<?php
Cookie::set("message");
Cookie::set("messageClass");
}
public static function input($varOptions)
{
$varOptions["tag"] = $varOptions["tag"] ?? "input";
$varOptionsExtras = $varOptions;
$varDefaultKeys = ["tag", "label", "name", "type", "value", "hint"];
foreach ($varDefaultKeys as $k)
if (array_key_exists($k, $varOptionsExtras))
unset($varOptionsExtras[$k]);
?>
<div class="mb-3">
<label class="form-label"><?= $varOptions["label"] ?? $varOptions["name"] ?? "input"; ?></label>
<<?= $varOptions["tag"]; ?>
type="<?= $varOptions["type"] ?? "text"; ?>"
class="form-control"
name="<?= $varOptions["name"] ?? "text"; ?>"
placeholder="Enter <?= $varOptions["label"] ?? "value"; ?>"
<?php if ($varOptions["tag"] !== "textarea"): ?>
value="<?= $varOptions["value"] ?? ""; ?>"
<?php endif; ?>
<?php foreach ($varOptionsExtras as $k => $v): ?>
<?= $k; ?>="<?= $v; ?>"
<?php endforeach; ?>
/><?= $varOptions["tag"] == "textarea"? "{$varOptions["value"]}</textarea>" : ""; ?>
<small class="text-muted"><?= $varOptions["hint"] ?? ""; ?></small>
</div>
<?php
}
public static function button($varOptions)
{
$varOptions["tag"] = $varOptions["tag"] ?? "a";
$varOptionsExtras = $varOptions;
$varDefaultKeys = ["tag", "label", "name", "type", "value", "hint"];
foreach ($varDefaultKeys as $k)
if (array_key_exists($k, $varOptionsExtras))
unset($varOptionsExtras[$k]);
?>
<<?= $varOptions["tag"]; ?>
class="btn btn-<?= $varOptions["class"] ?? "secondary"; ?>"
<?php foreach ($varOptionsExtras as $k => $v): ?>
<?= $k; ?>="<?= $v; ?>"
<?php endforeach; ?>
>
<?php if (array_key_exists("icon", $varOptions)): ?>
<i class="fa fa-fw fa-<?= $varOptions["icon"]; ?>"></i>
<?php endif; ?>
<?= $varOptions["label"] ?? "Button"; ?>
</<?= $varOptions["tag"]; ?>>
<?php
}
public static function buttons($varButtons)
{
?>
<div class="mb-3">
<label class="form-label">Actions</label>
<div>
<?php foreach ($varButtons as $b): ?>
<?php BootstrapRender::button($b); ?>
<?php endforeach; ?>
</div>
</div>
<?php
}
}
?>

1994
lib/Parsedown.php Normal file

File diff suppressed because it is too large Load Diff

68
lib/PostRender.php Normal file
View File

@ -0,0 +1,68 @@
<?php
class PostRender
{
public static function rows($varRows)
{
$varUser = UserAuth::getUser();
$varParsedown = new Parsedown();
$intRenderedRows = 0;
?>
<style>
<?php
echo Settings::get(
"css",
"/* Put in your custom CSS here: */\nblockquote {\n padding: 1em;\n background:\n rgba(127, 127, 127, 0.2);\n border-left: 3px solid rgba(127, 127, 127, 0.2); \n}",
true);
?>
</style>
<?php foreach ($varRows as $r): ?>
<?php if (!UserAuth::visible($r["visibility"])) continue; ?>
<div class="container my-5">
<div class="row">
<div class="col-lg-8">
<div class="xborder xborder-secondary xrounded xp-3">
<?php echo $varParsedown->text($r["content"]); ?>
</div>
<hr />
<div class="text-muted">
<div>by <?= $r["username"]; ?></div>
<div>on <?= $r["created"]; ?> UTC</div>
</div>
<?php if (UserAuth::has("is_admin")): ?>
<div>
<a class="link-underline link-underline-opacity-0" href="/post/<?= $r["id"]; ?>"><i class="fa fa-fw fa-edit"></i> Edit</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php $intRenderedRows++; ?>
<?php endforeach; ?>
<?php if ($intRenderedRows < 1): ?>
<div class="container my-5">
<div class="row">
<div class="col-lg-8">
<p>Sorry, there is nothing here to show.</p>
</div>
</div>
</div>
<?php endif; ?>
<script>
<?php
echo Settings::get(
"js",
"$(function() {\n // My script here.\n});",
true);
?>
</script>
<?php
}
}
?>

62
lib/Settings.php Normal file
View File

@ -0,0 +1,62 @@
<?php
class Settings
{
private static $varValues = null;
public static function get($strSettingName=null, $strDefault="", $intSave=0)
{
global $c;
if (Settings::$varValues == null)
{
$varRows = $c->query("SELECT * from settings order by setting");
$varValues = [];
foreach ($varRows as $r)
$varValues[$r["setting"]] = $r["value"];
Settings::$varValues = $varValues;
}
if ($strSettingName == null)
return Settings::$varValues;
if (array_key_exists($strSettingName, Settings::$varValues))
return Settings::$varValues[$strSettingName];
if ($intSave)
Settings::set($strSettingName, $strDefault);
return $strDefault;
}
public static function set($strSettingName, $strValue)
{
Settings::$varValues = null;
global $c;
$varExisting = $c->query("
SELECT *
from settings
where
setting like ?
order by
id desc",
$strSettingName);
if (count($varExisting) !== 1)
{
$c->query("DELETE from settings where setting like ?", $strSettingName);
$c->query("INSERT into settings (setting, value) values (?, ?)", $strSettingName, $strValue);
}
$c->query(
"UPDATE settings
set
value = ?
where setting like ?",
$strValue,
$strSettingName);
}
}
?>

264
lib/TableEditor.php Normal file
View File

@ -0,0 +1,264 @@
<?php
class TableEditor
{
public static function render($strTableName, $varColumns)
{
global $c;
$varRows = [];
$varKeys = [];
try
{
$varRows = $c->query("SELECT * from {$strTableName} order by sort asc");
}
catch (Exception $x)
{
$varRows = $c->query("SELECT * from {$strTableName}");
}
$strInput = file_get_contents("php://input");
if (strlen($strInput) > 0)
{
$a = json_decode($strInput, true);
$output = [];
foreach ($a as $r)
{
$strColumns = "";
$strQMarks = "";
$strSetLns = "";
$varValues = [];
foreach ($varColumns as $strCol)
{
$strColumns .= "{$strCol}, ";
$strQMarks .= "?, ";
$strSetLns .= "{$strCol} = ?, ";
$varValues[] = $r[$strCol];
}
$strColumns = preg_replace("/, $/", "", $strColumns);
$strQMarks = preg_replace("/, $/", "", $strQMarks);
$strSetLns = preg_replace("/, $/", "", $strSetLns);
if (strlen($r["id"]) < 1)
{
$c->query(
"INSERT into {$strTableName} ({$strColumns}) values ({$strQMarks})",
$varValues);
continue;
}
if (intval($r["delete"]) == 1)
{
$c->query("DELETE from {$strTableName} where id = ?", $r["id"]);
continue;
}
$c->query(
"UPDATE {$strTableName}
set {$strSetLns}
where id = ?",
$varValues,
$r["id"]);
$output[] = $r;
}
Respond::json(["message" => "success", "output" => $output]);
}
?>
<div class="navbar navbar-expand bg-body-tertiary d-flex px-3 sticky-top">
<div class="container justify-content-between">
<div class="navbar-nav d-inline-flex">
<span class="navbar-brand">Options</span>
<a class="btn btn-outline-success" onclick="fnSave();"><i class="fa fa-fw fa-save"></i> Save</a>
</div>
<div class="navbar-nav d-inline-flex">
</div>
</div>
</div>
<style>
/* https://github.com/twbs/bootstrap/issues/37184 */
.dropdown-menu {
z-index: 1040 !important;
}
/*
th:first-child,
th:last-child {
width: 7.5%;
background: #F00 !important;
}*/
.table-responsive {
overflow-x: scroll;
}
.w-input {
width: 15em !important;
}
tr td:first-child input[type="text"]
{
width: 5em !important;
}
</style>
<div class="container">
<div class="row my-5">
<div class="col-lg-12">
<?php if (count($varRows) > 0): ?>
<div class="table-responsive">
<table class="table table-borderless">
<thead>
<tr>
<?php foreach($varRows[0] as $k => $v): ?>
<?php
if ($k == "sort")
continue;
$varKeys[] = $k;
?>
<th><?= $k; ?></th>
<?php endforeach; ?>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($varRows as $r): ?>
<tr>
<?php foreach ($varKeys as $k): ?>
<td>
<div class="input-group">
<input type="text" class="form-control w-input" name="<?= $k; ?>" value="<?= $r[$k]; ?>" />
</div>
</td>
<?php endforeach; ?>
<td class="align-middle text-nowrap">
<input type="hidden" name="delete" value="0" />
<a class="" onclick="fnCloneRow(this);"><i class="fa fa-fw fa-copy"></i></a>
<a class="" onclick="fnDeleteRow(this);"><i class="fa fa-fw fa-trash"></i></a>
<?php if (in_array("sort", $varColumns)): ?>
<a class="" onclick="fnMoveRowUp(this);"><i class="fa fa-fw fa-arrow-up"></i></a>
<a class="" onclick="fnMoveRowDown(this);"><i class="fa fa-fw fa-arrow-down"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
$(function() {
$("[name='id']").each(function(i, x) {
x = $(x);
x.attr("readonly", 1);
});
fnSerialize = function() {
var a = [];
var sort = 0;
$("table tbody tr").each(function(i, x) {
x = $(x);
var inputs = x.find("input");
var o = {};
inputs.each(function(i2, x2) {
x2 = $(x2);
var key = x2.attr("name");
var value = x2.val();
o[key] = value;
});
o["sort"] = sort;
a.push(o);
sort++;
});
console.log(a);
return a;
};
fnSave = function()
{
var data = fnSerialize();
$.ajax({
url: "",
method: "post",
data: JSON.stringify(data),
success: function(r)
{
console.log(r);
window.location.href = window.location.href;
}
});
};
fnCloneRow = function(x)
{
x = $(x);
var row = x.parents("tr").first();
var rowCopy = row.clone();
rowCopy.insertAfter(row);
rowCopy.find("input").each(function(i, x2) {
x2 = $(x2);
x2.val("");
});
};
fnDeleteRow = function(x)
{
x = $(x);
var row = x.parents("tr").first();
row.hide();
row.find("[name='delete']").first().val("1");
};
fnMoveRowUp = function(x)
{
x = $(x);
var row = x.parents("tr").first();
row.insertBefore(row.prev());
}
fnMoveRowDown = function(x)
{
x = $(x);
var row = x.parents("tr").first();
row.insertAfter(row.next());
}
});
</script>
<?php
}
}
?>

107
lib/UserAuth.php Normal file
View File

@ -0,0 +1,107 @@
<?php
class UserAuth
{
public static function getUser()
{
global $c;
try
{
$strToken = Cookie::get("token");
if ($strToken !== null)
if (strlen($strToken) > 0)
{
$varSessions = $c->query(
"SELECT *
from sessions as s
join users as u on u.username = s.username
where
s.token = ?
and (
s.expires is null
or s.expires > current_timestamp
)",
$strToken);
if (count($varSessions) == 1)
return $varSessions[0];
}
}
catch (Exception $x) {}
return null;
}
public static function has($strColumnName)
{
global $c;
$varUser = UserAuth::getUser();
if ($varUser == null)
return false;
if (array_key_exists($strColumnName, $varUser))
if (intval($varUser[$strColumnName]) > 0)
return true;
return false;
}
public static function require($strColumnName)
{
if (!UserAuth::has($strColumnName))
{
BootstrapRender::message("You do not have permission to do that, please sign into an account that does.", "warning");
Respond::redirect("/user/signin");
}
}
public static function visible($strVisibility)
{
global $c;
if (UserAuth::has("is_admin"))
return true;
$varUser = UserAuth::getUser();
$strUsername = $varUser["username"] ?? null;
$varRegex = [
["/user/i", ($varUser == null)],
["/admin/i", (!UserAuth::has("is_admin"))],
];
// Support arrays with username and visibility keys:
if (is_array($strVisibility))
{
if (array_key_exists("username", $strVisibility))
if ($strVisibility["username"] == $strUsername)
return true;
if (!array_key_exists("visibility", $strVisibility))
return false;
$strVisibility = $strVisibility["visibility"];
}
// Handle hiding the post from non-admins:
if (preg_match("/^(admin|hid(e|den)|invisible|no(ne|body)|private)$/i", $strVisibility))
return false;
if (preg_match("/{$strUsername}/i", $strVisibility)) return true;
// Handle showing the post to everyone:
if (preg_match("/^(|(every|any)(body|one))|all|public)$/i", $strVisibility))
return true;
$intExit = 0;
foreach ($varRegex as $re)
if (preg_match($re[0], $strVisibility))
if ($re[1])
$intExit = 1;
if ($intExit == 1)
return false;
return true;
}
}
?>

46
pages/directory.php Normal file
View File

@ -0,0 +1,46 @@
<?php
global $c;
$varPostLocations = $c->query("SELECT distinct location from posts order by location");
$varLinks = $c->query("SELECT * from links order by sort");
?>
<div class="navbar navbar-expand bg-body-tertiary d-flex px-3 sticky-top">
<div class="container justify-content-between">
<div class="navbar-nav d-inline-flex align-items-center">
<span class="navbar-brand">Directory</span>
</div>
<div class="navbar-nav d-inline-flex">
</div>
</div>
</div>
<div class="container my-5">
<div class="row mb-4">
<div class="col-lg-6">
<h5 class="mb-3"><i class="fa fa-fw fa-comment pe-2"></i> Posts</h5>
<?php foreach ($varPostLocations as $i): ?>
<?php
$intPostCount = $c->query("SELECT count(*) as c from posts where location = ?", $i["location"])[0]["c"];
?>
<div class="border p-2 mb-2">
<a class="link-underline link-underline-opacity-0" href="<?= $i["location"]; ?>"><i class="fa fa-fw fa-file pe-2"></i> <?= $i["location"]; ?></a>
<small class="text-muted">&mdash; <?= $intPostCount !== 1? "{$intPostCount} posts": "{$intPostCount} post"; ?></small>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="row mb-4">
<div class="col-lg-6">
<h5 class="mb-3"><i class="fa fa-fw fa-link pe-2"></i> Links</h5>
<?php foreach ($varLinks as $i): ?>
<?php if (!UserAuth::visible($i["visibility"])) continue; ?>
<div class="border p-2 mb-2">
<a class="link-underline link-underline-opacity-0" href="<?= $i["url"]; ?>"><i class="fa fa-fw fa-<?= $i["icon"]; ?> pe-2"></i> <?= $i["label"]; ?></a>
<small class="text-muted">&mdash; <?= $i["position"]; ?></small>
</div>
<?php endforeach; ?>
</div>
</div>
</div>

5
pages/edit/links.php Normal file
View File

@ -0,0 +1,5 @@
<?php
global $c;
UserAuth::require("is_admin");
TableEditor::render("links", ["label", "url", "icon", "position", "visibility", "sort"]);
?>

71
pages/edit/setting.php Normal file
View File

@ -0,0 +1,71 @@
<?php
global $c;
UserAuth::require("is_admin");
$varUser = UserAuth::getUser();
$strId = Request::getArg(0);
if ($strId == null || strlen($strId) < 1)
$strId = "none";
if (Request::posts("content"))
Settings::set($strId, Request::getPosted("content"));
$strContent = Settings::get($strId, "");
?>
<style>
textarea {
font-family: monospace;
}
</style>
<form method="post">
<div class="navbar navbar-expand bg-body-tertiary d-flex px-3 sticky-top">
<div class="container justify-content-between">
<div class="navbar-nav d-inline-flex align-items-center">
<span class="navbar-brand">Setting</span>
<span class="nav-item text-nowrap me-2">Name</span>
<input class="form-control me-2" type="text" name="location" value="<?= $strId; ?>" readonly disabled />
<a class="btn btn-outline-success text-nowrap" onclick="fnSave();"><i class="fa fa-fw fa-save"></i> Save</a>
</div>
<div class="navbar-nav d-inline-flex">
<?php BootstrapRender::message(); ?>
</div>
</div>
</div>
<?php /**/ ?>
<div class="container my-5">
<div class="row">
<textarea
class="form-control border-0 shadow-none"
name="content"
placeholder="Enter content here..."
oninput="fnResize(this);"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"><?= $strContent; ?></textarea>
</div>
</div>
</form>
<script>
$(function() {
fnSave = function() {
$("form").first().submit();
};
fnResize = function(x) {
x.style.height = "auto";
x.style.height = x.scrollHeight + "px";
};
fnResize($("textarea").first()[0]);
});
</script>

163
pages/edit/settings.php Normal file
View File

@ -0,0 +1,163 @@
<?php
global $c;
UserAuth::require("is_admin");
$varUser = UserAuth::getUser();
$varRows = Settings::get();
$strInput = file_get_contents("php://input");
if (strlen($strInput) > 0)
{
$a = json_decode($strInput, true);
$output = [];
foreach ($a as $r)
{
$strSetting = $r["setting"];
$strValue = $r["value"];
Settings::set($strSetting, $strValue);
}
Respond::json(["message" => "success", "output" => $output]);
}
?>
<div class="navbar navbar-expand bg-body-tertiary d-flex px-3 sticky-top">
<div class="container justify-content-between">
<div class="navbar-nav d-inline-flex">
<span class="navbar-brand">Options</span>
<a class="btn btn-outline-success" onclick="fnSave();"><i class="fa fa-fw fa-save"></i> Save</a>
</div>
<div class="navbar-nav d-inline-flex">
</div>
</div>
</div>
<style>
/* https://github.com/twbs/bootstrap/issues/37184 */
.dropdown-menu {
z-index: 1040 !important;
}
/*
th:first-child,
th:last-child {
width: 7.5%;
background: #F00 !important;
}*/
.w-1 { width: 1%; }
.table-responsive {
overflow-x: scroll;
}
.w-input {
width: 15em !important;
}
tr td:first-child input[type="text"]
{
width: 5em !important;
}
</style>
<div class="container">
<div class="row my-5">
<div class="col-lg-12">
<?php if (count($varRows) > 0): ?>
<div class="table-responsive">
<table class="table table-borderless">
<thead>
<tr>
<th>Setting</th>
<th>Value<th>
</tr>
</thead>
<tbody>
<?php foreach ($varRows as $k => $v): ?>
<tr>
<td>
<div class="input-group">
<input type="text" class="form-control w-input" name="setting" value="<?= $k; ?>" />
</div>
</td>
<td>
<?php
$strClass = "";
if (preg_match("/\n/", $v))
$strClass = "disabled readonly";
?>
<div class="input-group">
<input type="text" class="form-control w-input <?= $strClass; ?>" name="value" value="<?= $v; ?>" <?= $strClass; ?> />
</div>
<div>
<small>
<a class="link-underline link-underline-opacity-0" href="/edit/setting/<?= $k; ?>">Edit in multi-line editor</a>
</small>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
$(function() {
$("[name='setting']").each(function(i, x) {
x = $(x);
x.attr("readonly", 1);
});
fnSerialize = function() {
var a = [];
$("table tbody tr").each(function(i, x) {
x = $(x);
var valueInput = x.find("[name='value']").first();
var o = {};
o["setting"] = x.find("[name='setting']").first().val().trim();
o["value"] = valueInput.val();
if (valueInput.attr("disabled"))
return;
a.push(o);
});
console.log(a);
return a;
};
fnSave = function()
{
var data = fnSerialize();
$.ajax({
url: "",
method: "post",
data: JSON.stringify(data),
success: function(r)
{
console.log(r);
window.location.href = window.location.href;
}
});
};
});
</script>

18
pages/index.php Normal file
View File

@ -0,0 +1,18 @@
<?php
global $c;
$strPath = "/";
$strPath .= implode("/", Request::getPathParts());
$varPosts = $c->query(
"SELECT *
from posts as p
where
location like ?
or location like '*'
order by
created desc",
$strPath);
?>
<?php PostRender::rows($varPosts); ?>

182
pages/post.php Normal file
View File

@ -0,0 +1,182 @@
<?php
global $c;
UserAuth::require("can_post");
$varUser = UserAuth::getUser();
$strId = Request::getArg(0);
$strContent = "";
$strLocation = Request::getParam("to") ?? "";
$strVisibility = "";
$strVerb = "Create";
if (strlen($strId) > 0)
{
$strVerb = "Edit";
$varRows = $c->query("SELECT * from posts where id = ?", $strId);
if (count($varRows) !== 1)
{
BootstrapRender::message("Zero or more than one row returned", "danger");
Respond::redirect("/post");
}
$varRow = $varRows[0];
$strContent = $varRow["content"];
$strLocation = $varRow["location"];
$strVisibility = $varRow["visibility"];
}
if (Request::posts("location", "content", "visibility"))
{
$strLocation = Request::getPosted("location");
$strContent = Request::getPosted("content");
$strVisibility = Request::getPosted("visibility");
if ($strId == null || strlen($strId) < 1)
{
$c->query(
"INSERT into posts (username, content, location, visibility)
values (?, ?, ?, ?)",
$varUser["username"],
$strContent,
$strLocation,
$strVisibility);
$strId = $c->query("get_last_post.sql")[0]["id"];
}
if (strlen($strContent) < 1)
{
$c->query("DELETE from posts where id = ?", $strId);
BootstrapRender::message("Post deleted successfully.", "success");
Respond::redirect("/post");
}
$c->query(
"UPDATE posts
set
content = ?,
location = ?,
visibility = ?,
updated = current_timestamp
where
id = ?",
$strContent,
$strLocation,
$strVisibility,
$strId);
BootstrapRender::message("Post saved.", "success");
Respond::redirect("/post/{$strId}");
}
?>
<style>
textarea {
font-family: monospace;
}
</style>
<form method="post">
<div class="navbar navbar-expand bg-body-tertiary d-flex px-3 sticky-top">
<div class="container justify-content-between">
<div class="navbar-nav d-inline-flex align-items-center">
<span class="navbar-brand"><?= $strVerb; ?> Post</span>
</div>
<div class="navbar-nav d-inline-flex">
</div>
</div>
</div>
<div class="container my-5">
<div class="row">
<div class="col-lg-3">
<?php BootstrapRender::message(); ?>
</div>
<div class="col-lg-12">
<div class="mb-3">
<label class="form-label">Content</label>
<textarea
class="form-control"
name="content"
placeholder="Enter markdown content here..."
oninput="fnResize(this);"
><?= $strContent; ?></textarea>
<small class="text-muted">Test</small>
</div>
</div>
<div class="col-lg-3">
<div class="mb-3">
<label class="form-label">Location</label>
<input
type="text"
class="form-control"
name="location"
placeholder="/"
value="<?= $strLocation; ?>"
fv-regex="^\/"
fv-warning="Location must start with a forward slash" />
<small class="text-muted">e.g. /home or /info</small>
</div>
</div>
<div class="col-lg-3">
<div class="mb-3">
<label class="form-label">Visible To</label>
<input
type="text"
class="form-control"
name="visibility"
placeholder="everyone"
value="<?= $strVisibility; ?>"
fv-regex="^(|everyone|users|admins)$"
fv-warning="Visibility must be empty, everyone, users, or admins" />
<small class="text-muted">e.g. everyone, users, admins</small>
</div>
</div>
<div class="col-lg-3">
<div class="mb-3">
<label class="form-label">Actions</label>
<div>
<?php if ($strId == null || strlen($strId) < 1): ?>
<a class="btn btn-primary" onclick="fnSave();"><i class="fa fa-fw fa-paper-plane"></i> Submit</a>
<?php else: ?>
<a class="btn btn-success" onclick="fnSave();"><i class="fa fa-fw fa-save"></i> Save</a>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</form>
<script>
$(function() {
fnSave = function() {
FormValidator.onError = function(str)
{
BootstrapRender.message(str, "danger");
};
FormValidator.validate(function() {
$("form").first().submit();
});
};
fnResize = function(x) {
x.style.minHeight = "2in";
x.style.height = "auto";
x.style.height = x.scrollHeight + "px";
};
fnResize($("textarea").first()[0]);
});
</script>

54
pages/search.php Normal file
View File

@ -0,0 +1,54 @@
<?php
global $c;
$strPath = "/";
$strPath .= implode("/", Request::getPathParts());
$varPosts = [];
$strQuery = Request::getParam("q");
if ($strQuery !== null && strlen($strQuery) > 0)
{
$varPosts = $c->query(
"SELECT *
from posts as p
where
content like concat('%', ?, '%')
order by
created desc",
$strQuery);
$i = 0;
for ($i = 0; $i < count($varPosts); $i++)
{
$varOld = $varPosts[$i];
$varOld["content"] = preg_replace("/({$strQuery})/i", "<mark>$1</mark>", $varOld["content"]);
$varPosts[$i] = $varOld;
}
}
?>
<form method="get">
<div class="navbar navbar-expand bg-body-tertiary d-flex px-3 sticky-top">
<div class="container justify-content-between">
<div class="navbar-nav d-inline-flex align-items-center">
<span class="navbar-brand">Search</span>
<input class="form-control me-2" type="text" name="q" value="<?= $strQuery; ?>" />
<button type="submit" class="btn btn-outline-info text-nowrap"><i class="fa fa-fw fa-search"></i> Go</a>
</div>
<div class="navbar-nav d-inline-flex"></div>
</div>
</div>
</form>
<div class="container">
<div class="row">
<div class="col-lg-4">
<?php BootstrapRender::message(); ?>
</div>
</div>
</div>
<?php PostRender::rows($varPosts); ?>

80
pages/user/info.php Normal file
View File

@ -0,0 +1,80 @@
<?php
global $c;
$strError = null;
if (UserAuth::getUser() == null)
Respond::redirect("/user/signin");
$varUser = UserAuth::getUser();
if ($varUser == null)
Respond::redirect("/");
try
{
if (Request::posts("user_name", "display_name"))
{
$strUsername = Request::getPosted("user_name");
$strDisplayName = Request::getPosted("display_name");
if (!preg_match("/^[A-Za-z0-9]{1,}$/", $strUsername))
throw new Exception("Username must be alphanumeric characters only");
$c->query(
"INSERT or replace into users (email, user_name, display_name)
select
?,
?,
?",
$varUser["email"],
$strUsername,
$strDisplayName);
BootstrapRender::message("Profile updated", "success");
}
}
catch (Exception $x)
{
BootstrapRender::message($x->getMessage(), "danger");
}
?>
<div class="container">
<div class="row my-5">
<div class="col-md-4">
<?php BootstrapRender::message(); ?>
<form method="post">
<?php BootstrapRender::input([
"name" => "username",
"label" => "Username",
"value" => $varUser["username"],
"disabled" => 1,
]); ?>
<?php BootstrapRender::input([
"name" => "user_name",
"label" => "Username",
"value" => $strUsername,
]); ?>
<?php BootstrapRender::input([
"name" => "display_name",
"label" => "Display Name",
"value" => $strDisplayName,
]); ?>
<?php BootstrapRender::button([
"tag" => "button",
"type" => "submit",
"class" => "outline-success",
"icon" => "save",
"label" => "Save"
]); ?>
</form>
</div>
</div>
</div>

5
pages/user/list.php Normal file
View File

@ -0,0 +1,5 @@
<?php
global $c;
UserAuth::require("is_admin");
TableEditor::render("credentials", ["email", "hash"]);
?>

View File

@ -0,0 +1,5 @@
<?php
global $c;
UserAuth::require("is_admin");
TableEditor::render("permissions", ["email", "permission"]);
?>

108
pages/user/register.php Normal file
View File

@ -0,0 +1,108 @@
<?php
global $c;
try
{
$intUserCount = $c->query("SELECT count(*) as c from users")[0]["c"];
if ($intUserCount < 1)
{
BootstrapRender::message(
"Please create an administrator account.",
"warning");
}
if (Request::posts("username", "password", "repeat"))
{
$strUsername = Request::getPosted("username");
$strPassword = Request::getPosted("password");
$strRepeat = Request::getPosted("repeat");
if (!preg_match("/^[A-Za-z0-9]{1,}$/", $strUsername))
throw new Exception("Not a valid username");
if (Request::getPosted("password") !== Request::getPosted("repeat"))
throw new Exception("Passwords do not match");
if (strlen($strPassword) < 6)
throw new Exception("Password must be at least 6 characters");
$varUsers = $c->query("SELECT * from users where username like ?", $strUsername);
if (count($varUsers) > 0)
throw new Exception("Username in use");
$strHash = sha1($strPassword);
$c->query(
"INSERT into users (username, hash) values (?, ?)",
$strUsername,
$strHash);
$intUserCount = $c->query("SELECT count(*) as c from users")[0]["c"];
if ($intUserCount == 1)
$c->query("UPDATE users set can_post = 1, is_admin = 1");
BootstrapRender::message("Registration was a success, please sign in to continue.");
Respond::redirect("/user/signin");
}
}
catch (Exception $x)
{
BootstrapRender::message($x->getMessage(), "danger");
}
?>
<script>
$(".app-header").hide();
</script>
<div class="container">
<div class="row my-5">
<div class="col-md-4 offset-md-4">
<?php BootstrapRender::message(); ?>
<form method="post">
<?php BootstrapRender::input([
"name" => "username",
"label" => "Username",
"value" => Request::getPosted("username")
]); ?>
<?php BootstrapRender::input([
"name" => "password",
"label" => "Password",
"value" => Request::getPosted("password"),
"type" => "password",
]); ?>
<?php BootstrapRender::input([
"name" => "repeat",
"label" => "Repeat Password",
"value" => Request::getPosted("repeat"),
"type" => "password",
]); ?>
<?php BootstrapRender::buttons([
[
"tag" => "button",
"icon" => "right-to-bracket",
"label" => "Continue",
"type" => "submit",
"class" => "outline-primary"
]
]); ?>
<div class="mb-3">
<a class="text-decoration-none" href="/user/signin">Already have an account?</a>
</div>
</form>
</div>
</div>
</div>

96
pages/user/signin.php Normal file
View File

@ -0,0 +1,96 @@
<?php
global $c;
try
{
$intUserCount = $c->query("SELECT count(*) as c from users")[0]["c"];
if ($intUserCount < 1)
Respond::redirect("/user/register");
if (Request::posts("username", "password"))
{
$strUsername = Request::getPosted("username");
$strPassword = Request::getPosted("password");
$strHash = sha1($strPassword);
$varUsers = $c->query(
"SELECT *
from users
where
username like ?
and hash = ?",
$strUsername,
$strHash);
if (count($varUsers) !== 1)
throw new Exception("Zero or more than one user returned for credentials provided");
$strToken = sha1(microtime());
$c->query(
"INSERT into sessions (username, token) values (?, ?)",
$strUsername,
$strToken);
Cookie::set("token", $strToken);
BootstrapRender::message(
"Successfully signed in",
"info");
Respond::redirect("/user/info");
}
}
catch (Exception $x)
{
BootstrapRender::message($x->getMessage(), "danger");
}
?>
<script>
$(".app-header").hide();
</script>
<div class="container">
<div class="row my-5">
<div class="col-md-4 offset-md-4">
<?php BootstrapRender::message(); ?>
<form method="post">
<?php BootstrapRender::input([
"name" => "username",
"label" => "Username",
"value" => Request::getPosted("email")
]); ?>
<?php BootstrapRender::input([
"name" => "password",
"label" => "Password",
"value" => Request::getPosted("password"),
"type" => "password",
]); ?>
<?php BootstrapRender::buttons([
[
"tag" => "button",
"icon" => "right-to-bracket",
"label" => "Continue",
"type" => "submit",
"class" => "outline-primary"
],
[
"icon" => "home",
"label" => "Home",
"href" => "/",
"class" => "outline-secondary"
]
]); ?>
<div class="mb-3">
<a class="text-decoration-none" href="/user/register">Don't have an account?</a>
</div>
</form>
</div>
</div>
</div>

29
pages/user/signout.php Normal file
View File

@ -0,0 +1,29 @@
<?php
global $c;
$varUser = UserAuth::getUser();
if ($varUser !== null)
{
if (Request::getArg(0) == "all")
{
$c->query(
"UPDATE sessions
set
expires = current_timestamp
where username = ?",
$varUser["username"]);
}
else
{
$c->query(
"UPDATE sessions
set
expires = current_timestamp
where token = ?",
$varUser["token"]);
}
}
BootstrapRender::message("You have successfully signed out");
Respond::redirect("/user/signin");
?>