Compare commits

92 Commits

Author SHA1 Message Date
b8e5730ec0 first functional demo 2025-05-09 20:50:34 +02: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
73 changed files with 8714 additions and 305 deletions

1
.gitignore vendored Normal file

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

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

23
CakePHP/.github/ISSUE_TEMPLATE.md vendored Normal file

@ -0,0 +1,23 @@
This is a (multiple allowed):
* [x] bug
* [ ] enhancement
* [ ] feature-discussion (RFC)
* CakePHP Application Skeleton Version: EXACT RELEASE VERSION OR COMMIT HASH, HERE.
* Platform and Target: YOUR WEB-SERVER, DATABASE AND OTHER RELEVANT INFO AND HOW THE REQUEST IS BEING MADE, HERE.
### What you did
EXPLAIN WHAT YOU DID, PREFERABLY WITH CODE EXAMPLES, HERE.
### What happened
EXPLAIN WHAT IS ACTUALLY HAPPENING, HERE.
### What you expected to happen
EXPLAIN WHAT IS TO BE EXPECTED, HERE.
P.S. Remember, an issue is not the place to ask questions. You can use [Stack Overflow](https://stackoverflow.com/questions/tagged/cakephp)
for that or join the #cakephp channel on irc.freenode.net, where we will be more
than happy to help answer your questions.
Before you open an issue, please check if a similar issue already exists or has been closed before.

@ -0,0 +1,14 @@
<!---
**PLEASE NOTE:**
This is only a issue tracker for issues related to the CakePHP Application Skeleton.
For CakePHP Framework issues please use this [issue tracker](https://github.com/cakephp/cakephp/issues).
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue.
The best way to propose a feature is to open an issue first and discuss your ideas there before implementing them.
Always follow the [contribution guidelines](https://github.com/cakephp/cakephp/blob/master/.github/CONTRIBUTING.md) guidelines when submitting a pull request. In particular, make sure existing tests still pass, and add tests for all new behavior. When fixing a bug, you may want to add a test to verify the fix.
-->

12
CakePHP/.github/dependabot.yml vendored Normal file

@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10

89
CakePHP/.github/workflows/ci.yml vendored Normal file

@ -0,0 +1,89 @@
name: CI
on:
push:
branches:
- '4.x'
- '4.next'
- '5.x'
pull_request:
branches:
- '*'
permissions:
contents: read
jobs:
testsuite:
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
php-version: ['7.4', '8.0', '8.1']
name: PHP ${{ matrix.php-version }}
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, intl, pdo_sqlite
coverage: none
- name: Composer install
run: |
if [[ ${{ matrix.php-version }} == '8.1' ]]; then
composer update --ignore-platform-reqs
else
composer update
fi
composer run-script post-install-cmd --no-interaction
- name: Run PHPUnit
run: |
cp config/app_local.example.php config/app_local.php
vendor/bin/phpunit
env:
DATABASE_TEST_URL: sqlite://./testdb.sqlite
coding-standard:
name: Coding Standard
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
extensions: mbstring, intl
coverage: none
- name: Composer install
run: composer install
- name: Run PHP CodeSniffer
run: composer cs-check
static-analysis:
name: Static Analysis
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
extensions: mbstring, intl
coverage: none
- name: Composer install
run: composer require --dev phpstan/phpstan:^1.0.0
- name: Run phpstan
run: vendor/bin/phpstan

29
CakePHP/.github/workflows/stale.yml vendored Normal file

@ -0,0 +1,29 @@
name: Mark stale issues and pull requests
on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
jobs:
stale:
permissions:
issues: write # for actions/stale to close stale issues
pull-requests: write # for actions/stale to close stale PRs
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open for 120 days with no activity. Remove the `stale` label or comment or this will be closed in 15 days'
stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove the `stale` label or comment on this issue, or it will be closed in 15 days'
stale-issue-label: 'stale'
stale-pr-label: 'stale'
days-before-stale: 120
days-before-close: 15
exempt-issue-labels: 'pinned'
exempt-pr-labels: 'pinned'

@ -0,0 +1,47 @@
#
# Bash completion file for CakePHP console.
# Copy this file to a file named `cake` under `/etc/bash_completion.d/`.
# For more info check https://book.cakephp.org/4/en/console-commands/completion.html#how-to-enable-bash-autocompletion-for-the-cakephp-console
#
_cake()
{
local cur prev opts cake
COMPREPLY=()
cake="${COMP_WORDS[0]}"
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
if [[ "$cur" == -* ]] ; then
if [[ ${COMP_CWORD} = 1 ]] ; then
opts=$(${cake} completion options)
elif [[ ${COMP_CWORD} = 2 ]] ; then
opts=$(${cake} completion options "${COMP_WORDS[1]}")
else
opts=$(${cake} completion options "${COMP_WORDS[1]}" "${COMP_WORDS[2]}")
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
if [[ ${COMP_CWORD} = 1 ]] ; then
opts=$(${cake} completion commands)
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
if [[ ${COMP_CWORD} = 2 ]] ; then
opts=$(${cake} completion subcommands $prev)
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
if [[ $COMPREPLY = "" ]] ; then
_filedir
return 0
fi
return 0
fi
return 0
}
complete -F _cake cake bin/cake

5790
CakePHP/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

6
CakePHP/phpcs.xml Normal file

@ -0,0 +1,6 @@
<?xml version="1.0"?>
<ruleset name="App">
<config name="installed_paths" value="../../cakephp/cakephp-codesniffer"/>
<rule ref="CakePHP"/>
</ruleset>

8
CakePHP/phpstan.neon Normal file

@ -0,0 +1,8 @@
parameters:
level: 8
checkMissingIterableValueType: false
treatPhpDocTypesAsCertain: false
paths:
- src/
excludePaths:
- src/Console/Installer.php

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Controller;
/**
* Addresses Controller
*
* @property \App\Model\Table\AddressesTable $Addresses
* @method \App\Model\Entity\Address[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
*/
class AddressesController extends AppController
{
/**
* Index method
*
* @return \Cake\Http\Response|null|void Renders view
*/
public function index()
{
$addresses = $this->paginate($this->Addresses);
$this->set(compact('addresses'));
}
/**
* View method
*
* @param string|null $id Address id.
* @return \Cake\Http\Response|null|void Renders view
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function view($id = null)
{
$address = $this->Addresses->get($id, [
'contain' => [],
]);
$this->set(compact('address'));
}
/**
* Add method
*
* @return \Cake\Http\Response|null|void Redirects on successful add, renders view otherwise.
*/
public function add()
{
$address = $this->Addresses->newEmptyEntity();
if ($this->request->is('post')) {
$address = $this->Addresses->patchEntity($address, $this->request->getData());
if ($this->Addresses->save($address)) {
$this->Flash->success(__('The address has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The address could not be saved. Please, try again.'));
}
$this->set(compact('address'));
}
/**
* Edit method
*
* @param string|null $id Address id.
* @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function edit($id = null)
{
$address = $this->Addresses->get($id, [
'contain' => [],
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$address = $this->Addresses->patchEntity($address, $this->request->getData());
if ($this->Addresses->save($address)) {
$this->Flash->success(__('The address has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The address could not be saved. Please, try again.'));
}
$this->set(compact('address'));
}
/**
* Delete method
*
* @param string|null $id Address id.
* @return \Cake\Http\Response|null|void Redirects to index.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function delete($id = null)
{
$this->request->allowMethod(['post', 'delete']);
$address = $this->Addresses->get($id);
if ($this->Addresses->delete($address)) {
$this->Flash->success(__('The address has been deleted.'));
} else {
$this->Flash->error(__('The address could not be deleted. Please, try again.'));
}
return $this->redirect(['action' => 'index']);
}
}

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Controller;
/**
* Users Controller
*
* @property \App\Model\Table\UsersTable $Users
* @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
*/
class UsersController extends AppController
{
/**
* Index method
*
* @return \Cake\Http\Response|null|void Renders view
*/
public function index()
{
$users = $this->paginate($this->Users);
$this->set(compact('users'));
}
/**
* View method
*
* @param string|null $id User id.
* @return \Cake\Http\Response|null|void Renders view
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function view($id = null)
{
$user = $this->Users->get($id, [
'contain' => [],
]);
$this->set(compact('user'));
}
/**
* Add method
*
* @return \Cake\Http\Response|null|void Redirects on successful add, renders view otherwise.
*/
public function add()
{
$user = $this->Users->newEmptyEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->getData());
if ($this->Users->save($user)) {
$this->Flash->success(__('The user has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The user could not be saved. Please, try again.'));
}
$this->set(compact('user'));
}
/**
* Edit method
*
* @param string|null $id User id.
* @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function edit($id = null)
{
$user = $this->Users->get($id, [
'contain' => [],
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$user = $this->Users->patchEntity($user, $this->request->getData());
if ($this->Users->save($user)) {
$this->Flash->success(__('The user has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The user could not be saved. Please, try again.'));
}
$this->set(compact('user'));
}
/**
* Delete method
*
* @param string|null $id User id.
* @return \Cake\Http\Response|null|void Redirects to index.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function delete($id = null)
{
$this->request->allowMethod(['post', 'delete']);
$user = $this->Users->get($id);
if ($this->Users->delete($user)) {
$this->Flash->success(__('The user has been deleted.'));
} else {
$this->Flash->error(__('The user could not be deleted. Please, try again.'));
}
return $this->redirect(['action' => 'index']);
}
}

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Model\Entity;
use Cake\ORM\Entity;
/**
* Address Entity
*
* @property int $id
* @property int $owner
* @property string $first
* @property string $last
* @property string $street
* @property string $zip
* @property string $city
* @property string $phone
*/
class Address extends Entity
{
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* @var array<string, bool>
*/
protected $_accessible = [
'owner' => true,
'first' => true,
'last' => true,
'street' => true,
'zip' => true,
'city' => true,
'phone' => true,
];
}

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Model\Entity;
use Cake\ORM\Entity;
/**
* User Entity
*
* @property int $id
* @property string $password
* @property string $nick
* @property string $first
* @property string $last
* @property bool $is_admin
*/
class User extends Entity
{
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* @var array<string, bool>
*/
protected $_accessible = [
'password' => true,
'nick' => true,
'first' => true,
'last' => true,
'is_admin' => true,
];
/**
* Fields that are excluded from JSON versions of the entity.
*
* @var array<string>
*/
protected $_hidden = [
'password',
];
}

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* Addresses Model
*
* @method \App\Model\Entity\Address newEmptyEntity()
* @method \App\Model\Entity\Address newEntity(array $data, array $options = [])
* @method \App\Model\Entity\Address[] newEntities(array $data, array $options = [])
* @method \App\Model\Entity\Address get($primaryKey, $options = [])
* @method \App\Model\Entity\Address findOrCreate($search, ?callable $callback = null, $options = [])
* @method \App\Model\Entity\Address patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
* @method \App\Model\Entity\Address[] patchEntities(iterable $entities, array $data, array $options = [])
* @method \App\Model\Entity\Address|false save(\Cake\Datasource\EntityInterface $entity, $options = [])
* @method \App\Model\Entity\Address saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = [])
* @method \App\Model\Entity\Address[]|\Cake\Datasource\ResultSetInterface|false saveMany(iterable $entities, $options = [])
* @method \App\Model\Entity\Address[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable $entities, $options = [])
* @method \App\Model\Entity\Address[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable $entities, $options = [])
* @method \App\Model\Entity\Address[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable $entities, $options = [])
*/
class AddressesTable extends Table
{
/**
* Initialize method
*
* @param array $config The configuration for the Table.
* @return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('addresses');
$this->setDisplayField('id');
$this->setPrimaryKey('id');
}
/**
* Default validation rules.
*
* @param \Cake\Validation\Validator $validator Validator instance.
* @return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator): Validator
{
$validator
->integer('owner')
->requirePresence('owner', 'create')
->notEmptyString('owner');
$validator
->scalar('first')
->maxLength('first', 80)
->requirePresence('first', 'create')
->notEmptyString('first');
$validator
->scalar('last')
->maxLength('last', 80)
->requirePresence('last', 'create')
->notEmptyString('last');
$validator
->scalar('street')
->maxLength('street', 80)
->requirePresence('street', 'create')
->notEmptyString('street');
$validator
->scalar('zip')
->maxLength('zip', 10)
->requirePresence('zip', 'create')
->notEmptyString('zip');
$validator
->scalar('city')
->maxLength('city', 80)
->requirePresence('city', 'create')
->notEmptyString('city');
$validator
->scalar('phone')
->maxLength('phone', 30)
->requirePresence('phone', 'create')
->notEmptyString('phone');
return $validator;
}
}

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* Users Model
*
* @method \App\Model\Entity\User newEmptyEntity()
* @method \App\Model\Entity\User newEntity(array $data, array $options = [])
* @method \App\Model\Entity\User[] newEntities(array $data, array $options = [])
* @method \App\Model\Entity\User get($primaryKey, $options = [])
* @method \App\Model\Entity\User findOrCreate($search, ?callable $callback = null, $options = [])
* @method \App\Model\Entity\User patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
* @method \App\Model\Entity\User[] patchEntities(iterable $entities, array $data, array $options = [])
* @method \App\Model\Entity\User|false save(\Cake\Datasource\EntityInterface $entity, $options = [])
* @method \App\Model\Entity\User saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = [])
* @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface|false saveMany(iterable $entities, $options = [])
* @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable $entities, $options = [])
* @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable $entities, $options = [])
* @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable $entities, $options = [])
*/
class UsersTable extends Table
{
/**
* Initialize method
*
* @param array $config The configuration for the Table.
* @return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('users');
$this->setDisplayField('id');
$this->setPrimaryKey('id');
}
/**
* Default validation rules.
*
* @param \Cake\Validation\Validator $validator Validator instance.
* @return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator): Validator
{
$validator
->scalar('password')
->maxLength('password', 256)
->requirePresence('password', 'create')
->notEmptyString('password');
$validator
->scalar('nick')
->maxLength('nick', 20)
->requirePresence('nick', 'create')
->notEmptyString('nick')
->add('nick', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']);
$validator
->scalar('first')
->maxLength('first', 40)
->requirePresence('first', 'create')
->notEmptyString('first');
$validator
->scalar('last')
->maxLength('last', 40)
->requirePresence('last', 'create')
->notEmptyString('last');
$validator
->boolean('is_admin')
->requirePresence('is_admin', 'create')
->notEmptyString('is_admin');
return $validator;
}
/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
* @return \Cake\ORM\RulesChecker
*/
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add($rules->isUnique(['nick']), ['errorField' => 'nick']);
return $rules;
}
}

@ -0,0 +1,33 @@
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\Address $address
*/
?>
<div class="row">
<aside class="column">
<div class="side-nav">
<h4 class="heading"><?= __('Actions') ?></h4>
<?= $this->Html->link(__('List Addresses'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
</div>
</aside>
<div class="column-responsive column-80">
<div class="addresses form content">
<?= $this->Form->create($address) ?>
<fieldset>
<legend><?= __('Add Address') ?></legend>
<?php
echo $this->Form->control('owner');
echo $this->Form->control('first');
echo $this->Form->control('last');
echo $this->Form->control('street');
echo $this->Form->control('zip');
echo $this->Form->control('city');
echo $this->Form->control('phone');
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>
</div>
</div>
</div>

@ -0,0 +1,38 @@
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\Address $address
*/
?>
<div class="row">
<aside class="column">
<div class="side-nav">
<h4 class="heading"><?= __('Actions') ?></h4>
<?= $this->Form->postLink(
__('Delete'),
['action' => 'delete', $address->id],
['confirm' => __('Are you sure you want to delete # {0}?', $address->id), 'class' => 'side-nav-item']
) ?>
<?= $this->Html->link(__('List Addresses'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
</div>
</aside>
<div class="column-responsive column-80">
<div class="addresses form content">
<?= $this->Form->create($address) ?>
<fieldset>
<legend><?= __('Edit Address') ?></legend>
<?php
echo $this->Form->control('owner');
echo $this->Form->control('first');
echo $this->Form->control('last');
echo $this->Form->control('street');
echo $this->Form->control('zip');
echo $this->Form->control('city');
echo $this->Form->control('phone');
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>
</div>
</div>
</div>

@ -0,0 +1,56 @@
<?php
/**
* @var \App\View\AppView $this
* @var iterable<\App\Model\Entity\Address> $addresses
*/
?>
<div class="addresses index content">
<?= $this->Html->link(__('New Address'), ['action' => 'add'], ['class' => 'button float-right']) ?>
<h3><?= __('Addresses') ?></h3>
<div class="table-responsive">
<table>
<thead>
<tr>
<th><?= $this->Paginator->sort('id') ?></th>
<th><?= $this->Paginator->sort('owner') ?></th>
<th><?= $this->Paginator->sort('first') ?></th>
<th><?= $this->Paginator->sort('last') ?></th>
<th><?= $this->Paginator->sort('street') ?></th>
<th><?= $this->Paginator->sort('zip') ?></th>
<th><?= $this->Paginator->sort('city') ?></th>
<th><?= $this->Paginator->sort('phone') ?></th>
<th class="actions"><?= __('Actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($addresses as $address): ?>
<tr>
<td><?= $this->Number->format($address->id) ?></td>
<td><?= $this->Number->format($address->owner) ?></td>
<td><?= h($address->first) ?></td>
<td><?= h($address->last) ?></td>
<td><?= h($address->street) ?></td>
<td><?= h($address->zip) ?></td>
<td><?= h($address->city) ?></td>
<td><?= h($address->phone) ?></td>
<td class="actions">
<?= $this->Html->link(__('View'), ['action' => 'view', $address->id]) ?>
<?= $this->Html->link(__('Edit'), ['action' => 'edit', $address->id]) ?>
<?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $address->id], ['confirm' => __('Are you sure you want to delete # {0}?', $address->id)]) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="paginator">
<ul class="pagination">
<?= $this->Paginator->first('<< ' . __('first')) ?>
<?= $this->Paginator->prev('< ' . __('previous')) ?>
<?= $this->Paginator->numbers() ?>
<?= $this->Paginator->next(__('next') . ' >') ?>
<?= $this->Paginator->last(__('last') . ' >>') ?>
</ul>
<p><?= $this->Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?></p>
</div>
</div>

@ -0,0 +1,56 @@
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\Address $address
*/
?>
<div class="row">
<aside class="column">
<div class="side-nav">
<h4 class="heading"><?= __('Actions') ?></h4>
<?= $this->Html->link(__('Edit Address'), ['action' => 'edit', $address->id], ['class' => 'side-nav-item']) ?>
<?= $this->Form->postLink(__('Delete Address'), ['action' => 'delete', $address->id], ['confirm' => __('Are you sure you want to delete # {0}?', $address->id), 'class' => 'side-nav-item']) ?>
<?= $this->Html->link(__('List Addresses'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
<?= $this->Html->link(__('New Address'), ['action' => 'add'], ['class' => 'side-nav-item']) ?>
</div>
</aside>
<div class="column-responsive column-80">
<div class="addresses view content">
<h3><?= h($address->id) ?></h3>
<table>
<tr>
<th><?= __('First') ?></th>
<td><?= h($address->first) ?></td>
</tr>
<tr>
<th><?= __('Last') ?></th>
<td><?= h($address->last) ?></td>
</tr>
<tr>
<th><?= __('Street') ?></th>
<td><?= h($address->street) ?></td>
</tr>
<tr>
<th><?= __('Zip') ?></th>
<td><?= h($address->zip) ?></td>
</tr>
<tr>
<th><?= __('City') ?></th>
<td><?= h($address->city) ?></td>
</tr>
<tr>
<th><?= __('Phone') ?></th>
<td><?= h($address->phone) ?></td>
</tr>
<tr>
<th><?= __('Id') ?></th>
<td><?= $this->Number->format($address->id) ?></td>
</tr>
<tr>
<th><?= __('Owner') ?></th>
<td><?= $this->Number->format($address->owner) ?></td>
</tr>
</table>
</div>
</div>
</div>

@ -0,0 +1,31 @@
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\User $user
*/
?>
<div class="row">
<aside class="column">
<div class="side-nav">
<h4 class="heading"><?= __('Actions') ?></h4>
<?= $this->Html->link(__('List Users'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
</div>
</aside>
<div class="column-responsive column-80">
<div class="users form content">
<?= $this->Form->create($user) ?>
<fieldset>
<legend><?= __('Add User') ?></legend>
<?php
echo $this->Form->control('password');
echo $this->Form->control('nick');
echo $this->Form->control('first');
echo $this->Form->control('last');
echo $this->Form->control('is_admin');
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>
</div>
</div>
</div>

@ -0,0 +1,36 @@
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\User $user
*/
?>
<div class="row">
<aside class="column">
<div class="side-nav">
<h4 class="heading"><?= __('Actions') ?></h4>
<?= $this->Form->postLink(
__('Delete'),
['action' => 'delete', $user->id],
['confirm' => __('Are you sure you want to delete # {0}?', $user->id), 'class' => 'side-nav-item']
) ?>
<?= $this->Html->link(__('List Users'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
</div>
</aside>
<div class="column-responsive column-80">
<div class="users form content">
<?= $this->Form->create($user) ?>
<fieldset>
<legend><?= __('Edit User') ?></legend>
<?php
echo $this->Form->control('password');
echo $this->Form->control('nick');
echo $this->Form->control('first');
echo $this->Form->control('last');
echo $this->Form->control('is_admin');
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>
</div>
</div>
</div>

@ -0,0 +1,50 @@
<?php
/**
* @var \App\View\AppView $this
* @var iterable<\App\Model\Entity\User> $users
*/
?>
<div class="users index content">
<?= $this->Html->link(__('New User'), ['action' => 'add'], ['class' => 'button float-right']) ?>
<h3><?= __('Users') ?></h3>
<div class="table-responsive">
<table>
<thead>
<tr>
<th><?= $this->Paginator->sort('id') ?></th>
<th><?= $this->Paginator->sort('nick') ?></th>
<th><?= $this->Paginator->sort('first') ?></th>
<th><?= $this->Paginator->sort('last') ?></th>
<th><?= $this->Paginator->sort('is_admin') ?></th>
<th class="actions"><?= __('Actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?= $this->Number->format($user->id) ?></td>
<td><?= h($user->nick) ?></td>
<td><?= h($user->first) ?></td>
<td><?= h($user->last) ?></td>
<td><?= h($user->is_admin) ?></td>
<td class="actions">
<?= $this->Html->link(__('View'), ['action' => 'view', $user->id]) ?>
<?= $this->Html->link(__('Edit'), ['action' => 'edit', $user->id]) ?>
<?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $user->id], ['confirm' => __('Are you sure you want to delete # {0}?', $user->id)]) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="paginator">
<ul class="pagination">
<?= $this->Paginator->first('<< ' . __('first')) ?>
<?= $this->Paginator->prev('< ' . __('previous')) ?>
<?= $this->Paginator->numbers() ?>
<?= $this->Paginator->next(__('next') . ' >') ?>
<?= $this->Paginator->last(__('last') . ' >>') ?>
</ul>
<p><?= $this->Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?></p>
</div>
</div>

@ -0,0 +1 @@
<?php

@ -0,0 +1,44 @@
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\User $user
*/
?>
<div class="row">
<aside class="column">
<div class="side-nav">
<h4 class="heading"><?= __('Actions') ?></h4>
<?= $this->Html->link(__('Edit User'), ['action' => 'edit', $user->id], ['class' => 'side-nav-item']) ?>
<?= $this->Form->postLink(__('Delete User'), ['action' => 'delete', $user->id], ['confirm' => __('Are you sure you want to delete # {0}?', $user->id), 'class' => 'side-nav-item']) ?>
<?= $this->Html->link(__('List Users'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
<?= $this->Html->link(__('New User'), ['action' => 'add'], ['class' => 'side-nav-item']) ?>
</div>
</aside>
<div class="column-responsive column-80">
<div class="users view content">
<h3><?= h($user->id) ?></h3>
<table>
<tr>
<th><?= __('Nick') ?></th>
<td><?= h($user->nick) ?></td>
</tr>
<tr>
<th><?= __('First') ?></th>
<td><?= h($user->first) ?></td>
</tr>
<tr>
<th><?= __('Last') ?></th>
<td><?= h($user->last) ?></td>
</tr>
<tr>
<th><?= __('Id') ?></th>
<td><?= $this->Number->format($user->id) ?></td>
</tr>
<tr>
<th><?= __('Is Admin') ?></th>
<td><?= $user->is_admin ? __('Yes') : __('No'); ?></td>
</tr>
</table>
</div>
</div>
</div>

@ -0,0 +1,11 @@
<?php
/**
* @var \App\View\AppView $this
* @var array $params
* @var string $message
*/
if (!isset($params['escape']) || $params['escape'] !== false) {
$message = h($message);
}
?>
<div class="message" onclick="this.classList.add('hidden');"><?= $message ?></div>

@ -0,0 +1,11 @@
<?php
/**
* @var \App\View\AppView $this
* @var array $params
* @var string $message
*/
if (!isset($params['escape']) || $params['escape'] !== false) {
$message = h($message);
}
?>
<div class="message warning" onclick="this.classList.add('hidden');"><?= $message ?></div>

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Test\Fixture;
use Cake\TestSuite\Fixture\TestFixture;
/**
* AddressesFixture
*/
class AddressesFixture extends TestFixture
{
/**
* Init method
*
* @return void
*/
public function init(): void
{
$this->records = [
[
'id' => 1,
'owner' => 1,
'first' => 'Lorem ipsum dolor sit amet',
'last' => 'Lorem ipsum dolor sit amet',
'street' => 'Lorem ipsum dolor sit amet',
'zip' => 'Lorem ip',
'city' => 'Lorem ipsum dolor sit amet',
'phone' => 'Lorem ipsum dolor sit amet',
],
];
parent::init();
}
}

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Test\Fixture;
use Cake\TestSuite\Fixture\TestFixture;
/**
* UsersFixture
*/
class UsersFixture extends TestFixture
{
/**
* Init method
*
* @return void
*/
public function init(): void
{
$this->records = [
[
'id' => 1,
'password' => 'Lorem ipsum dolor sit amet',
'nick' => 'Lorem ipsum dolor ',
'first' => 'Lorem ipsum dolor sit amet',
'last' => 'Lorem ipsum dolor sit amet',
'is_admin' => 1,
],
];
parent::init();
}
}

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Controller;
use App\Controller\AddressesController;
use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;
/**
* App\Controller\AddressesController Test Case
*
* @uses \App\Controller\AddressesController
*/
class AddressesControllerTest extends TestCase
{
use IntegrationTestTrait;
/**
* Fixtures
*
* @var array<string>
*/
protected $fixtures = [
'app.Addresses',
];
/**
* Test index method
*
* @return void
* @uses \App\Controller\AddressesController::index()
*/
public function testIndex(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test view method
*
* @return void
* @uses \App\Controller\AddressesController::view()
*/
public function testView(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test add method
*
* @return void
* @uses \App\Controller\AddressesController::add()
*/
public function testAdd(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test edit method
*
* @return void
* @uses \App\Controller\AddressesController::edit()
*/
public function testEdit(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test delete method
*
* @return void
* @uses \App\Controller\AddressesController::delete()
*/
public function testDelete(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
}

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Controller;
use App\Controller\UsersController;
use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;
/**
* App\Controller\UsersController Test Case
*
* @uses \App\Controller\UsersController
*/
class UsersControllerTest extends TestCase
{
use IntegrationTestTrait;
/**
* Fixtures
*
* @var array<string>
*/
protected $fixtures = [
'app.Users',
];
/**
* Test index method
*
* @return void
* @uses \App\Controller\UsersController::index()
*/
public function testIndex(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test view method
*
* @return void
* @uses \App\Controller\UsersController::view()
*/
public function testView(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test add method
*
* @return void
* @uses \App\Controller\UsersController::add()
*/
public function testAdd(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test edit method
*
* @return void
* @uses \App\Controller\UsersController::edit()
*/
public function testEdit(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test delete method
*
* @return void
* @uses \App\Controller\UsersController::delete()
*/
public function testDelete(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
}

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Model\Table;
use App\Model\Table\AddressesTable;
use Cake\TestSuite\TestCase;
/**
* App\Model\Table\AddressesTable Test Case
*/
class AddressesTableTest extends TestCase
{
/**
* Test subject
*
* @var \App\Model\Table\AddressesTable
*/
protected $Addresses;
/**
* Fixtures
*
* @var array<string>
*/
protected $fixtures = [
'app.Addresses',
];
/**
* setUp method
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$config = $this->getTableLocator()->exists('Addresses') ? [] : ['className' => AddressesTable::class];
$this->Addresses = $this->getTableLocator()->get('Addresses', $config);
}
/**
* tearDown method
*
* @return void
*/
protected function tearDown(): void
{
unset($this->Addresses);
parent::tearDown();
}
/**
* Test validationDefault method
*
* @return void
* @uses \App\Model\Table\AddressesTable::validationDefault()
*/
public function testValidationDefault(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
}

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Test\TestCase\Model\Table;
use App\Model\Table\UsersTable;
use Cake\TestSuite\TestCase;
/**
* App\Model\Table\UsersTable Test Case
*/
class UsersTableTest extends TestCase
{
/**
* Test subject
*
* @var \App\Model\Table\UsersTable
*/
protected $Users;
/**
* Fixtures
*
* @var array<string>
*/
protected $fixtures = [
'app.Users',
];
/**
* setUp method
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$config = $this->getTableLocator()->exists('Users') ? [] : ['className' => UsersTable::class];
$this->Users = $this->getTableLocator()->get('Users', $config);
}
/**
* tearDown method
*
* @return void
*/
protected function tearDown(): void
{
unset($this->Users);
parent::tearDown();
}
/**
* Test validationDefault method
*
* @return void
* @uses \App\Model\Table\UsersTable::validationDefault()
*/
public function testValidationDefault(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test buildRules method
*
* @return void
* @uses \App\Model\Table\UsersTable::buildRules()
*/
public function testBuildRules(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
}

4
CakePHP/tests/schema.sql Normal file

@ -0,0 +1,4 @@
-- Test database schema.
--
-- If you are not using CakePHP migrations you can put
-- your application's schema in this file and use it in tests.

8
CakePHP/webroot/css/normalize.min.css vendored Normal file

@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using clean-css v4.2.1.
* Original file: /npm/normalize.css@8.0.1/normalize.css
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}

@ -1,3 +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 than done in a few hours.
The address book itself was then done in a few hours.

88
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

@ -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\Repository\UserRepository;
use App\Service\Router;
use App\Service\Template;
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\AddressRepository;
use App\Entity\AddressBookEntry;
use App\Entity\User;
use App\Enums\StatusCode;
use App\Enums\UserAuth;
use App\Service\Router;
use App\Service\Template;
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,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,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 Mikro24\Service\DatabaseConnection;
use App\Entity\AddressBookEntry;
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,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')
}
})

@ -26,4 +26,102 @@ 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;
}

@ -1,13 +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\AddressBook;
use App\Controller\AddressBookAdminController;
use App\Controller\AddressBookController;
use App\Controller\SecurityController;
use App\Service\Container;
use App\Service\Router;
$container = new \App\Service\Container();
$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);
$addressBook = $container->get(AddressBook::class);
//$addressBook = new AddressBook();
$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();

@ -1,40 +0,0 @@
<?php
namespace App\Controller;
use App\Service\Template;
class AddressBook
{
public function __construct(private readonly Template $template)
{
}
public function main(): void
{
try {
$this->template->render(templateName: 'index.tpl');
} catch (\Exception $e) {
die($e->getMessage());
}
}
public function admin(string $command = '')
{
try {
$this->template->render(templateName: 'admin/index.tpl');
} catch (\Exception $e) {
die($e->getMessage());
}
}
public function login()
{
try {
$this->template->render(templateName: 'admin/index.tpl');
} catch (\Exception $e) {
die($e->getMessage());
}
}
}

@ -1,33 +0,0 @@
<?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\Template;
use App\Repository\UserRepository;
class AddressBookController
{
public function __construct(
private readonly Template $template,
private readonly User $user,
private readonly UserRepository $userRepository
)
{
}
public function main(): void
{
$this->template->render(templateName: 'index.html.php', vars: [
'user' => $this->user
]);
}
}

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

@ -1,63 +0,0 @@
<?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 $userid,
private string $first,
private string $last,
private string $nick,
)
{
// empty body
}
public function getUserid(): int
{
return $this->userid;
}
public function setUserid(int $userid): void
{
$this->userid = $userid;
}
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 getNick(): string
{
return $this->nick;
}
public function setNick(string $nick): void
{
$this->nick = $nick;
}
}

@ -14,16 +14,31 @@ use App\Enums\UserAuth;
class User
{
public function __construct(
private string $nick = '',
private string $password = '',
private string $first = '',
private string $last = '',
private int $id = 0,
private bool $isAdmin = false,
private UserAuth $userAuth = UserAuth::AUTH_ANONYMOUS
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
)
{
// empty body
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
@ -86,12 +101,12 @@ class User
$this->isAdmin = $isAdmin;
}
public function getAuth()
public function getAuth(): UserAuth
{
return UserAuth::AUTH_ANONYMOUS;
return $this->userAuth;
}
public function setAuth(UserAuth $userAuth)
public function setAuth(UserAuth $userAuth): void
{
$this->userAuth = $userAuth;
}

@ -7,15 +7,15 @@
*
*/
namespace App\Repository;
namespace Mikro24\Repositories;
use App\Service\DatabaseConnection;
use App\Entity\User;
use Mikro24\Service\DatabaseConnection;
use Mikro24\Models\User;
use PDO;
use PDOException;
/**
* Handles CRUD od User class.
* Handles CRUD of User class.
*/
class UserRepository
{
@ -24,25 +24,27 @@ class UserRepository
// empty body
}
/**
* @return array
*/
public function findAll(string $orderBy = 'nick'): array
{
$users = [];
$sql = "
SELECT id, nick, first, last, is_admin
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: $order);
$statement->bindParam(param: ':order', var: $orderBy);
$statement->execute();
while ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
$user = new User(nick: $result['nick'], first: $result['first'], last: $result['last'], id: $result['id']);
$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;
@ -55,7 +57,7 @@ class UserRepository
public function findByID(int $id): ?User
{
$sql = "
SELECT id, nick, first, last, is_admin
SELECT id, nick, password, first, last, is_admin
FROM " . DatabaseConnection::TABLE_USERS . "
WHERE id = :id";
@ -64,7 +66,13 @@ class UserRepository
$statement->bindParam(param: ':id', var: $id);
$statement->execute();
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
return new User(nick: $result['nick'], first: $result['first'], last: $result['last'], id: $result['id']);
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;
}
@ -75,8 +83,10 @@ class UserRepository
public function findByNick(string $nick): ?User
{
$nick = strtolower(string: $nick);
$sql = "
SELECT id, nick, first, last, is_admin
SELECT id, nick, password, first, last, is_admin
FROM " . DatabaseConnection::TABLE_USERS . "
WHERE nick = :nick";
@ -85,7 +95,13 @@ class UserRepository
$statement->bindParam(param: ':nick', var: $nick);
$statement->execute();
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
return new User(nick: $result['nick'], first: $result['first'], last: $result['last'], id: $result['id']);
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;
}
@ -96,69 +112,72 @@ class UserRepository
public function insert(User $user): bool|string
{
/*
$sql = "
INSERT INTO " . DatabaseConnection::TABLE_USERS . " (name, panel)
VALUES (:name, :panel)";
INSERT INTO " . DatabaseConnection::TABLE_USERS . " (nick, password, first, last, is_admin)
VALUES (:nick, :password, :first, :last, :is_admin)";
try {
$name = $domain->getName();
$panel = $domain->getPanel();
$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: ':name', var: $name);
$statement->bindParam(param: ':panel', var: $panel);
$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());
}
*/
return false;
}
public function update(User $user): bool|int
public function update(User $user): bool
{
$id = $user->getId();
$current = $this->findByID(id: $id);
$nick = $user->getNick();
$first = $user->getFirst();
$last = $user->getLast();
$isAdmin = $user->isAdmin() ? 1 : 0;
/*
if (empty($domain->getName())) {
$name = $current->getName();
if ($user->getPassword()) {
$password = $user->getPassword();
} else {
$name = $domain->getName();
}
if (empty($domain->getPanel())) {
$panel = $current->getPanel();
} else {
$panel = $domain->getPanel();
$current = $this->findByID(id: $id);
$password = $current->getPassword();
}
$sql = "
UPDATE " . DatabaseConnection::TABLE_USER . " SET
name = :name,
panel = :panel
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: 'name', var: $name);
$statement->bindParam(param: 'panel', var: $panel);
$statement->execute();
return $statement->rowCount();
$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;
}
*/
return false;
}
public function delete(User $user): int
public function delete(User $user): bool
{
$sql = "
DELETE FROM " . DatabaseConnection::TABLE_USERS . "
@ -168,9 +187,7 @@ class UserRepository
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$id = $user->getId();
$statement->bindParam(param: 'id', var: $id);
$statement->execute();
return $statement->rowCount();
return $statement->execute();
} catch (PDOException $e) {
exit($e->getMessage());
}

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

@ -7,9 +7,7 @@
*
*/
namespace App\Service;
use Exception;
namespace Mikro24\Services;
/**
*
@ -21,7 +19,7 @@ class Config
public function __construct()
{
// Check for either config.json.local or config.json.
$configFile = dirname(path: __DIR__, levels: 2) . "/config.json.local";
$configFile = dirname(path: __DIR__, levels: 2) . "/config.local.json";
if (!file_exists(filename: $configFile)) {
$configFile = dirname(path: __DIR__, levels: 2) . "/config.json";
}

@ -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 Mikro24\Services;
use Mikro24\Controllers\SecurityController;
use Mikro24\Models\User;
use Mikro24\Repositories\UserRepository;
use App\Controllers\AddressBookAdminController;
use App\Controllers\AddressBookController;
use App\Repositories\AddressRepository;
/*
* 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\Controllers\AddressBookController' => $this->addressBook,
'App\Controllers\AddressBookAdminController' => $this->addressBookAdmin,
'Mikro24\Controllers\SecurityController' => $this->securityController,
'Mikro24\Service\Router' => $this->router,
//default => throw new Exception(message: "Missing class definition: $class")
default => die("Missing class definition: $className")
};
}
}

@ -7,7 +7,7 @@
*
*/
namespace App\Service;
namespace Mikro24\Services;
use PDO;
@ -36,6 +36,7 @@ class DatabaseConnection
username: $dbUser,
password: $dbPassword
);
$this->dbConnection->setAttribute(attribute: PDO::ATTR_ERRMODE, value: PDO::ERRMODE_EXCEPTION);
}
public function getConnection(): PDO

@ -7,26 +7,26 @@
*
*/
namespace App\Service;
namespace Mikro24\Services;
use App\Entity\Route;
use Mikro24\Models\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 support GET as well.
* But if I reuse the code in my bindApi I'll maybe support GET as well.
*/
class Router
{
/*
* The easiest wy to differentiate between static and dynamic routes is using
* 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;
private array $staticRoutes = [];
private array $dynamicRoutes = [];
public function __construct(private readonly Template $template)
{
@ -35,7 +35,7 @@ class Router
/*
* This method takes a route like /admin/users/{user} and creates a regex to match on call
* More complex routes as /posts/{thread}/show/{page} are supported as well.
* More complex routes like /posts/{thread}/show/{page} are supported as well.
*/
function addRoute(string $name, string $route, Closure $callback): void
{
@ -44,13 +44,10 @@ class Router
$parameters = $matches[0];
// create regex for route:
$regex = preg_replace(pattern: '/(?<={).+?(?=})/', replacement: '(.*?)', subject: $route);
// code below is ugly, better match including the braces
$regex = str_replace(search: '{', replace: '', subject: $regex);
$regex = str_replace(search: '}', replace: '', subject: $regex);
$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) {
@ -61,38 +58,67 @@ class Router
}
/*
* Check if there is a known route and executes the callback.
* Checks if there is a known route and executes the callback.
*/
public function handleRouting(): void
{
$requestUri = $_SERVER['REQUEST_URI'];
/*
* Static routes have preference over dynamic ones, so
* 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)) {
$parameters = [];
foreach ($route->getParameters() as $id => $parameter) {
$parameters[$parameter] = $matches[$id + 1];
}
// PHP is mad about named parameters in call_user_func
// Uncaught Error: Unknown named parameter $args in
// 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");
}
}

@ -6,15 +6,13 @@
* file that was distributed with this source code.
*/
namespace App\Service;
namespace Mikro24\Services;
/*
* As I'm not allowed to use 3rd party code like Twig or Smarty I ended up
* using PHP as a templating engine.
*/
use JetBrains\PhpStorm\NoReturn;
class Template
{
/*
@ -28,8 +26,7 @@ class Template
/*
* Add variables to template and throw it out
*/
#[NoReturn]
public function render(string $templateName, array $vars = []): void
public function render(string $templateName, array $vars = []): never
{
// assign template vars
foreach ($vars as $name => $value) {
@ -44,4 +41,14 @@ class Template
}
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);
}
}

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

@ -1,3 +0,0 @@
<!-- mind the javascript -->
</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>

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

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

@ -1,6 +1,10 @@
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
<br>
<h1>Address Book - Admin</h1>
<a href="/admin/users">👱Users</a>
<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>

@ -7,7 +7,7 @@
<th>First</th>
<th>Last</th>
<th>Is Admin</th>
<th>&nbsp;</th>
<th colspan="2">&nbsp;</th>
</tr>
<?php foreach ($users as $userLine): ?>
<tr>
@ -22,11 +22,14 @@
<?php endif; ?>
</td>
<td>
<a href="/admin/users/<?= $userLine->getNick(); ?>">edit</a>
<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="/admin/users/add">Add User</a>
<a href="<?= $router->path('app_admin_users_add'); ?>">Add User</a>
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>

@ -2,22 +2,22 @@
<form method="POST">
<label for="nick">Username</label>
<input type="text" name="nick" id="nick" required>
<input type="text" name="nick" id="nick" maxlength="20" required>
<br>
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<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" required>
<input type="text" name="first" id="first" maxlength="40" required>
<br>
<label for="last">Last</label>
<input type="text" name="last" id="last" required>
<input type="text" name="last" id="last" maxlength="40" required>
<br>
<label for="is-admin">Is Admin</label>
<label for="is_admin">Is Admin</label>
<input type="checkbox" name="is_admin" id="is_admin">
<br>

@ -4,22 +4,22 @@
<input type="hidden" name="id" value="<?= $editUser->getId() ?>">
<label for="nick">Username</label>
<input type="text" name="nick" id="nick" value="<?= $editUser->getNick() ?>" required>
<input type="text" name="nick" id="nick" value="<?= $editUser->getNick() ?>" maxlength="20" required>
<br>
<label for="password">Password (leave empty to keep the current one)</label>
<input type="password" name="password" id="password">
<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() ?>" required>
<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() ?>" required>
<input type="text" name="last" id="last" value="<?= $editUser->getLast() ?>" maxlength="40" required>
<br>
<label for="is-admin">Is Admin</label>
<label for="is_admin">Is Admin</label>
<input type="checkbox" name="is_admin" id="is_admin" <?= $editUser->isAdmin()?'checked':'' ?>>
<br>

@ -1,6 +1,45 @@
<?php include '_header.html.php' ?>
<br>
Welcome to Address Book
<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' ?>

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

@ -5,12 +5,6 @@
<?php else: ?>
<br>
<?php if ($message): ?>
<div class="info">
<?= $message ?>
</div>
<?php endif; ?>
<form method="POST">
<label for="nick">Username</label>
<input type="text" name="nick" id="nick">

@ -1,5 +1,3 @@
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
<h2>404 Page not found</h2>
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>
The requested URL cannot be found on this server.