Compare commits

...

135 Commits

Author SHA1 Message Date
e262987298 split up into Vanilla and CakePHP 2022-11-25 12:01:28 +01:00
5f1f5e847d changed dir structure to handle second version of the book 2022-11-25 11:46:00 +01:00
b6fd7876d3 added addressbook dump 2022-11-01 18:37:46 +01:00
1dc31bcb1b check if message exists 2022-10-30 16:03:44 +01:00
3ff406d053 removed static message div 2022-10-30 16:02:03 +01:00
6abe519a44 added error message in text box 2022-10-30 16:01:29 +01:00
c6c88456cf keep sort order when changing the column, only revert in same colum 2022-10-29 14:26:42 +02:00
5a805aba07 removed sort indicator from html table 2022-10-29 14:07:12 +02:00
1541d05715 removed sort indicator from html table 2022-10-29 14:06:56 +02:00
dc78e203ea added ajax error handling 2022-10-28 17:02:16 +02:00
17a90358a7 added styling for info & error boxes and test buttons 2022-10-28 17:01:55 +02:00
0266a89ae3 added error handling to ajax calls 2022-10-28 17:01:29 +02:00
56a3e584e7 changed return to bool on update and delete 2022-10-28 17:01:08 +02:00
e2bf38299b changed return to bool on update and delete 2022-10-28 17:00:53 +02:00
24c8a3d9d7 added renderJson 2022-10-28 17:00:16 +02:00
dba37e57f9 added test buttons for info & error 2022-10-28 16:59:36 +02:00
c68cf1643e added info & error boxes 2022-10-28 16:58:58 +02:00
affe02ec04 added htmlspecialchars to visible fields 2022-10-27 15:42:43 +02:00
b678720ebd added maxlength 2022-10-27 15:38:33 +02:00
14f9f247bc changed some wording in comments 2022-10-27 12:13:37 +02:00
65a87acc48 fixed a stupid bug breaking the sort of row numbers larger than 9 2022-10-27 12:13:06 +02:00
e04cf94edd added htmlspecialchars for output 2022-10-27 10:53:32 +02:00
9ee8ae39df added opcache_ireset() for debugging 2022-10-27 10:37:05 +02:00
70aa4d1f06 changed some wording in comments 2022-10-26 13:16:04 +02:00
2f99531780 changed render from void to never 2022-10-26 13:15:41 +02:00
60860a0bce removed unused exception 2022-10-26 13:15:08 +02:00
bb3d0f6e1b lowercase login user 2022-10-26 13:14:39 +02:00
7ee8e8860e lowercase login user 2022-10-26 13:14:29 +02:00
32dcab7592 removed owner in update, we don't use it 2022-10-26 12:47:42 +02:00
88cf11d45d removed init from sortOrder 2022-10-26 12:46:03 +02:00
6c203a0213 added DOCTYPE 2022-10-26 12:45:36 +02:00
9867df0a04 sort only if table is actually displayed 2022-10-26 12:33:38 +02:00
0fba7bc0a0 break up if user already exists 2022-10-26 12:32:37 +02:00
f46e73c647 changed icon to html entity 2022-10-26 12:23:52 +02:00
1a8b31b3a2 changed icon to html entity 2022-10-26 12:21:50 +02:00
a832a3c60b changed default order from asc to desc as it will be reversed on first call 2022-10-26 12:13:42 +02:00
904fd798f6 removed redundant initializer from newTitle 2022-10-26 12:12:34 +02:00
4ed3dcd603 Added indicator to show if sorting is ascending or descending 2022-10-26 11:58:27 +02:00
8bc252ba4d finished things up 2022-10-26 11:16:20 +02:00
78f25fdfd3 removed some PPHPStorm warnings. 2022-10-26 11:13:04 +02:00
e2da6000b3 fixed a bug while generating the addAddress route. 2022-10-26 11:11:58 +02:00
32133a0d05 some format changes 2022-10-25 19:40:33 +02:00
572cc1eb89 reverted last name change, not needed 2022-10-25 19:40:33 +02:00
48585f14ab removed debug statements 2022-10-25 19:40:33 +02:00
47439fe358 removed debug statements 2022-10-25 19:40:33 +02:00
8e0a2ff6e8 every click on a column title changes the sort order 2022-10-25 19:40:33 +02:00
ce798e3b65 Delete '.gitinore' 2022-10-25 17:57:01 +02:00
21fd674dd0 initial commit 2022-10-25 17:56:29 +02:00
8864875699 initial commit 2022-10-25 16:09:17 +02:00
fe0a3c212d removed welcome until fixed 2022-10-25 16:09:17 +02:00
d2f5427df1 Delete 'config.json' 2022-10-25 16:04:20 +02:00
5ed539a471 added routes for the addressbook 2022-10-25 15:57:46 +02:00
5bc0b7966b addressbook seems functional 2022-10-25 15:57:17 +02:00
d0e1d2e87d temporary removed welcome line 2022-10-25 15:56:35 +02:00
1b794f775d fixed a wrong named variable 2022-10-25 15:53:28 +02:00
744117d958 added CRUD implementation 2022-10-25 15:52:46 +02:00
cce18f6516 added update() 2022-10-25 15:51:51 +02:00
a973a4362f added AddressBookController 2022-10-25 15:51:23 +02:00
3bf0c2b44f added error handling 2022-10-25 15:50:57 +02:00
ec022d09e4 added js include 2022-10-25 15:50:25 +02:00
148eff1557 added js include 2022-10-25 15:48:36 +02:00
ab4a00d25c added router->path() 2022-10-25 15:48:11 +02:00
753b832cbc inserted addressbook 2022-10-25 15:47:31 +02:00
e5f4656d71 removed header & footer 2022-10-25 15:46:52 +02:00
117903ac7c finished descending sorting 2022-10-25 15:46:06 +02:00
4665e1706f initial commit 2022-10-25 13:47:30 +02:00
8189abfd29 removed userid, added id 2022-10-25 13:47:07 +02:00
72503d7fe2 finished adding 2022-10-25 13:46:30 +02:00
3f4653b81f updated the fields 2022-10-25 10:34:16 +02:00
0ed667d594 updated some comments 2022-10-24 20:42:26 +02:00
fddc58fb14 prepared for addressbook handling 2022-10-24 20:29:34 +02:00
ad63a40a0d removed one line to be reinserted soon. 2022-10-24 20:27:39 +02:00
e13bbee1e6 fixed a typo 2022-10-24 20:27:03 +02:00
c6af0d1b1c Removing ignored files 2022-10-24 20:25:49 +02:00
fd368d895d added minimal table styling 2022-10-24 20:24:19 +02:00
e1d1ef5eeb added all needed routes (so far) 2022-10-24 20:20:52 +02:00
cfc4e22497 initial commit 2022-10-24 20:19:28 +02:00
66f02c8c6e added router to main 2022-10-24 20:12:48 +02:00
10efd6d1a6 moved insert from mock to working code 2022-10-24 20:12:04 +02:00
5e8c945170 added password hashing 2022-10-24 20:07:59 +02:00
54ecb6b15e added more classes 2022-10-24 20:05:08 +02:00
7160fd0804 renamed a label 2022-10-24 20:04:36 +02:00
2b34a89068 renamed a label 2022-10-24 20:04:24 +02:00
ab7ac074ba renamed password to new_password 2022-10-24 20:03:43 +02:00
6461046c31 renamed password to new_password 2022-10-24 20:03:27 +02:00
d2cdcdded4 simplified regex for route creation 2022-10-24 18:56:30 +02:00
7e19efd420 added path method 2022-10-24 18:54:43 +02:00
ffe4d43600 added router->path 2022-10-24 18:44:42 +02:00
c05aee49d5 added router->path 2022-10-24 18:44:18 +02:00
10d56751e4 renamed a variable 2022-10-24 18:43:43 +02:00
71f8e1623e renamed a variable 2022-10-24 18:43:28 +02:00
2836e0d878 renamed template 2022-10-24 18:43:00 +02:00
61aa012dd6 renamed template 2022-10-24 18:41:28 +02:00
ed1e795941 renamed template 2022-10-24 18:40:37 +02:00
7559651447 initial commit 2022-10-24 18:37:32 +02:00
bf5425737b added required to fields 2022-10-24 18:37:00 +02:00
0a9a253b84 initial commit 2022-10-24 18:36:14 +02:00
442cd25aa6 fixed a wrong named label 2022-10-23 19:16:10 +02:00
63d4f339e3 initial commit 2022-10-23 19:14:13 +02:00
2cd75e4694 initial commit 2022-10-23 19:13:39 +02:00
18a754add7 removed a leftover from the last refactoring 2022-10-23 18:57:07 +02:00
9b5f3a2847 fixed a typo 2022-10-23 18:51:26 +02:00
a88e861792 refactored admin stuff into own class 2022-10-23 18:49:30 +02:00
6284918056 added preference for static routes. 2022-10-23 18:46:42 +02:00
33618b3be0 die with error message if template doesn't exist. 2022-10-23 18:46:12 +02:00
b60d9481ab added 404 support 2022-10-23 18:04:03 +02:00
582698a8d0 initial commit 2022-10-23 14:49:41 +02:00
06c9b9443d initial commit 2022-10-23 14:27:54 +02:00
74381857f2 initial commit 2022-10-23 14:27:40 +02:00
f652132911 shut down after rendering the page 2022-10-23 14:22:18 +02:00
b4ab876463 added getters & setters 2022-10-23 12:46:00 +02:00
5447a7dbad added getters & setters 2022-10-23 12:44:55 +02:00
a3b2bf27f1 added getters & setters 2022-10-23 12:43:50 +02:00
4a85f45e3a initial commit 2022-10-23 12:41:14 +02:00
9a485b5424 added orderBy to findAll() 2022-10-23 12:40:40 +02:00
d19e7d5b25 fixed a typo 2022-10-23 12:36:24 +02:00
cbbfe45c1b added a side note 2022-10-23 12:32:53 +02:00
9cacf9ced9 fixed some typos 2022-10-23 12:30:09 +02:00
8a7a4a2253 added copyright 2022-10-23 12:26:30 +02:00
e9fd8e153d Added some small notes. 2022-10-23 12:11:57 +02:00
f571d7548a Removed named arguments from call_user_func 2022-10-22 19:51:20 +02:00
a55b14c71c Removed custom template logic, just use PHP as a template engine. 2022-10-22 13:11:52 +02:00
87959d34b7 initial commit 2022-10-22 12:44:26 +02:00
1ad567f337 refactored routing to handle any number of parameters 2022-10-22 12:43:57 +02:00
2a8fa3f397 dir commit 2022-10-21 20:26:20 +02:00
f6a5e96576 added year and contact info 2022-10-21 20:09:30 +02:00
b731e2c0e1 rewrite everything to index.php 2022-10-21 20:07:54 +02:00
f15c2f9ff2 just names changed 2022-10-21 19:55:06 +02:00
7d15313503 updated comment, cleaner names 2022-10-21 19:54:23 +02:00
98e6c7fc64 some cleanup of variable names 2022-10-21 19:53:44 +02:00
0a334498df adapted the tables 2022-10-21 19:51:09 +02:00
02c6154629 initial commit 2022-10-21 14:56:14 +02:00
65e2836f23 initial commit 2022-10-21 14:54:42 +02:00
983da7fe88 initial commit 2022-10-21 14:49:12 +02:00
9b3a6b1f3a initial commit 2022-10-21 14:48:56 +02:00
47 changed files with 2174 additions and 187 deletions

