changed dir structure to handle second version of the book

This commit is contained in:
2022-11-25 11:46:00 +01:00
parent b6fd7876d3
commit 5f1f5e847d
35 changed files with 7 additions and 0 deletions

9
Vanilla/LICENSE Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2022 Micha Espey <tracer@24unix.net>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

3
Vanilla/README.md Normal file
View File

@@ -0,0 +1,3 @@
As I was not allowed to use any framework, respectively no foreign code, most of the time was spent, well creating some kind of framework myself. :-)
The address book itself was then done in a few hours.

88
Vanilla/addressbook.sql Normal file
View File

@@ -0,0 +1,88 @@
-- MariaDB dump 10.19 Distrib 10.5.15-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: tracer_addressbook
-- ------------------------------------------------------
-- Server version 10.5.15-MariaDB-0+deb11u1-log
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `addresses`
--
DROP TABLE IF EXISTS `addresses`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `addresses` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`owner` int(11) NOT NULL,
`first` varchar(80) NOT NULL,
`last` varchar(80) NOT NULL,
`street` varchar(80) NOT NULL,
`zip` varchar(10) NOT NULL,
`city` varchar(80) NOT NULL,
`phone` varchar(30) NOT NULL,
PRIMARY KEY (`id`),
KEY `fk_user` (`owner`),
CONSTRAINT `fk_user` FOREIGN KEY (`owner`) REFERENCES `users` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `addresses`
--
LOCK TABLES `addresses` WRITE;
/*!40000 ALTER TABLE `addresses` DISABLE KEYS */;
INSERT INTO `addresses` VALUES (1,2,'a \"test\"','a','Webfoot Street','1313','Duckburg2','555-12345'),(4,1,'c','b','street4','zip4','city4','phone4'),(6,1,'Huey','Duck','Webfoot Street','1010','Duckburg','555.3456'),(7,1,'Dewey','Duck','Webfoot Street','2020','Duckburg','555-9876'),(8,1,'Louie','Duck','Webfoot Street','3030','Duckburg 3','555-8765'),(11,1,'b','Clapton','sdfg','12456^^^>','https://xd.adobe.com/','23343'),(14,1,'d','aa','','','<script>alert(\'test\')</script>','x'),(16,1,'Adam \"The Badass\"','Black','piouhpouhpouh','132213','piugpiugh','9760978');
/*!40000 ALTER TABLE `addresses` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`password` varchar(256) NOT NULL,
`nick` varchar(20) NOT NULL,
`first` varchar(40) NOT NULL,
`last` varchar(40) NOT NULL,
`is_admin` tinyint(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `nick` (`nick`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `users`
--
LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES (1,'$argon2i$v=19$m=65536,t=4,p=1$OFV0Rnl6OXNWZXZJNTFjTw$fC9K1ykszZ/UaEm21XR9M3+XxnBc+dZ7PRIn5aaGw8I','donald','Donald','Duck',1),(2,'$argon2i$v=19$m=65536,t=4,p=1$NVRKNm0xUmplYkcwTFZXdw$GLp1jjLDBRjKSw6nH8SqqSls6fQPi4Hb7ot0k3naf5s','Daisy','Daisy','Duck',0);
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2022-11-01 18:37:04

7
Vanilla/config.json Normal file
View File

@@ -0,0 +1,7 @@
{
"dbHost": "localhost",
"dbPort": 3306,
"dbDatabase": "tracer_addressbook",
"dbUser": "tracer_addressbook",
"dbPassword": "SKTh_6#YM?%q"
}

View File

@@ -0,0 +1,7 @@
{
"dbHost": "localhost",
"dbPort": 3306,
"dbDatabase": "tracer_addressbook",
"dbUser": "tracer_addressbook",
"dbPassword": "secret",
}

13
Vanilla/public/.htaccess Normal file
View File

@@ -0,0 +1,13 @@
DirectoryIndex index.php
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
RewriteRule .* - [E=BASE:%1]
RewriteCond %{HTTP:Authorization} .+
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
RewriteCond %{ENV:REDIRECT_STATUS} =""
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ %{ENV:BASE}/index.php [L]
</IfModule>

View File

@@ -0,0 +1,206 @@
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
function addAddress(url) {
location.href = url
}
function editAddress(id) {
if (document.getElementById('edit_button_' + id).value === 'Save') {
// save
const url = "/address/update";
fetch(url, {
method: "POST",
body: JSON.stringify({
id: id,
owner: document.getElementById('owner_' + id).value,
first: document.getElementById('first_' + id).value,
last: document.getElementById('last_' + id).value,
street: document.getElementById('street_' + id).value,
zip: document.getElementById('zip_' + id).value,
city: document.getElementById('city_' + id).value,
phone: document.getElementById('phone_' + id).value,
})
})
.then(
response => response.text()
).then(
json => {
let jsonObject = JSON.parse(json)
if (jsonObject.status === 200) {
setInfo('Data successfully saved.')
} else {
setError(jsonObject.message);
}
}
);
document.getElementById('first_' + id).disabled = true
document.getElementById('last_' + id).disabled = true
document.getElementById('street_' + id).disabled = true
document.getElementById('zip_' + id).disabled = true
document.getElementById('city_' + id).disabled = true
document.getElementById('phone_' + id).disabled = true
document.getElementById('edit_button_' + id).value = 'Edit'
} else {
//switch to edit
document.getElementById('first_' + id).disabled = false
document.getElementById('last_' + id).disabled = false
document.getElementById('street_' + id).disabled = false
document.getElementById('zip_' + id).disabled = false
document.getElementById('city_' + id).disabled = false
document.getElementById('phone_' + id).disabled = false
document.getElementById('edit_button_' + id).value = 'Save'
}
}
function deleteAddress(id) {
if (confirm('Are you sure?')) {
const url = "/address/delete";
fetch(url, {
method: "POST",
body: JSON.stringify({
id: id
})
})
.then(
response => response.text()
).then(
json => {
let jsonObject = JSON.parse(json)
if (jsonObject.status === 200) {
setInfo('Data successfully saved.')
} else {
setError(jsonObject.message);
}
}
);
let row = document.getElementById('row_' + id)
row.parentNode.removeChild(row)
}
}
function upCase(text) {
return text[0].toUpperCase() + text.substring(1);
}
function sortBy(column) {
// clear titles
const titles = ['first', 'last', 'street', 'zip', 'city', 'phone']
titles.forEach((title) =>
document.getElementById(title).innerHTML = upCase(title)
)
console.log("col", column)
console.log("curcol", currentColumn)
if (currentColumn === column) {
console.log("in switch")
// switch direction on every call on same column
if (currentSortOrder === 'asc') {
currentSortOrder = 'desc'
} else {
currentSortOrder = 'asc'
}
console.log("col", column)
} else {
currentColumn = column
}
let currentTitleElement = document.getElementById(column)
let currentTitle = currentTitleElement.innerHTML
let newTitle
if (currentSortOrder === 'asc') {
newTitle = currentTitle[0] + currentTitle.substring(1) + '&nbsp;&#11015;'
} else {
newTitle = currentTitle[0] + currentTitle.substring(1) + '&nbsp;&#11014;'
}
currentTitleElement.innerHTML = newTitle
const table = document.getElementById('address_table');
let dirty = true;
// loop until clean
while (dirty) {
// assume we are finished
dirty = false
const rows = table.rows;
for (let i = 1; i < (rows.length - 2); i++) {
let x = rows[i]
let rowXId = x.id
let rowXNumber = rowXId.match(/\d+/)
let valueX = document.getElementById(column + '_' + rowXNumber).value
let y = rows[i + 1]
let rowYId = y.id
let rowYNumber = rowYId.match(/\d+/)
let valueY = document.getElementById(column + '_' + rowYNumber).value
let sortOrder
if (currentSortOrder === 'asc') {
sortOrder = 1
} else {
sortOrder = -1
}
if (valueX.localeCompare(valueY) === sortOrder) {
x.parentNode.insertBefore(y, x);
dirty = true
}
}
}
}
function setInfo(info) {
const infoBox = document.getElementById('info_box')
infoBox.innerHTML = info
infoBox.style.display = 'block'
infoBox.classList.add('panel_float')
setTimeout(() => {
infoBox.style.display = 'none'
}, 2500)
}
function setError(error) {
const errorBox = document.getElementById('error_box')
const errorText = document.getElementById('error_text')
const infoButton = document.getElementById('info_button')
if (errorBox.style.display === 'block') {
errorBox.style.display = 'none'
return
}
if (infoButton != null) {
infoButton.disabled = true
}
errorText.innerHTML = error
errorBox.style.display = 'block'
errorBox.classList.add('panel_float')
}
function closeError() {
const errorBox = document.getElementById('error_box')
const infoButton = document.getElementById('info_button')
if (infoButton) {
infoButton.disabled = false
}
errorBox.style.display = 'none'
}
// global scope
let currentSortOrder = 'desc'
let currentColumn = 'last'
document.addEventListener('DOMContentLoaded', () => {
const table = document.getElementById('address_table') || false
if (table) {
sortBy('last')
}
})

View File

@@ -0,0 +1,127 @@
body {
background: #1f1f1f;
color: #cdcdcd;
}
/* unvisited link */
a:link {
color: #ff8844;
text-decoration: none;
}
/* visited link */
a:visited {
color: #ff8844;
text-decoration: none;
}
/* mouse over link */
a:hover {
color: #ff8844;
text-decoration: none;
}
/* selected link */
a:active {
color: #ff8844;
text-decoration: none;
font-weight: bold;
}
table, th, td {
border: 1px solid;
}
label {
display: block;
padding: 1ex;
}
.panel_float {
position: fixed;
overflow: hidden;
z-index: 2400;
opacity: 0.70;
margin: auto;
top: 110px !important;
-webkit-transition: all 0.5s ease-in-out;
-moz-transition: all 0.5s ease-in-out;
-ms-transition: all 0.5s ease-in-out;
-o-transition: all 0.5s ease-in-out;
transition: all 0.5s ease-in-out;
}
#info_box {
background-color: #3fc52a;
border: solid #cdcdcd;
border-radius: 5px;
color: #1f1f1f;
display: none;
padding: 10px;
font-weight: bold;
position: absolute;
z-index: 10;
width: 50%;
margin-left: 200px;
margin-right: 200px;
}
.info_button {
border-radius: 5px;
border: solid #cdcdcd;
color: #1f1f1f;
padding: 8px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
background-color: #3fc52a;
transition-duration: 0.4s;
cursor: pointer;
}
#error_box {
background-color: #e06844;
border: solid #cdcdcd;
border-radius: 5px;
color: #1f1f1f;
display: none;
padding: 10px;
font-weight: bold;
position: absolute;
z-index: 10;
width: 50%;
margin-left: 200px;
margin-right: 200px;
}
.error_button {
border-radius: 5px;
border: solid #cdcdcd;
color: #1f1f1f;
padding: 8px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
background-color: #e06844;
transition-duration: 0.4s;
cursor: pointer;
}
.close_button {
margin-left: 15px;
color: white;
font-weight: bold;
float: right;
font-size: 22px;
line-height: 20px;
cursor: pointer;
transition: 0.3s;
}
.close_button:hover {
color: black;
}

