Forking from BootstrapSQLiteBlog
This commit is contained in:
parent
bf40752d9e
commit
eafc2588dc
@ -1,2 +1,3 @@
|
|||||||
# PlainSQLiteBlog
|
# BootstrapSQLiteBlog
|
||||||
|
|
||||||
|
A blogging plugin for the php-webapp-framwork. Clone to the "plugins/" directory to use.
|
8
db/mysql/create_links_table.sql
Normal file
8
db/mysql/create_links_table.sql
Normal 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)
|
9
db/mysql/create_posts_table.sql
Normal file
9
db/mysql/create_posts_table.sql
Normal 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)
|
5
db/mysql/create_sessions_table.sql
Normal file
5
db/mysql/create_sessions_table.sql
Normal 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)
|
4
db/mysql/create_settings_table.sql
Normal file
4
db/mysql/create_settings_table.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
CREATE table if not exists settings (
|
||||||
|
id integer primary key auto_increment,
|
||||||
|
setting text not null,
|
||||||
|
value text null)
|
7
db/mysql/create_users_table.sql
Normal file
7
db/mysql/create_users_table.sql
Normal 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)
|
4
db/mysql/get_last_post.sql
Normal file
4
db/mysql/get_last_post.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
SELECT *
|
||||||
|
from posts
|
||||||
|
where
|
||||||
|
id = last_insert_id()
|
8
db/sqlite/create_links_table.sql
Normal file
8
db/sqlite/create_links_table.sql
Normal 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)
|
9
db/sqlite/create_posts_table.sql
Normal file
9
db/sqlite/create_posts_table.sql
Normal 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)
|
5
db/sqlite/create_sessions_table.sql
Normal file
5
db/sqlite/create_sessions_table.sql
Normal 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)
|
4
db/sqlite/create_settings_table.sql
Normal file
4
db/sqlite/create_settings_table.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
CREATE table if not exists settings (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
setting text not null,
|
||||||
|
value text null)
|
7
db/sqlite/create_users_table.sql
Normal file
7
db/sqlite/create_users_table.sql
Normal 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)
|
4
db/sqlite/get_last_post.sql
Normal file
4
db/sqlite/get_last_post.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
SELECT *
|
||||||
|
from posts
|
||||||
|
where
|
||||||
|
rowid = last_insert_rowid()
|
46
files/FormValidator/FormValidator.js
Normal file
46
files/FormValidator/FormValidator.js
Normal 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");
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
});
|
37
files/SearchableInput/SearchableInput.css
Normal file
37
files/SearchableInput/SearchableInput.css
Normal 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;
|
||||||
|
}
|
206
files/SearchableInput/SearchableInput.js
Normal file
206
files/SearchableInput/SearchableInput.js
Normal 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
61
footer.php
Normal 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
26
head.php
Normal 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
92
header.php
Normal 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"> <i class="fa fa-fw fa-bars"></i> </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> </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
47
init.php
Normal 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
140
lib/BootstrapRender.php
Normal 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
1994
lib/Parsedown.php
Normal file
File diff suppressed because it is too large
Load Diff
68
lib/PostRender.php
Normal file
68
lib/PostRender.php
Normal 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
62
lib/Settings.php
Normal 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
264
lib/TableEditor.php
Normal 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
107
lib/UserAuth.php
Normal 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
46
pages/directory.php
Normal 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">— <?= $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">— <?= $i["position"]; ?></small>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
5
pages/edit/links.php
Normal file
5
pages/edit/links.php
Normal 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
71
pages/edit/setting.php
Normal 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
163
pages/edit/settings.php
Normal 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
18
pages/index.php
Normal 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
182
pages/post.php
Normal 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
54
pages/search.php
Normal 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
80
pages/user/info.php
Normal 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
5
pages/user/list.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
global $c;
|
||||||
|
UserAuth::require("is_admin");
|
||||||
|
TableEditor::render("credentials", ["email", "hash"]);
|
||||||
|
?>
|
5
pages/user/permissions.php
Normal file
5
pages/user/permissions.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
global $c;
|
||||||
|
UserAuth::require("is_admin");
|
||||||
|
TableEditor::render("permissions", ["email", "permission"]);
|
||||||
|
?>
|
108
pages/user/register.php
Normal file
108
pages/user/register.php
Normal 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
96
pages/user/signin.php
Normal 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
29
pages/user/signout.php
Normal 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");
|
||||||
|
?>
|
Loading…
Reference in New Issue
Block a user