1
.gitignore vendored Normal file

@ -0,0 +1 @@
/config.json

@ -1,2 +1,8 @@
# addressbook
This repo hold a basic programming task I had to do for my new job.
The task was to write a simple address book with plain PHP and Javascript.
No external resources were allowed, so I had to write my own router DBAL and so on.
The result can be found in the Vanilla-Folder.
Now I got the job and will start working on a CakePHP project, so I decided to rewrite the address book in CakePHP.
Work will happen in the CakePHP-Folder.

@ -1,6 +1,6 @@
MIT License
Copyright (c) <year> <copyright holders>
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:

3
Vanilla/README.md Normal 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

@ -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

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

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

13
Vanilla/public/.htaccess Normal 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>

@ -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')
}
})

@ -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

@ -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();

@ -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
]);
}
}
}

@ -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'
]);
}
}
}

@ -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: /');
}
}

@ -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;
}
}

@ -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

@ -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;
}
}

@ -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;
}

@ -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());
}
}
}

@ -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());
}
}
}

@ -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];
}
}

@ -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")
};
}
}

@ -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;
}
}

@ -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");
}
}

@ -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);
}
}

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

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

@ -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>

@ -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' ?>

@ -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>

@ -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' ?>

@ -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' ?>

@ -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' ?>

@ -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' ?>