80
Vanilla/public/index.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
ini_set(option: 'display_errors', value: 1);
ini_set(option: 'display_startup_errors', value: 1);
// no one sane should ignore deprecations
error_reporting(error_level: E_ALL);
// just during dev
opcache_reset();
session_start();
require dirname(path: __DIR__) . '/src/bootstrap.php';
use App\Controller\AddressBookAdminController;
use App\Controller\AddressBookController;
use App\Controller\SecurityController;
use App\Service\Container;
use App\Service\Router;
$container = new Container();
$router = $container->get(className: Router::class);
$security = $container->get(className: SecurityController::class);
$addressBook = $container->get(className: AddressBookController::class);
$addressBookAdmin = $container->get(className: AddressBookAdminController::class);
$router->addRoute(name: 'app_login', route: '/login', callback: function () use ($security) {
$security->login();
});
$router->addRoute(name: 'app_logout', route: '/logout', callback: function () use ($security) {
$security->logout();
});
$router->addRoute(name: 'app_main', route: '/', callback: function () use ($addressBook) {
$addressBook->main();
});
$router->addRoute(name: 'app_admin', route: '/admin', callback: function () use ($addressBookAdmin) {
$addressBookAdmin->admin();
});
$router->addRoute(name: 'app_admin_users', route: '/admin/users', callback: function () use ($addressBookAdmin) {
$addressBookAdmin->adminUser();
});
$router->addRoute(name: 'app_admin_users_edit', route: '/admin/users/{nick}', callback: function (array $parameters) use ($addressBookAdmin) {
$addressBookAdmin->adminUserEdit(parameters: $parameters);
});
$router->addRoute(name: 'app_admin_users_add', route: '/admin/users/add', callback: function () use ($addressBookAdmin) {
$addressBookAdmin->adminUserAdd();
});
$router->addRoute(name: 'app_admin_users_delete', route: '/admin/users/delete/{nick}', callback: function (array $parameters) use ($addressBookAdmin) {
$addressBookAdmin->adminUserDelete(parameters: $parameters);
});
$router->addRoute(name: 'address_add', route: '/address/add', callback: function () use ($addressBook) {
$addressBook->addAddress();
});
$router->addRoute(name: 'address_add', route: '/address/update', callback: function () use ($addressBook) {
$addressBook->updateAddress();
});
$router->addRoute(name: 'address_add', route: '/address/delete', callback: function () use ($addressBook) {
$addressBook->deleteAddress();
});
$router->handleRouting();