@ -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' ?>

@ -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' ?>

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

@ -1,29 +0,0 @@
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;
}

@ -1,13 +0,0 @@
<?php
ini_set(option: 'display_errors', value: 1);
ini_set(option: 'display_startup_errors', value: 1);
error_reporting(error_level: E_ALL);
require dirname(path: __DIR__) . '/src/bootstrap.php';
use App\Controller\AddressBook;
$container = new \App\Service\Container();
$addressBook = $container->get(AddressBook::class);
//$addressBook = new AddressBook();

@ -1,14 +0,0 @@
<?php
namespace App\Controller;
use App\Service\Template;
use stdClass;
class AddressBook extends stdClass
{
public function __construct(Template $template)
{
$template->render(templateName: 'index.tpl');
}
}

@ -1,16 +0,0 @@
<?php
namespace App\Entity;
class AddressBookEntry
{
public function __construct(
private int $userid,
private string $first,
private string $last,
private string $nick,
)
{
// empty body
}
}

@ -1,17 +0,0 @@
<?php
namespace App\Entity;
class User
{
public function __construct(
private string $nick,
private string $password,
private string $first = '',
private string $last = '',
private int $id = 0
)
{
// empty body
}
}

@ -1,33 +0,0 @@
<?php
namespace App\Service;
use App\Controller\AddressBook;
use Exception;
use stdClass;
class Container
{
// no autowiring yet, maybe later, but it might fit for a demo
private Template $template;
private AddressBook $addressBook;
public function __construct()
{
$this->template = new Template(templateDir: dirname(path: __DIR__, levels: 2) . '/templates/');
$this->addressBook = new AddressBook(template: $this->template);
}
/**
* @throws Exception
*/
public function get(string $class): stdClass
{
return match($class) {
'App\Controller\AddressBook' => $this->addressBook,
default => throw new Exception(message: "Missing class definition: $class")
};
}
}