View File

@@ -0,0 +1,161 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Controller;
use App\Entity\User;
use App\Service\Router;
use App\Service\Template;
use App\Repository\UserRepository;
class AddressBookAdminController
{
public function __construct(
private readonly Template $template,
private readonly User $user,
private readonly UserRepository $userRepository,
private readonly Router $router
)
{
}
private function adminCheck(): void
{
if (!$this->user->isAdmin()) {
$this->template->render(templateName: 'status/403.html.php', vars: [
'user' => $this->user,
'router' => $this->router
]);
}
}
public function admin(): never
{
$this->adminCheck();
$this->template->render(templateName: 'admin/index.html.php', vars: [
'user' => $this->user,
'router' => $this->router
]);
}
public function adminUser(): never
{
$this->adminCheck();
$users = $this->userRepository->findAll();
$this->template->render(templateName: 'admin/users.html.php', vars: [
'user' => $this->user,
'users' => $users,
'router' => $this->router
]);
}
public function adminUserEdit(array $parameters): never
{
$this->adminCheck();
if (!empty($_POST)) {
if (!empty($_POST['is_admin'])) {
$isAdmin = 1;
} else {
$isAdmin = 0;
}
if (empty($_POST['new_password'])) {
$current = $this->userRepository->findByID(id: $_POST['id']);
$password = $current->getPassword();
$updateUser = new User(nick: $_POST['nick'], password: $password, first: $_POST['first'], last: $_POST['last'], id: $_POST['id'], isAdmin: $isAdmin);
} else {
$password = $_POST['new_password'];
$updateUser = new User(nick: $_POST['nick'], newPassword: $password, first: $_POST['first'], last: $_POST['last'], id: $_POST['id'], isAdmin: $isAdmin);
}
$this->userRepository->update(user: $updateUser);
$users = $this->userRepository->findAll();
$this->template->render(templateName: 'admin/users.html.php', vars: [
'user' => $this->user,
'users' => $users,
'router' => $this->router
]);
}
$editUser = $this->userRepository->findByNick(nick: $parameters['nick']);
$this->template->render(templateName: 'admin/users_edit.html.php', vars: [
'user' => $this->user,
'editUser' => $editUser,
'router' => $this->router
]);
}
public function adminUserAdd(): never
{
$this->adminCheck();
$nick = $_POST['nick'];
if ($this->userRepository->findByNick(nick: $nick)) {
die("User: $nick already exists");
}
if (!empty($_POST)) {
$isAdmin = empty($_POST['is_admin']) ? 0 : 1;
$user = new User(nick: $_POST['nick'], newPassword: $_POST['new_password'], first: $_POST['first'], last: $_POST['last'], isAdmin: $isAdmin);
if ($this->userRepository->insert(user: $user)) {
$users = $this->userRepository->findAll();
$this->template->render(templateName: 'admin/users.html.php', vars: [
'user' => $this->user,
'users' => $users,
'router' => $this->router
]);
} else {
die("Error inserting user");
}
}
$this->template->render(templateName: 'admin/users_add.html.php', vars: [
'user' => $this->user,
'router' => $this->router
]);
}
public function adminUserDelete(array $parameters): never
{
$this->adminCheck();
$nick = $parameters['nick'];
if ($user = $this->userRepository->findByNick(nick: $nick)) {
if ($this->userRepository->delete(user: $user)) {
$users = $this->userRepository->findAll();
$this->template->render(templateName: 'admin/users.html.php', vars: [
'user' => $this->user,
'users' => $users,
'router' => $this->router
]);
} else {
die("Error deleting user");
}
} else {
$this->template->render(templateName: 'status/404.html.php', vars: [
'user' => $this->user,
'router' => $this->router
]);
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Controller;
use App\Entity\User;
use App\Entity\AddressBookEntry;
use App\Enums\StatusCode;
use App\Enums\UserAuth;
use App\Service\Router;
use App\Service\Template;
use App\Repository\AddressRepository;
class AddressBookController
{
public function __construct(
private readonly Template $template,
private readonly User $user,
private readonly AddressRepository $addressRepository,
private readonly Router $router
)
{
// empty body
}
public function main(): never
{
if ($this->user->getAuth() != UserAuth::AUTH_ANONYMOUS) {
$addresses = $this->addressRepository->findAll();
}
$this->template->render(templateName: 'index.html.php', vars: [
'user' => $this->user,
'router' => $this->router,
'addresses' => $addresses ?? []
]);
}
public function addAddress(): never
{
if (!empty($_POST)) {
$address = new AddressBookEntry(owner: $_POST['owner'], first: $_POST['first'], last: $_POST['last'], street: $_POST['street'], zip: $_POST['zip'], city: $_POST['city'], phone: $_POST['phone']);
if ($this->addressRepository->insert(address: $address)) {
$addresses = $this->addressRepository->findAll();
$this->template->render(templateName: 'index.html.php', vars: [
'user' => $this->user,
'addresses' => $addresses,
'router' => $this->router
]);
} else {
die("Error inserting user");
}
}
$this->template->render(templateName: 'addressbook/add_address.html.php', vars: [
'user' => $this->user,
'router' => $this->router
]);
}
public function updateAddress(): void
{
$_POST = json_decode(json: file_get_contents(filename: "php://input"), associative: true);
if (empty($_POST)) {
$this->template->renderJson(results: [
'status' => 400,
'message' => 'BAD REQUEST'
]);
}
if ($address = new AddressBookEntry(owner: $_POST['owner'], first: $_POST['first'], last: $_POST['last'], street: $_POST['street'], zip: $_POST['zip'], city: $_POST['city'], phone: $_POST['phone'], id: $_POST['id'])) {
if ($this->addressRepository->update(address: $address)) {
$status = 200;
$message = 'OK';
} else {
$status = 400;
$message = 'BAD_REQUEST';
}
} else {
$status = 400;
$message = "BAD REQUEST";
}
$this->template->renderJson(results: [
'status' => $status,
'message' => $message
]);
}
public function deleteAddress(): void
{
$_POST = json_decode(json: file_get_contents(filename: "php://input"), associative: true);
if (empty($_POST)) {
$this->template->renderJson(results: [
'status' => 400,
'message' => 'BAD REQUEST'
]);
}
if ($address = $this->addressRepository->findByID(id: $_POST['id'])) {
if ($this->addressRepository->delete(addressBookEntry: $address)) {
$this->template->renderJson(results: [
'status' => 200,
'message' => 'OK'
]);
} else {
$this->template->renderJson(results: [
'status' => 400,
'message' => 'BAD REQUEST'
]);
}
} else {
$this->template->renderJson(results: [
'status' => 400,
'message' => 'BAD REQUEST'
]);
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Controller;
use App\Entity\User;
use App\Repository\UserRepository;
use App\Service\Router;
use App\Service\Template;
class SecurityController
{
public function __construct(
private readonly Template $template,
private readonly UserRepository $userRepository,
private readonly Router $router
)
{
}
public function login(): never
{
if (!empty($_POST)) {
$nick = $_POST['nick'] ?? '';
$password = $_POST['password'] ?? '';
if ($nick && $password) {
$nick = strtolower(string: $nick);
if ($user = $this->userRepository->findbyNick(nick: $nick)) {
if (password_verify(password: $password, hash: $user->getPassword())) {
$_SESSION['user_id'] = $user->getId();
header(header: 'Location: /');
exit(0);
} else {
$message = "Wrong credentials.";
}
} else {
$message = "User not found.";
}
} else {
$message = 'You need to enter your credentials.';
}
}
$this->template->render(templateName: 'security/login.html.php', vars: [
'user' => $user ?? new User(),
'message' => $message ?? '',
'router' => $this->router
]);
}
function logout(): void
{
session_unset();
header(header: 'Location: /');
}
}

View File

@@ -0,0 +1,107 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Entity;
class AddressBookEntry
{
public function __construct(
private int $owner,
private string $first,
private string $last,
private string $street,
private string $zip,
private string $city,
private string $phone,
private int $id = 0,
)
{
// empty body
}
public function getOwner(): int
{
return $this->owner;
}
public function setOwner(int $owner): void
{
$this->owner = $owner;
}
public function getStreet(): string
{
return $this->street;
}
public function setStreet(string $street): void
{
$this->street = $street;
}
public function getZip(): string
{
return $this->zip;
}
public function setZip(string $zip): void
{
$this->zip = $zip;
}
public function getCity(): string
{
return $this->city;
}
public function setCity(string $city): void
{
$this->city = $city;
}
public function getPhone(): string
{
return $this->phone;
}
public function setPhone(string $phone): void
{
$this->phone = $phone;
}
public function getId(): int
{
return $this->id;
}
public function setId(int $id): void
{
$this->id = $id;
}
public function getFirst(): string
{
return $this->first;
}
public function setFirst(string $first): void
{
$this->first = $first;
}
public function getLast(): string
{
return $this->last;
}
public function setLast(string $last): void
{
$this->last = $last;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Entity;
use Closure;
class Route
{
public function __construct(
private string $name,
private string $route,
private string $regEx,
private array $parameters,
private Closure $callback
)
{
// empty body
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getRoute(): string
{
return $this->route;
}
public function setRoute(string $route): void
{
$this->route = $route;
}
public function getRegEx(): string
{
return $this->regEx;
}
public function setRegEx(string $regEx): void
{
$this->regEx = $regEx;
}
public function getParameters(): array
{
return $this->parameters;
}
public function setParameters(array $parameters): void
{
$this->parameters = $parameters;
}
public function getCallback(): Closure
{
return $this->callback;
}
public function setCallback(Closure $callback): void
{
$this->callback = $callback;
}
}

114
Vanilla/src/Entity/User.php Normal file
View File

@@ -0,0 +1,114 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Entity;
use App\Enums\UserAuth;
class User
{
public function __construct(
private string $nick = '',
private string $password = '',
private readonly string $newPassword = '',
private string $first = '',
private string $last = '',
private int $id = 0,
private bool $isAdmin = false,
private UserAuth $userAuth = UserAuth::AUTH_ANONYMOUS
)
{
if (!empty($this->newPassword)) {
echo "password";
$this->password = password_hash(password: $this->newPassword, algo: PASSWORD_ARGON2I);
}
if (session_status() === PHP_SESSION_ACTIVE) {
// ANONYMOUS has id 0
if ($this->id != 0) {
if ($this->isAdmin) {
$this->userAuth = UserAuth::AUTH_ADMIN;
} else {
$this->userAuth = UserAuth::AUTH_USER;
}
}
}
}
public function getNick(): string
{
return $this->nick;
}
public function setNick(string $nick): void
{
$this->nick = $nick;
}
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): void
{
$this->password = $password;
}
public function getFirst(): string
{
return $this->first;
}
public function setFirst(string $first): void
{
$this->first = $first;
}
public function getLast(): string
{
return $this->last;
}
public function setLast(string $last): void
{
$this->last = $last;
}
public function getId(): int
{
return $this->id;
}
public function setId(int $id): void
{
$this->id = $id;
}
public function isAdmin(): bool
{
return $this->isAdmin;
}
public function setIsAdmin(bool $isAdmin): void
{
$this->isAdmin = $isAdmin;
}
public function getAuth(): UserAuth
{
return $this->userAuth;
}
public function setAuth(UserAuth $userAuth): void
{
$this->userAuth = $userAuth;
}
}

View File

@@ -0,0 +1,17 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Enums;
enum UserAuth
{
case AUTH_ANONYMOUS;
case AUTH_USER;
case AUTH_ADMIN;
}

View File

@@ -0,0 +1,174 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Repository;
use App\Entity\AddressBookEntry;
use App\Service\DatabaseConnection;
use PDO;
use PDOException;
/**
* Handles CRUD of Addresses class.
*/
class AddressRepository
{
public function __construct(private readonly DatabaseConnection $databaseConnection)
{
// empty body
}
public function findAll(string $orderBy = 'last'): array
{
$sql = "
SELECT id, owner, first, last, street, zip, city, phone
FROM " . DatabaseConnection::TABLE_ADDRESSES . "
ORDER BY :order";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':order', var: $orderBy);
$statement->execute();
$addresses = [];
while ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
$address = new AddressBookEntry(
owner: htmlspecialchars(string: $result['owner']),
first: htmlspecialchars(string: $result['first']),
last: htmlspecialchars(string: $result['last']),
street: htmlspecialchars(string: $result['street']),
zip: htmlspecialchars(string: $result['zip']),
city: htmlspecialchars(string: $result['city']),
phone: htmlspecialchars(string: $result['phone']),
id: htmlspecialchars(string: $result['id']));
$addresses[] = $address;
}
return $addresses;
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function findByID(int $id): ?AddressBookEntry
{
$sql = "
SELECT id, owner, first, last, street, zip, city, phone
FROM " . DatabaseConnection::TABLE_ADDRESSES . "
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':id', var: $id);
$statement->execute();
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
return new AddressBookEntry(
owner: htmlspecialchars(string: $result['owner']),
first: htmlspecialchars(string: $result['first']),
last: htmlspecialchars(string: $result['last']),
street: htmlspecialchars(string: $result['street']),
zip: htmlspecialchars(string: $result['zip']),
city: htmlspecialchars(string: $result['city']),
phone: htmlspecialchars(string: $result['phone']),
id: htmlspecialchars(string: $result['id']));
} else {
return null;
}
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function insert(AddressBookEntry $address): bool|string
{
$sql = "
INSERT INTO " . DatabaseConnection::TABLE_ADDRESSES . " (owner, first, last, city, zip, street, phone)
VALUES (:owner, :first, :last, :city, :zip, :street, :phone)";
try {
$owner = $address->getOwner();
$first = $address->getFirst();
$last = $address->getLast();
$city = $address->getCity();
$zip = $address->getZip();
$street = $address->getStreet();
$phone = $address->getPhone();
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':owner', var: $owner);
$statement->bindParam(param: ':first', var: $first);
$statement->bindParam(param: ':last', var: $last);
$statement->bindParam(param: ':city', var: $city);
$statement->bindParam(param: ':zip', var: $zip);
$statement->bindParam(param: ':street', var: $street);
$statement->bindParam(param: ':phone', var: $phone);
$statement->execute();
return $this->databaseConnection->getConnection()->lastInsertId();
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function update(AddressBookEntry $address): bool|int
{
$id = $address->getId();
$first = $address->getFirst();
$last = $address->getLast();
$street = $address->getStreet();
$zip = $address->getZip();
$city = $address->getCity();
$phone = $address->getPhone();
$sql = "
UPDATE " . DatabaseConnection::TABLE_ADDRESSES . " SET
first = :first,
last = :last,
street = :street,
zip = :zip,
city = :city,
phone = :phone
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: 'id', var: $id);
$statement->bindParam(param: 'first', var: $first);
$statement->bindParam(param: 'last', var: $last);
$statement->bindParam(param: 'street', var: $street);
$statement->bindParam(param: 'zip', var: $zip);
$statement->bindParam(param: 'city', var: $city);
$statement->bindParam(param: 'phone', var: $phone);
return $statement->execute();
} catch (PDOException $e) {
echo $e->getMessage();
return false;
}
}
public function delete(AddressBookEntry $addressBookEntry): int
{
$sql = "
DELETE FROM " . DatabaseConnection::TABLE_ADDRESSES . "
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$id = $addressBookEntry->getId();
$statement->bindParam(param: 'id', var: $id);
return $statement->execute();
} catch (PDOException $e) {
exit($e->getMessage());
}
}
}

View File

@@ -0,0 +1,196 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Repository;
use App\Service\DatabaseConnection;
use App\Entity\User;
use PDO;
use PDOException;
/**
* Handles CRUD of User class.
*/
class UserRepository
{
public function __construct(private readonly DatabaseConnection $databaseConnection)
{
// empty body
}
public function findAll(string $orderBy = 'nick'): array
{
$users = [];
$sql = "
SELECT id, nick, password, first, last, is_admin
FROM " . DatabaseConnection::TABLE_USERS . "
ORDER BY :order";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':order', var: $orderBy);
$statement->execute();
while ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
$user = new User(
nick: htmlspecialchars(string: $result['nick']),
password: $result['password'],
first: htmlspecialchars(string: $result['first']),
last: htmlspecialchars(string: $result['last']),
id: $result['id'],
isAdmin: $result['is_admin']);
$users[] = $user;
}
return $users;
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function findByID(int $id): ?User
{
$sql = "
SELECT id, nick, password, first, last, is_admin
FROM " . DatabaseConnection::TABLE_USERS . "
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':id', var: $id);
$statement->execute();
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
return new User(
nick: htmlspecialchars(string: $result['nick']),
password: $result['password'],
first: htmlspecialchars(string: $result['first']),
last: htmlspecialchars(string: $result['last']),
id: $result['id'],
isAdmin: $result['is_admin']);
} else {
return null;
}
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function findByNick(string $nick): ?User
{
$nick = strtolower(string: $nick);
$sql = "
SELECT id, nick, password, first, last, is_admin
FROM " . DatabaseConnection::TABLE_USERS . "
WHERE nick = :nick";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':nick', var: $nick);
$statement->execute();
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
return new User(
nick: htmlspecialchars(string: $result['nick']),
password: $result['password'],
first: htmlspecialchars(string: $result['first']),
last: htmlspecialchars(string: $result['last']),
id: $result['id'],
isAdmin: $result['is_admin']);
} else {
return null;
}
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function insert(User $user): bool|string
{
$sql = "
INSERT INTO " . DatabaseConnection::TABLE_USERS . " (nick, password, first, last, is_admin)
VALUES (:nick, :password, :first, :last, :is_admin)";
try {
$nick = $user->getNick();
$password = $user->getPassword();
$first = $user->getFirst();
$last = $user->getLast();
$isAdmin = $user->isAdmin() ? 1 : 0;
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':nick', var: $nick);
$statement->bindParam(param: ':password', var: $password);
$statement->bindParam(param: ':first', var: $first);
$statement->bindParam(param: ':last', var: $last);
$statement->bindParam(param: ':is_admin', var: $isAdmin);
$statement->execute();
return $this->databaseConnection->getConnection()->lastInsertId();
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function update(User $user): bool
{
$id = $user->getId();
$nick = $user->getNick();
$first = $user->getFirst();
$last = $user->getLast();
$isAdmin = $user->isAdmin() ? 1 : 0;
if ($user->getPassword()) {
$password = $user->getPassword();
} else {
$current = $this->findByID(id: $id);
$password = $current->getPassword();
}
$sql = "
UPDATE " . DatabaseConnection::TABLE_USERS . " SET
nick = :nick,
password = :password,
first = :first,
last = :last,
is_admin = :is_admin
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: 'id', var: $id);
$statement->bindParam(param: 'nick', var: $nick);
$statement->bindParam(param: 'password', var: $password);
$statement->bindParam(param: 'first', var: $first);
$statement->bindParam(param: 'last', var: $last);
$statement->bindParam(param: 'is_admin', var: $isAdmin);
return $statement->execute();
} catch (PDOException $e) {
echo $e->getMessage();
return false;
}
}
public function delete(User $user): bool
{
$sql = "
DELETE FROM " . DatabaseConnection::TABLE_USERS . "
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$id = $user->getId();
$statement->bindParam(param: 'id', var: $id);
return $statement->execute();
} catch (PDOException $e) {
exit($e->getMessage());
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Service;
/**
*
*/
class Config
{
private array $config;
public function __construct()
{
// Check for either config.json.local or config.json.
$configFile = dirname(path: __DIR__, levels: 2) . "/config.json.local";
if (!file_exists(filename: $configFile)) {
$configFile = dirname(path: __DIR__, levels: 2) . "/config.json";
}
if (!file_exists(filename: $configFile)) {
die('Missing config file');
}
$configJSON = file_get_contents(filename: $configFile);
if (json_decode(json: $configJSON) === null) {
die('Config file is not valid JSON.');
}
$this->config = json_decode(json: $configJSON, associative: true);
}
public function getConfig(string $configKey): string
{
return $this->config[$configKey];
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Service;
use App\Controller\AddressBookAdminController;
use App\Controller\AddressBookController;
use App\Controller\SecurityController;
use App\Entity\User;
use App\Repository\AddressRepository;
use App\Repository\UserRepository;
/*
* A quick and dirty class container for DI.
* Caveat: Classes are always instantiated
* No autowiring (yet, maybe later, but it might fit for a demo)
*/
class Container
{
private AddressBookController $addressBook;
private AddressBookAdminController $addressBookAdmin;
private AddressRepository $addressRepository;
private Config $config;
private DatabaseConnection $databaseConnection;
private Router $router;
private SecurityController $securityController;
private Template $template;
private User $user;
private UserRepository $userRepository;
public function __construct()
{
$this->config = new Config();
$this->databaseConnection = new DatabaseConnection(config: $this->config);
$this->template = new Template(templateDir: dirname(path: __DIR__, levels: 2) . '/templates/');
$this->router = new Router(template: $this->template);
$this->userRepository = new UserRepository(databaseConnection: $this->databaseConnection);
$this->addressRepository = new AddressRepository(databaseConnection: $this->databaseConnection);
$this->securityController = new SecurityController(template: $this->template, userRepository: $this->userRepository, router: $this->router);
if (empty($_SESSION['user_id'])) {
$this->user = new User(); // ANONYMOUS
} else {
$this->user = $this->userRepository->findByID(id: $_SESSION['user_id']);
}
$this->addressBook = new AddressBookController(template: $this->template, user: $this->user, addressRepository: $this->addressRepository, router: $this->router);
$this->addressBookAdmin = new AddressBookAdminController(template: $this->template, user: $this->user, userRepository: $this->userRepository, router: $this->router);
}
public function get(string $className): object
{
return match ($className) {
'App\Controller\AddressBookController' => $this->addressBook,
'App\Controller\AddressBookAdminController' => $this->addressBookAdmin,
'App\Controller\SecurityController' => $this->securityController,
'App\Service\Router' => $this->router,
//default => throw new Exception(message: "Missing class definition: $class")
default => die("Missing class definition: $className")
};
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Service;
use PDO;
/**
* Take care of the PDO object.
*/
class DatabaseConnection
{
private PDO $dbConnection;
// Currently no prefixes are used, but could be easily added to config.json.
const TABLE_PREFIX = '';
const TABLE_USERS = self::TABLE_PREFIX . "users";
const TABLE_ADDRESSES = self::TABLE_PREFIX . "addresses";
public function __construct(private readonly Config $config)
{
$dbHost = $this->config->getConfig(configKey: 'dbHost');
$dbPort = $this->config->getConfig(configKey: 'dbPort');
$dbDatabase = $this->config->getConfig(configKey: 'dbDatabase');
$dbUser = $this->config->getConfig(configKey: 'dbUser');
$dbPassword = $this->config->getConfig(configKey: 'dbPassword');
$this->dbConnection = new PDO(
dsn: "mysql:host=$dbHost;port=$dbPort;charset=utf8mb4;dbname=$dbDatabase",
username: $dbUser,
password: $dbPassword
);
$this->dbConnection->setAttribute(attribute: PDO::ATTR_ERRMODE, value: PDO::ERRMODE_EXCEPTION);
}
public function getConnection(): PDO
{
return $this->dbConnection;
}
}

View File

@@ -0,0 +1,124 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Service;
use App\Entity\Route;
use Closure;
/*
* A small router implementation for the address book demo.
* Currently it doesn't handle GET requests, as not needed.
* But if I reuse the code in my bindApi I'll maybe support GET as well.
*/
class Router
{
/*
* The easiest way to differentiate between static and dynamic routes is using
* two arrays, no need to pollute the class Route with that information
*/
private array $staticRoutes = [];
private array $dynamicRoutes = [];
public function __construct(private readonly Template $template)
{
// empty body
}
/*
* This method takes a route like /admin/users/{user} and creates a regex to match on call
* More complex routes like /posts/{thread}/show/{page} are supported as well.
*/
function addRoute(string $name, string $route, Closure $callback): void
{
// check for parameters
preg_match_all(pattern: "/(?<={).+?(?=})/", subject: $route, matches: $matches);
$parameters = $matches[0];
// create regex for route:
$regex = preg_replace(pattern: '/{.+?}/', replacement: '([a-zA-Z0-9]*)', subject: $route);
// escape \ in regex
$regex = '/^' . str_replace(search: "/", replace: '\\/', subject: $regex) . '$/i';
$route = new Route(name: $name, route: $route, regEx: $regex, parameters: $parameters, callback: $callback);
if ($parameters) {
$this->dynamicRoutes[] = $route;
} else {
$this->staticRoutes[] = $route;
}
}
/*
* Checks if there is a known route and executes the callback.
*/
public function handleRouting(): void
{
$requestUri = $_SERVER['REQUEST_URI'];
/*
* Static routes have precedence over dynamic ones, so
* /admin/user/add to add and
* /admin/user/{name} to edit is possible.
* A user named "add" of course not :)
*
* But who wants to call their users "add" or "delete"?
* That's as weird as Little Bobby Tables … (https://xkcd.com/327/)
*/
foreach ($this->staticRoutes as $route) {
if (preg_match(pattern: $route->getRegex(), subject: $requestUri, matches: $matches)) {
call_user_func(callback: $route->getCallback());
// We've found our route, bail out.
return;
}
}
foreach ($this->dynamicRoutes as $route) {
// PHPStorm doesn't know that $parameters are always available,
// (as these are dynamic routes) so I init the array just to make PHPStorm happy.
$parameters = [];
if (preg_match(pattern: $route->getRegex(), subject: $requestUri, matches: $matches)) {
foreach ($route->getParameters() as $id => $parameter) {
$parameters[$parameter] = $matches[$id + 1];
}
// PHP is mad about named parameters in call_user_func when adding parameters.
// Uncaught Error: Unknown named parameter $args in <sourceFile>
// But PHPStorm seems happy without them. So what?
call_user_func($route->getCallback(), $parameters);
return;
}
}
// if no route is matched, throw a 404
$this->template->render(templateName: 'status/404.html.php');
}
public function path(string $routeName, array $vars = [])
{
foreach (array_merge($this->dynamicRoutes, $this->staticRoutes) as $route) {
if ($route->getName() == $routeName) {
if ($vars) {
// build route for dynamic routes
$route = $route->getRoute();
// replace placeholder with current values
foreach ($vars as $key => $value) {
$route = str_replace(search: '{' . $key . '}', replace: $value, subject: $route);
}
return $route;
} else {
return $route->getRoute();
}
}
}
// no 404, this is reached only if the code is buggy
die("Missing Route: $routeName");
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* Copyright (c) 2022 Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Service;
/*
* As I'm not allowed to use 3rd party code like Twig or Smarty I ended up
* using PHP as a templating engine.
*/
class Template
{
/*
* Just store the information about the template base dir.
*/
public function __construct(private readonly string $templateDir)
{
// empty body
}
/*
* Add variables to template and throw it out
*/
public function render(string $templateName, array $vars = []): never
{
// assign template vars
foreach ($vars as $name => $value) {
$$name = $value;
}
$templateFile = $this->templateDir . $templateName;
if (file_exists(filename: $templateFile)) {
include $this->templateDir . $templateName;
} else {
die("Missing template: $templateFile");
}
exit(0);
}
/*
* For AJAX calls, return json
*/
public function renderJson(array $results): never
{
http_response_code(response_code: $results['status']);
echo json_encode(value: $results);
exit(0);
}
}

18
Vanilla/src/bootstrap.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
spl_autoload_register(callback: function ($className) {
$prefix = 'App';
$baseDir = __DIR__;
$prefixLen = strlen(string: $prefix);
if (strncmp(string1: $prefix, string2: $className, length: $prefixLen) !== 0) {
die("Invalid class: $className");
}
$realClassNamePSRpath = substr(string: $className, offset: $prefixLen);
$classLocation = $baseDir . str_replace(search: '\\', replace: '/', subject: $realClassNamePSRpath) . '.php';
if (file_exists(filename: $classLocation)) {
require $classLocation;
} else {
die("Invalid class: $className");
}
});

View File

@@ -0,0 +1,7 @@
<script src="/assets/js/functions.js"></script>
<?php if (!empty($message)): ?>
<script>setError('<?= $message ?>')</script>
<?php endif; ?>
</body>
</html>

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Address Book
<?php if (!empty($user->getNick())): ?>
&nbsp;-&nbsp;<?= $user->getNick() ?>
<?php endif; ?>
</title>
<link rel="stylesheet" href="/assets/styles/main.css">
</head>
<body>
<h1>Address Book</h1>
<a href="<?= $router->path('app_main'); ?>">&#8962;&nbsp;Home</a>
<a href="<?= $router->path('app_admin'); ?>">&#9881;&nbsp;Admin</a>
<?php if (empty($user) || $user->getAuth() == \App\Enums\UserAuth::AUTH_ANONYMOUS): ?>
<a href="<?= $router->path('app_login'); ?>">&#9094;&nbsp;Login</a>
<?php else: ?>
<a href="<?= $router->path('app_logout'); ?>">&#9099;&nbsp;Logout</a>
<?php endif; ?>
<br>
<div id="info_box">Info</div>
<div id="error_box">
<span class="close_button" onclick="closeError()">&times;</span>
<div id="error_text"></div>
</div>
<br>

View File

@@ -0,0 +1,35 @@
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
<form method="POST">
<input type="hidden" name="owner" id="owner" value="<?= $user->getId() ?>">
<label for="first">First</label>
<input type="text" name="first" id="first" required>
<br>
<label for="last">Last</label>
<input type="text" name="last" id="last" required>
<br>
<label for="city">City</label>
<input type="text" name="city" id="city">
<br>
<label for="zip">Zip</label>
<input type="text" name="zip" id="zip">
<br>
<label for="street">Street</label>
<input type="text" name="street" id="street">
<br>
<label for="phone">Phone</label>
<input type="text" name="phone" id="phone">
<br>
<!-- maybe later -->
<!-- <input type="hidden" name="_csrf" value="csrf_token" -->
<input type="submit" value="Save">
</form>
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>

View File

@@ -0,0 +1,10 @@
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
<br>
<h1>Address Book - Admin</h1>
<a href="<?= $router->path('app_admin_users'); ?>">&#128113;&nbsp;Users</a>
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>
<button type="button" class="info_button" id="info_button" onclick="setInfo('Test Info - auto hide')">Info</button>
<button type="button" class="error_button" onclick="setError('Test Error - must be closed manually')">Error</button>

View File

@@ -0,0 +1,35 @@
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
<h2>User list</h2>
<table>
<tr>
<th>Nick</th>
<th>First</th>
<th>Last</th>
<th>Is Admin</th>
<th colspan="2">&nbsp;</th>
</tr>
<?php foreach ($users as $userLine): ?>
<tr>
<td><?= $userLine->getNick() ?></td>
<td><?= $userLine->getFirst() ?></td>
<td><?= $userLine->getLast() ?></td>
<td style="text-align: center;">
<?php if ($userLine->isAdmin()): ?>
☑️
<?php else: ?>
👎
<?php endif; ?>
</td>
<td>
<a href="<?= $router->path('app_admin_users_edit', vars: ['nick' => $userLine->getNick()]); ?>">edit</a>
</td>
<td>
<a href="<?= $router->path('app_admin_users_delete', vars: ['nick' => $userLine->getNick()]); ?>">delete</a>
</td>
</tr>
<?php endforeach; ?>
</table>
<a href="<?= $router->path('app_admin_users_add'); ?>">Add User</a>
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>

View File

@@ -0,0 +1,29 @@
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
<form method="POST">
<label for="nick">Username</label>
<input type="text" name="nick" id="nick" maxlength="20" required>
<br>
<label for="new_password">Password</label>
<input type="password" name="new_password" id="new_password" maxlength="40" required>
<br>
<label for="first">First</label>
<input type="text" name="first" id="first" maxlength="40" required>
<br>
<label for="last">Last</label>
<input type="text" name="last" id="last" maxlength="40" required>
<br>
<label for="is_admin">Is Admin</label>
<input type="checkbox" name="is_admin" id="is_admin">
<br>
<!-- maybe later -->
<!-- <input type="hidden" name="_csrf" value="csrf_token" -->
<input type="submit" value="Save">
</form>
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>

View File

@@ -0,0 +1,31 @@
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
<form method="POST">
<input type="hidden" name="id" value="<?= $editUser->getId() ?>">
<label for="nick">Username</label>
<input type="text" name="nick" id="nick" value="<?= $editUser->getNick() ?>" maxlength="20" required>
<br>
<label for="new_password">Password (leave empty to keep the current one)</label>
<input type="password" name="new_password" id="new_password" maxlength="40">
<br>
<label for="first">First</label>
<input type="text" name="first" id="first" value="<?= $editUser->getFirst() ?>" maxlength="40" required>
<br>
<label for="last">Last</label>
<input type="text" name="last" id="last" value="<?= $editUser->getLast() ?>" maxlength="40" required>
<br>
<label for="is_admin">Is Admin</label>
<input type="checkbox" name="is_admin" id="is_admin" <?= $editUser->isAdmin()?'checked':'' ?>>
<br>
<!-- maybe later -->
<!-- <input type="hidden" name="_csrf" value="csrf_token" -->
<input type="submit" value="Save">
</form>
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>

View File

@@ -0,0 +1,45 @@
<?php include '_header.html.php' ?>
<br>
<h2>Welcome to Address Book</h2>
<?php if(!empty($addresses)): ?>
<form method="POST">
<table id="address_table">
<tr>
<th id="first" onclick="sortBy('first')">First</th>
<th id="last" onclick="sortBy('last')">Last</th>
<th id="street" onclick="sortBy('street')">Street</th>
<th id="zip" onclick="sortBy('zip')">Zip</th>
<th id="city" onclick="sortBy('city')">City</th>
<th id="phone" onclick="sortBy('phone')">Phone</th>
<th colspan="2">&nbsp;</th>
</tr>
<?php foreach ($addresses as $address): ?>
<?php $id = $address->getId(); ?>
<tr id="row_<?= $id ?>">
<td><input type="text" id="first_<?= $id ?>" value="<?= $address->getFirst(); ?>" maxlength="80" disabled></td>
<td><input type="text" id="last_<?= $id ?>" value="<?= $address->getLast(); ?>" maxlength="80" disabled></td>
<td><input type="text" id="street_<?= $id ?>" value="<?= $address->getStreet(); ?>" maxlength="80" disabled></td>
<td><input type="text" id="zip_<?= $id ?>" value="<?= $address->getZip(); ?>" maxlength="10" disabled></td>
<td><input type="text" id="city_<?= $id ?>" value="<?= $address->getCity(); ?>" maxlength="80" disabled></td>
<td><input type="text" id="phone_<?= $id ?>" value="<?= $address->getPhone(); ?>" maxlength="30" disabled></td>
<td>
<input type="button" value="Edit" id="edit_button_<?= $id ?>" onclick="editAddress('<?= $id ?>')">
</td>
<td>
<input type="button" value="Delete" onclick="deleteAddress('<?= $id ?>')">
<input type="hidden" id="owner_<?= $id ?>" value="<?= $id ?>">
</td>
</tr>
<?php endforeach; ?>
</table>
</form>
<?php $addAddress = $router->path('address_add'); ?>
<input type="button" value="Add Address" onclick="addAddress('<?= $addAddress ?>')">
<?php else: ?>
Your addresses will be listed soon …
<?php endif; ?>
<?php include '_footer.html.php' ?>

View File

@@ -0,0 +1,21 @@
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
<?php if (false): ?>
You need Admin rights to access this area.
<?php else: ?>
<br>
<form method="POST">
<label for="nick">Username</label>
<input type="text" name="nick" id="nick">
<label for="password">Password</label>
<input type="password" name="password" id="password">
<!-- maybe later -->
<!-- <input type="hidden" name="_csrf" value="csrf_token" -->
<input type="submit" value="Login">
</form>
<?php endif; ?>
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>

View File

@@ -0,0 +1,5 @@
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
<h2>403 Permission denied</h2>
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>

View File

@@ -0,0 +1,3 @@
<h2>404 Page not found</h2>
The requested URL cannot be found on this server.