@ -1,40 +0,0 @@
<?php
namespace App\Service;
use Exception;
class Template
{
public function __construct(private readonly string $templateDir)
{
// empty body
}
/**
* @throws Exception
*/
public function render(string $templateName): void
{
$template = file_get_contents(filename: $this->templateDir . $templateName);
// search for includes
preg_match_all(pattern: '/{% include ?\'?(.*?)\'? ?%}/i', subject: $template, matches: $matches, flags: PREG_SET_ORDER);
foreach ($matches as $value) {
$token = $value[0];
$include = $this->templateDir . $value[1];
if (file_exists(filename: $include)) {
$replacement = file_get_contents(filename: $include);
} else {
throw new Exception(message: "Missing included file: $include");
}
$template = str_replace(search: $token, replace: $replacement, subject: $template);
}
// remove the original template code
$template = preg_replace(pattern: '/{% include ?\'?(.*?)\'? ?%}/i', replacement: '', subject: $template);
echo $template;
}
}

@ -1,3 +0,0 @@
<!-- mind the javascript -->
</body>
</html>

@ -1,5 +0,0 @@
<html>
<head>
<title>Address Book</title>
<link rel="stylesheet" href="/assets/styles/main.css">
</head>

@ -1,8 +0,0 @@
{% include '_header.tpl' %}
<h1>Address Book</h1>
<a href="/">🏠 Home</a>
<a href="/">⚙ Admin</a>
<a href="/">🚪 Login</a>
{% include '_footer.tpl' %}