Compare commits
118 Commits
e9fd8e153d
...
mikro24
Author | SHA1 | Date | |
---|---|---|---|
b8e5730ec0 | |||
b6fd7876d3 | |||
1dc31bcb1b | |||
3ff406d053 | |||
6abe519a44 | |||
c6c88456cf | |||
5a805aba07 | |||
1541d05715 | |||
dc78e203ea | |||
17a90358a7 | |||
0266a89ae3 | |||
56a3e584e7 | |||
e2bf38299b | |||
24c8a3d9d7 | |||
dba37e57f9 | |||
c68cf1643e | |||
affe02ec04 | |||
b678720ebd | |||
14f9f247bc | |||
65a87acc48 | |||
e04cf94edd | |||
9ee8ae39df | |||
70aa4d1f06 | |||
2f99531780 | |||
60860a0bce | |||
bb3d0f6e1b | |||
7ee8e8860e | |||
32dcab7592 | |||
88cf11d45d | |||
6c203a0213 | |||
9867df0a04 | |||
0fba7bc0a0 | |||
f46e73c647 | |||
1a8b31b3a2 | |||
a832a3c60b | |||
904fd798f6 | |||
4ed3dcd603 | |||
8bc252ba4d | |||
78f25fdfd3 | |||
e2da6000b3 | |||
32133a0d05 | |||
572cc1eb89 | |||
48585f14ab | |||
47439fe358 | |||
8e0a2ff6e8 | |||
ce798e3b65 | |||
21fd674dd0 | |||
8864875699 | |||
fe0a3c212d | |||
d2f5427df1 | |||
5ed539a471 | |||
5bc0b7966b | |||
d0e1d2e87d | |||
1b794f775d | |||
744117d958 | |||
cce18f6516 | |||
a973a4362f | |||
3bf0c2b44f | |||
ec022d09e4 | |||
148eff1557 | |||
ab4a00d25c | |||
753b832cbc | |||
e5f4656d71 | |||
117903ac7c | |||
4665e1706f | |||
8189abfd29 | |||
72503d7fe2 | |||
3f4653b81f | |||
0ed667d594 | |||
fddc58fb14 | |||
ad63a40a0d | |||
e13bbee1e6 | |||
c6af0d1b1c | |||
fd368d895d | |||
e1d1ef5eeb | |||
cfc4e22497 | |||
66f02c8c6e | |||
10efd6d1a6 | |||
5e8c945170 | |||
54ecb6b15e | |||
7160fd0804 | |||
2b34a89068 | |||
ab7ac074ba | |||
6461046c31 | |||
d2cdcdded4 | |||
7e19efd420 | |||
ffe4d43600 | |||
c05aee49d5 | |||
10d56751e4 | |||
71f8e1623e | |||
2836e0d878 | |||
61aa012dd6 | |||
ed1e795941 | |||
7559651447 | |||
bf5425737b | |||
0a9a253b84 | |||
442cd25aa6 | |||
63d4f339e3 | |||
2cd75e4694 | |||
18a754add7 | |||
9b5f3a2847 | |||
a88e861792 | |||
6284918056 | |||
33618b3be0 | |||
b60d9481ab | |||
582698a8d0 | |||
06c9b9443d | |||
74381857f2 | |||
f652132911 | |||
b4ab876463 | |||
5447a7dbad | |||
a3b2bf27f1 | |||
4a85f45e3a | |||
9a485b5424 | |||
d19e7d5b25 | |||
cbbfe45c1b | |||
9cacf9ced9 | |||
8a7a4a2253 |
.gitignore.gitinoreREADME.mdaddressbook.sql
CakePHP
.github
bin
composer.lockphpcs.xmlphpstan.neonsrc
Controller
Model
templates
Addresses
App
Pages
Users
element
flash
tests
webroot
app
Controllers
Models
Repositories
public
src
Controller
Controllers
Entity
Enums
Models
Repositories
Repository
Service
Services
templates
1
.gitignore
vendored
Normal file
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
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.
|
14
CakePHP/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
14
CakePHP/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -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
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
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
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'
|
47
CakePHP/bin/bash_completion.sh
Normal file
47
CakePHP/bin/bash_completion.sh
Normal file
@ -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
5790
CakePHP/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
CakePHP/phpcs.xml
Normal file
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
8
CakePHP/phpstan.neon
Normal file
@ -0,0 +1,8 @@
|
||||
parameters:
|
||||
level: 8
|
||||
checkMissingIterableValueType: false
|
||||
treatPhpDocTypesAsCertain: false
|
||||
paths:
|
||||
- src/
|
||||
excludePaths:
|
||||
- src/Console/Installer.php
|
105
CakePHP/src/Controller/AddressesController.php
Normal file
105
CakePHP/src/Controller/AddressesController.php
Normal file
@ -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']);
|
||||
}
|
||||
}
|
105
CakePHP/src/Controller/UsersController.php
Normal file
105
CakePHP/src/Controller/UsersController.php
Normal file
@ -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']);
|
||||
}
|
||||
}
|
40
CakePHP/src/Model/Entity/Address.php
Normal file
40
CakePHP/src/Model/Entity/Address.php
Normal file
@ -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,
|
||||
];
|
||||
}
|
45
CakePHP/src/Model/Entity/User.php
Normal file
45
CakePHP/src/Model/Entity/User.php
Normal file
@ -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',
|
||||
];
|
||||
}
|
96
CakePHP/src/Model/Table/AddressesTable.php
Normal file
96
CakePHP/src/Model/Table/AddressesTable.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
99
CakePHP/src/Model/Table/UsersTable.php
Normal file
99
CakePHP/src/Model/Table/UsersTable.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
33
CakePHP/templates/Addresses/add.php
Normal file
33
CakePHP/templates/Addresses/add.php
Normal file
@ -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>
|
38
CakePHP/templates/Addresses/edit.php
Normal file
38
CakePHP/templates/Addresses/edit.php
Normal file
@ -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>
|
56
CakePHP/templates/Addresses/index.php
Normal file
56
CakePHP/templates/Addresses/index.php
Normal file
@ -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>
|
56
CakePHP/templates/Addresses/view.php
Normal file
56
CakePHP/templates/Addresses/view.php
Normal file
@ -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
CakePHP/templates/App/index.php
Normal file
0
CakePHP/templates/App/index.php
Normal file
0
CakePHP/templates/Pages/main.php
Normal file
0
CakePHP/templates/Pages/main.php
Normal file
31
CakePHP/templates/Users/add.php
Normal file
31
CakePHP/templates/Users/add.php
Normal file
@ -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>
|
36
CakePHP/templates/Users/edit.php
Normal file
36
CakePHP/templates/Users/edit.php
Normal file
@ -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>
|
50
CakePHP/templates/Users/index.php
Normal file
50
CakePHP/templates/Users/index.php
Normal file
@ -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>
|
1
CakePHP/templates/Users/login.php
Normal file
1
CakePHP/templates/Users/login.php
Normal file
@ -0,0 +1 @@
|
||||
<?php
|
44
CakePHP/templates/Users/view.php
Normal file
44
CakePHP/templates/Users/view.php
Normal file
@ -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>
|
11
CakePHP/templates/element/flash/info.php
Normal file
11
CakePHP/templates/element/flash/info.php
Normal file
@ -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>
|
11
CakePHP/templates/element/flash/warning.php
Normal file
11
CakePHP/templates/element/flash/warning.php
Normal file
@ -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>
|
34
CakePHP/tests/Fixture/AddressesFixture.php
Normal file
34
CakePHP/tests/Fixture/AddressesFixture.php
Normal file
@ -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();
|
||||
}
|
||||
}
|
32
CakePHP/tests/Fixture/UsersFixture.php
Normal file
32
CakePHP/tests/Fixture/UsersFixture.php
Normal file
@ -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.');
|
||||
}
|
||||
}
|
82
CakePHP/tests/TestCase/Controller/UsersControllerTest.php
Normal file
82
CakePHP/tests/TestCase/Controller/UsersControllerTest.php
Normal file
@ -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.');
|
||||
}
|
||||
}
|
64
CakePHP/tests/TestCase/Model/Table/AddressesTableTest.php
Normal file
64
CakePHP/tests/TestCase/Model/Table/AddressesTableTest.php
Normal file
@ -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.');
|
||||
}
|
||||
}
|
75
CakePHP/tests/TestCase/Model/Table/UsersTableTest.php
Normal file
75
CakePHP/tests/TestCase/Model/Table/UsersTableTest.php
Normal file
@ -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
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
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,2 +1,3 @@
|
||||
# addressbook
|
||||
As I was not allowed to use any framework, respectively no foreign code, most of the time was spent, well creating some kind of framework myself. :-)
|
||||
|
||||
The address book itself was then done in a few hours.
|
||||
|
88
addressbook.sql
Normal file
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
|
161
app/Controllers/AddressBookAdminController.php
Normal file
161
app/Controllers/AddressBookAdminController.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
133
app/Controllers/AddressBookController.php
Normal file
133
app/Controllers/AddressBookController.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
107
app/Models/AddressBookEntry.php
Normal file
107
app/Models/AddressBookEntry.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
class AddressBookEntry
|
||||
{
|
||||
public function __construct(
|
||||
private int $owner,
|
||||
private string $first,
|
||||
private string $last,
|
||||
private string $street,
|
||||
private string $zip,
|
||||
private string $city,
|
||||
private string $phone,
|
||||
private int $id = 0,
|
||||
)
|
||||
{
|
||||
// empty body
|
||||
}
|
||||
|
||||
public function getOwner(): int
|
||||
{
|
||||
return $this->owner;
|
||||
}
|
||||
|
||||
public function setOwner(int $owner): void
|
||||
{
|
||||
$this->owner = $owner;
|
||||
}
|
||||
|
||||
public function getStreet(): string
|
||||
{
|
||||
return $this->street;
|
||||
}
|
||||
|
||||
public function setStreet(string $street): void
|
||||
{
|
||||
$this->street = $street;
|
||||
}
|
||||
|
||||
public function getZip(): string
|
||||
{
|
||||
return $this->zip;
|
||||
}
|
||||
|
||||
public function setZip(string $zip): void
|
||||
{
|
||||
$this->zip = $zip;
|
||||
}
|
||||
|
||||
public function getCity(): string
|
||||
{
|
||||
return $this->city;
|
||||
}
|
||||
|
||||
public function setCity(string $city): void
|
||||
{
|
||||
$this->city = $city;
|
||||
}
|
||||
|
||||
public function getPhone(): string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
public function setPhone(string $phone): void
|
||||
{
|
||||
$this->phone = $phone;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(int $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getFirst(): string
|
||||
{
|
||||
return $this->first;
|
||||
}
|
||||
|
||||
public function setFirst(string $first): void
|
||||
{
|
||||
$this->first = $first;
|
||||
}
|
||||
|
||||
public function getLast(): string
|
||||
{
|
||||
return $this->last;
|
||||
}
|
||||
|
||||
public function setLast(string $last): void
|
||||
{
|
||||
$this->last = $last;
|
||||
}
|
||||
}
|
174
app/Repositories/AddressRepository.php
Normal file
174
app/Repositories/AddressRepository.php
Normal file
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
206
public/assets/js/functions.js
Normal file
206
public/assets/js/functions.js
Normal file
@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
function addAddress(url) {
|
||||
location.href = url
|
||||
}
|
||||
|
||||
function editAddress(id) {
|
||||
if (document.getElementById('edit_button_' + id).value === 'Save') {
|
||||
// save
|
||||
const url = "/address/update";
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
id: id,
|
||||
owner: document.getElementById('owner_' + id).value,
|
||||
first: document.getElementById('first_' + id).value,
|
||||
last: document.getElementById('last_' + id).value,
|
||||
street: document.getElementById('street_' + id).value,
|
||||
zip: document.getElementById('zip_' + id).value,
|
||||
city: document.getElementById('city_' + id).value,
|
||||
phone: document.getElementById('phone_' + id).value,
|
||||
})
|
||||
})
|
||||
.then(
|
||||
response => response.text()
|
||||
).then(
|
||||
json => {
|
||||
let jsonObject = JSON.parse(json)
|
||||
if (jsonObject.status === 200) {
|
||||
setInfo('Data successfully saved.')
|
||||
} else {
|
||||
setError(jsonObject.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
document.getElementById('first_' + id).disabled = true
|
||||
document.getElementById('last_' + id).disabled = true
|
||||
document.getElementById('street_' + id).disabled = true
|
||||
document.getElementById('zip_' + id).disabled = true
|
||||
document.getElementById('city_' + id).disabled = true
|
||||
document.getElementById('phone_' + id).disabled = true
|
||||
|
||||
document.getElementById('edit_button_' + id).value = 'Edit'
|
||||
} else {
|
||||
//switch to edit
|
||||
document.getElementById('first_' + id).disabled = false
|
||||
document.getElementById('last_' + id).disabled = false
|
||||
document.getElementById('street_' + id).disabled = false
|
||||
document.getElementById('zip_' + id).disabled = false
|
||||
document.getElementById('city_' + id).disabled = false
|
||||
document.getElementById('phone_' + id).disabled = false
|
||||
|
||||
document.getElementById('edit_button_' + id).value = 'Save'
|
||||
}
|
||||
}
|
||||
|
||||
function deleteAddress(id) {
|
||||
if (confirm('Are you sure?')) {
|
||||
const url = "/address/delete";
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
id: id
|
||||
})
|
||||
})
|
||||
.then(
|
||||
response => response.text()
|
||||
).then(
|
||||
json => {
|
||||
let jsonObject = JSON.parse(json)
|
||||
if (jsonObject.status === 200) {
|
||||
setInfo('Data successfully saved.')
|
||||
} else {
|
||||
setError(jsonObject.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
let row = document.getElementById('row_' + id)
|
||||
row.parentNode.removeChild(row)
|
||||
}
|
||||
}
|
||||
|
||||
function upCase(text) {
|
||||
return text[0].toUpperCase() + text.substring(1);
|
||||
}
|
||||
|
||||
function sortBy(column) {
|
||||
// clear titles
|
||||
const titles = ['first', 'last', 'street', 'zip', 'city', 'phone']
|
||||
titles.forEach((title) =>
|
||||
document.getElementById(title).innerHTML = upCase(title)
|
||||
)
|
||||
|
||||
console.log("col", column)
|
||||
console.log("curcol", currentColumn)
|
||||
|
||||
if (currentColumn === column) {
|
||||
console.log("in switch")
|
||||
// switch direction on every call on same column
|
||||
if (currentSortOrder === 'asc') {
|
||||
currentSortOrder = 'desc'
|
||||
} else {
|
||||
currentSortOrder = 'asc'
|
||||
}
|
||||
console.log("col", column)
|
||||
} else {
|
||||
currentColumn = column
|
||||
}
|
||||
|
||||
let currentTitleElement = document.getElementById(column)
|
||||
let currentTitle = currentTitleElement.innerHTML
|
||||
let newTitle
|
||||
|
||||
if (currentSortOrder === 'asc') {
|
||||
newTitle = currentTitle[0] + currentTitle.substring(1) + ' ⬇'
|
||||
} else {
|
||||
newTitle = currentTitle[0] + currentTitle.substring(1) + ' ⬆'
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
65
src/Controllers/SecurityController.php
Normal file
65
src/Controllers/SecurityController.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace App\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,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
class AddressBookEntry
|
||||
{
|
||||
public function __construct(
|
||||
private int $userid,
|
||||
private string $first,
|
||||
private string $last,
|
||||
private string $nick,
|
||||
)
|
||||
{
|
||||
// empty body
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
class User
|
||||
{
|
||||
public function __construct(
|
||||
private string $nick,
|
||||
private string $password,
|
||||
private string $first = '',
|
||||
private string $last = '',
|
||||
private int $id = 0,
|
||||
private bool $isAdmin = false
|
||||
)
|
||||
{
|
||||
// empty body
|
||||
}
|
||||
}
|
17
src/Enums/UserAuth.php
Normal file
17
src/Enums/UserAuth.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum UserAuth
|
||||
{
|
||||
case AUTH_ANONYMOUS;
|
||||
case AUTH_USER;
|
||||
case AUTH_ADMIN;
|
||||
}
|
@ -1,4 +1,11 @@
|
||||
<?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;
|
||||
|
||||
@ -17,81 +24,51 @@ class Route
|
||||
// empty body
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*/
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRoute(): string
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
*/
|
||||
public function setRoute(string $route): void
|
||||
{
|
||||
$this->route = $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRegEx(): string
|
||||
{
|
||||
return $this->regEx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $regEx
|
||||
*/
|
||||
public function setRegEx(string $regEx): void
|
||||
{
|
||||
$this->regEx = $regEx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $parameters
|
||||
*/
|
||||
public function setParameters(array $parameters): void
|
||||
{
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Closure
|
||||
*/
|
||||
public function getCallback(): Closure
|
||||
{
|
||||
return $this->callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure $callback
|
||||
*/
|
||||
public function setCallback(Closure $callback): void
|
||||
{
|
||||
$this->callback = $callback;
|
114
src/Models/User.php
Normal file
114
src/Models/User.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Enums\UserAuth;
|
||||
|
||||
class User
|
||||
{
|
||||
public function __construct(
|
||||
private string $nick = '',
|
||||
private string $password = '',
|
||||
private readonly string $newPassword = '',
|
||||
private string $first = '',
|
||||
private string $last = '',
|
||||
private int $id = 0,
|
||||
private bool $isAdmin = false,
|
||||
private UserAuth $userAuth = UserAuth::AUTH_ANONYMOUS
|
||||
)
|
||||
{
|
||||
if (!empty($this->newPassword)) {
|
||||
echo "password";
|
||||
$this->password = password_hash(password: $this->newPassword, algo: PASSWORD_ARGON2I);
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
// ANONYMOUS has id 0
|
||||
if ($this->id != 0) {
|
||||
if ($this->isAdmin) {
|
||||
$this->userAuth = UserAuth::AUTH_ADMIN;
|
||||
} else {
|
||||
$this->userAuth = UserAuth::AUTH_USER;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getNick(): string
|
||||
{
|
||||
return $this->nick;
|
||||
}
|
||||
|
||||
public function setNick(string $nick): void
|
||||
{
|
||||
$this->nick = $nick;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): void
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
public function getFirst(): string
|
||||
{
|
||||
return $this->first;
|
||||
}
|
||||
|
||||
public function setFirst(string $first): void
|
||||
{
|
||||
$this->first = $first;
|
||||
}
|
||||
|
||||
public function getLast(): string
|
||||
{
|
||||
return $this->last;
|
||||
}
|
||||
|
||||
public function setLast(string $last): void
|
||||
{
|
||||
$this->last = $last;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(int $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->isAdmin;
|
||||
}
|
||||
|
||||
public function setIsAdmin(bool $isAdmin): void
|
||||
{
|
||||
$this->isAdmin = $isAdmin;
|
||||
}
|
||||
|
||||
public function getAuth(): UserAuth
|
||||
{
|
||||
return $this->userAuth;
|
||||
}
|
||||
|
||||
public function setAuth(UserAuth $userAuth): void
|
||||
{
|
||||
$this->userAuth = $userAuth;
|
||||
}
|
||||
|
||||
}
|
196
src/Repositories/UserRepository.php
Normal file
196
src/Repositories/UserRepository.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Mikro24\Repositories;
|
||||
|
||||
use Mikro24\Service\DatabaseConnection;
|
||||
use Mikro24\Models\User;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
|
||||
/**
|
||||
* Handles CRUD of User class.
|
||||
*/
|
||||
class UserRepository
|
||||
{
|
||||
public function __construct(private readonly DatabaseConnection $databaseConnection)
|
||||
{
|
||||
// empty body
|
||||
}
|
||||
|
||||
public function findAll(string $orderBy = 'nick'): array
|
||||
{
|
||||
$users = [];
|
||||
$sql = "
|
||||
SELECT id, nick, password, first, last, is_admin
|
||||
FROM " . DatabaseConnection::TABLE_USERS . "
|
||||
ORDER BY :order";
|
||||
|
||||
try {
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$statement->bindParam(param: ':order', var: $orderBy);
|
||||
|
||||
$statement->execute();
|
||||
while ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
|
||||
$user = new User(
|
||||
nick: htmlspecialchars(string: $result['nick']),
|
||||
password: $result['password'],
|
||||
first: htmlspecialchars(string: $result['first']),
|
||||
last: htmlspecialchars(string: $result['last']),
|
||||
id: $result['id'],
|
||||
isAdmin: $result['is_admin']);
|
||||
$users[] = $user;
|
||||
}
|
||||
return $users;
|
||||
} catch (PDOException $e) {
|
||||
exit($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function findByID(int $id): ?User
|
||||
{
|
||||
$sql = "
|
||||
SELECT id, nick, password, first, last, is_admin
|
||||
FROM " . DatabaseConnection::TABLE_USERS . "
|
||||
WHERE id = :id";
|
||||
|
||||
try {
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$statement->bindParam(param: ':id', var: $id);
|
||||
$statement->execute();
|
||||
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
|
||||
return new User(
|
||||
nick: htmlspecialchars(string: $result['nick']),
|
||||
password: $result['password'],
|
||||
first: htmlspecialchars(string: $result['first']),
|
||||
last: htmlspecialchars(string: $result['last']),
|
||||
id: $result['id'],
|
||||
isAdmin: $result['is_admin']);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
exit($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function findByNick(string $nick): ?User
|
||||
{
|
||||
$nick = strtolower(string: $nick);
|
||||
|
||||
$sql = "
|
||||
SELECT id, nick, password, first, last, is_admin
|
||||
FROM " . DatabaseConnection::TABLE_USERS . "
|
||||
WHERE nick = :nick";
|
||||
|
||||
try {
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$statement->bindParam(param: ':nick', var: $nick);
|
||||
$statement->execute();
|
||||
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
|
||||
return new User(
|
||||
nick: htmlspecialchars(string: $result['nick']),
|
||||
password: $result['password'],
|
||||
first: htmlspecialchars(string: $result['first']),
|
||||
last: htmlspecialchars(string: $result['last']),
|
||||
id: $result['id'],
|
||||
isAdmin: $result['is_admin']);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
exit($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function insert(User $user): bool|string
|
||||
{
|
||||
$sql = "
|
||||
INSERT INTO " . DatabaseConnection::TABLE_USERS . " (nick, password, first, last, is_admin)
|
||||
VALUES (:nick, :password, :first, :last, :is_admin)";
|
||||
|
||||
try {
|
||||
$nick = $user->getNick();
|
||||
$password = $user->getPassword();
|
||||
$first = $user->getFirst();
|
||||
$last = $user->getLast();
|
||||
$isAdmin = $user->isAdmin() ? 1 : 0;
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$statement->bindParam(param: ':nick', var: $nick);
|
||||
$statement->bindParam(param: ':password', var: $password);
|
||||
$statement->bindParam(param: ':first', var: $first);
|
||||
$statement->bindParam(param: ':last', var: $last);
|
||||
$statement->bindParam(param: ':is_admin', var: $isAdmin);
|
||||
$statement->execute();
|
||||
|
||||
return $this->databaseConnection->getConnection()->lastInsertId();
|
||||
} catch (PDOException $e) {
|
||||
exit($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function update(User $user): bool
|
||||
{
|
||||
$id = $user->getId();
|
||||
$nick = $user->getNick();
|
||||
$first = $user->getFirst();
|
||||
$last = $user->getLast();
|
||||
$isAdmin = $user->isAdmin() ? 1 : 0;
|
||||
|
||||
if ($user->getPassword()) {
|
||||
$password = $user->getPassword();
|
||||
} else {
|
||||
$current = $this->findByID(id: $id);
|
||||
$password = $current->getPassword();
|
||||
}
|
||||
|
||||
$sql = "
|
||||
UPDATE " . DatabaseConnection::TABLE_USERS . " SET
|
||||
nick = :nick,
|
||||
password = :password,
|
||||
first = :first,
|
||||
last = :last,
|
||||
is_admin = :is_admin
|
||||
WHERE id = :id";
|
||||
|
||||
try {
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$statement->bindParam(param: 'id', var: $id);
|
||||
$statement->bindParam(param: 'nick', var: $nick);
|
||||
$statement->bindParam(param: 'password', var: $password);
|
||||
$statement->bindParam(param: 'first', var: $first);
|
||||
$statement->bindParam(param: 'last', var: $last);
|
||||
$statement->bindParam(param: 'is_admin', var: $isAdmin);
|
||||
return $statement->execute();
|
||||
} catch (PDOException $e) {
|
||||
echo $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function delete(User $user): bool
|
||||
{
|
||||
$sql = "
|
||||
DELETE FROM " . DatabaseConnection::TABLE_USERS . "
|
||||
WHERE id = :id";
|
||||
|
||||
try {
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$id = $user->getId();
|
||||
$statement->bindParam(param: 'id', var: $id);
|
||||
return $statement->execute();
|
||||
} catch (PDOException $e) {
|
||||
exit($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,255 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Service\Config;
|
||||
use App\Service\DatabaseConnection;
|
||||
use App\Entity\User;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class DomainRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DatabaseConnection $databaseConnection,
|
||||
private readonly Config $configController)
|
||||
{
|
||||
// empty body
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
$users = [];
|
||||
$sql = "
|
||||
SELECT id, nick, first, last, is_admin
|
||||
FROM " . DatabaseConnection::TABLE_USERS . "
|
||||
ORDER BY name";
|
||||
|
||||
try {
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$statement->execute();
|
||||
while ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
|
||||
$domain = new Domain(name: $result['name'], panel: $result['panel'], id: $result['id']);
|
||||
$domains[] = $domain;
|
||||
}
|
||||
return $domains;
|
||||
} catch (PDOException $e) {
|
||||
exit($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
*
|
||||
* @return bool|\App\Entity\Domain
|
||||
*/
|
||||
public function findByID(int $id): bool|Domain
|
||||
{
|
||||
$this->logger->debug(message: "findById($id)");
|
||||
|
||||
$sql = "
|
||||
SELECT id, name, panel
|
||||
FROM . " . DatabaseConnection::TABLE_DOMAINS . "
|
||||
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 Domain(name: $result['name'], panel: $result['panel'], id: $result['id']);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
exit($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param String $name
|
||||
*
|
||||
* @return \App\Entity\Domain|bool
|
||||
*/
|
||||
public function findByName(string $name): Domain|bool
|
||||
{
|
||||
$this->logger->debug(message: "findByName($name)");
|
||||
|
||||
$sql = "
|
||||
SELECT id, name, panel
|
||||
FROM " . DatabaseConnection::TABLE_DOMAINS . "
|
||||
WHERE name = :name";
|
||||
|
||||
try {
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$statement->bindParam(param: ':name', var: $name);
|
||||
$statement->execute();
|
||||
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
|
||||
return new Domain(name: $result['name'], panel: $result['panel'], id: $result['id']);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
exit($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $host
|
||||
*
|
||||
* @return \App\Entity\Domain|bool
|
||||
*/
|
||||
public function findByHost(string $host): Domain|bool
|
||||
{
|
||||
$this->logger->debug(message: "findByHost($host)");
|
||||
|
||||
$host = strtolower(string: trim(string: $host));
|
||||
$count = substr_count(haystack: $host, needle: '.');
|
||||
if ($count == 2) {
|
||||
if (strlen(string: explode(separator: '.', string: $host)[1]) > 3) {
|
||||
$host = explode(separator: '.', string: $host, limit: 2)[1];
|
||||
}
|
||||
} elseif ($count > 2) {
|
||||
$host = $this->findByHost(host: explode(separator: '.', string: $host, limit: 2)[1]);
|
||||
}
|
||||
|
||||
if ($domain = $this->findByName(name: $host)) {
|
||||
return $domain;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param \App\Entity\Domain $domain
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function insert(Domain $domain): bool|string
|
||||
{
|
||||
$domainName = $domain->getName();
|
||||
$this->logger->info(message: "insert($domainName)");
|
||||
|
||||
$sql = "
|
||||
INSERT INTO " . DatabaseConnection::TABLE_DOMAINS . " (name, panel)
|
||||
VALUES (:name, :panel)";
|
||||
|
||||
try {
|
||||
$name = $domain->getName();
|
||||
$panel = $domain->getPanel();
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$statement->bindParam(param: ':name', var: $name);
|
||||
$statement->bindParam(param: ':panel', var: $panel);
|
||||
$statement->execute();
|
||||
|
||||
return $this->databaseConnection->getConnection()->lastInsertId();
|
||||
} catch (PDOException $e) {
|
||||
exit($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param \App\Entity\Domain $domain
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
public function update(Domain $domain): bool|int
|
||||
{
|
||||
$domainName = $domain->getName();
|
||||
$this->logger->debug(message: "update($domainName)");
|
||||
|
||||
$id = $domain->getId();
|
||||
$current = $this->findByID(id: $id);
|
||||
|
||||
if (empty($domain->getName())) {
|
||||
$name = $current->getName();
|
||||
} else {
|
||||
$name = $domain->getName();
|
||||
}
|
||||
if (empty($domain->getPanel())) {
|
||||
$panel = $current->getPanel();
|
||||
} else {
|
||||
$panel = $domain->getPanel();
|
||||
}
|
||||
|
||||
$sql = "
|
||||
UPDATE " . DatabaseConnection::TABLE_DOMAINS . " SET
|
||||
name = :name,
|
||||
panel = :panel
|
||||
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();
|
||||
} catch (PDOException $e) {
|
||||
echo $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param \App\Entity\Domain $domain
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function delete(Domain $domain): int
|
||||
{
|
||||
$domainName = $domain->getName();
|
||||
$this->logger->debug(message: "delete($domainName)");
|
||||
|
||||
$sql = "
|
||||
DELETE FROM " . DatabaseConnection::TABLE_DOMAINS . "
|
||||
WHERE id = :id";
|
||||
|
||||
try {
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$id = $domain->getId();
|
||||
$statement->bindParam(param: 'id', var: $id);
|
||||
$statement->execute();
|
||||
|
||||
return $statement->rowCount();
|
||||
} catch (PDOException $e) {
|
||||
exit($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param String $field
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLongestEntry(string $field): int
|
||||
{
|
||||
$sql = "
|
||||
SELECT MAX(LENGTH(" . $field . ")) as length FROM " . DatabaseConnection::TABLE_DOMAINS;
|
||||
|
||||
try {
|
||||
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
|
||||
$statement->execute();
|
||||
$result = $statement->fetch();
|
||||
return $result['length'];
|
||||
} 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")
|
||||
};
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class DatabaseConnection
|
||||
{
|
||||
private PDO $dbConnection;
|
||||
|
||||
const TABLE_PREFIX = '';
|
||||
const TABLE_USERS = self::TABLE_PREFIX . "users";
|
||||
const TABLE_ADDRESSES = self::TABLE_PREFIX . "addresses";
|
||||
|
||||
public function __construct(private readonly Config $configController)
|
||||
{
|
||||
$dbHost = $this->configController->getConfig(configKey: 'dbHost');
|
||||
$dbPort = $this->configController->getConfig(configKey: 'dbPort');
|
||||
$dbDatabase = $this->configController->getConfig(configKey: 'dbDatabase');
|
||||
$dbUser = $this->configController->getConfig(configKey: 'dbUser');
|
||||
$dbPassword = $this->configController->getConfig(configKey: 'dbPassword');
|
||||
|
||||
$this->dbConnection = new PDO(
|
||||
dsn: "mysql:host=$dbHost;port=$dbPort;charset=utf8mb4;dbname=$dbDatabase",
|
||||
username: $dbUser,
|
||||
password: $dbPassword
|
||||
);
|
||||
}
|
||||
|
||||
public function getConnection(): PDO
|
||||
{
|
||||
return $this->dbConnection;
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
|
||||
use App\Entity\Route;
|
||||
use Closure;
|
||||
|
||||
/*
|
||||
* A small router implementation for the address book demo.
|
||||
* Currently it doesn't handle GET requests, as not needed.
|
||||
* But if I reuse the code in my bind Api I'll enable GET as well.
|
||||
*/
|
||||
class Router
|
||||
{
|
||||
private array $routes;
|
||||
|
||||
/*
|
||||
* This method takes a route like /admin/users/{user} and creates a regex to match on call
|
||||
*/
|
||||
function addRoute(string $name, string $route, Closure $callback): void
|
||||
{
|
||||
// check for parameters
|
||||
preg_match_all(pattern: "/(?<={).+?(?=})/", subject: $route, matches: $matches);
|
||||
$parameters = $matches[0];
|
||||
|
||||
// create regex for route:
|
||||
$regex = preg_replace(pattern: '/(?<={).+?(?=})/', replacement: '(.*?)', 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 = '/^' . str_replace(search: "/", replace: '\\/', subject: $regex) . '$/i';
|
||||
$route = new Route(name: $name, route: $route, regEx: $regex, parameters: $parameters, callback: $callback);
|
||||
|
||||
$this->routes[] = $route;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check is there is a known route and executed the callback.
|
||||
* Currently no 404 handling.
|
||||
*/
|
||||
public function handleRouting(): void
|
||||
{
|
||||
$requestUri = $_SERVER['REQUEST_URI'];
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
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 …
|
||||
// But PHPStorm seems happy without them. So what?
|
||||
call_user_func($route->getCallback(), $parameters);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Throw a 404 result later …
|
||||
die("Invalid route: $requestUri");
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Exception;
|
||||
|
||||
class Template
|
||||
{
|
||||
public function __construct(private readonly string $templateDir)
|
||||
{
|
||||
// empty body
|
||||
}
|
||||
|
||||
public function render(string $templateName): void
|
||||
{
|
||||
include $this->templateDir . $templateName;
|
||||
}
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Exception;
|
||||
namespace Mikro24\Services;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -11,23 +16,21 @@ class Config
|
||||
{
|
||||
private array $config;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$configFile = dirname(path: __DIR__, levels: 2) . "/config.json.local";
|
||||
// Check for either config.json.local or config.json.
|
||||
$configFile = dirname(path: __DIR__, levels: 2) . "/config.local.json";
|
||||
if (!file_exists(filename: $configFile)) {
|
||||
$configFile = dirname(path: __DIR__, levels: 2) . "/config.json";
|
||||
}
|
||||
|
||||
if (!file_exists(filename: $configFile)) {
|
||||
throw new Exception(message: 'Missing config file');
|
||||
die('Missing config file');
|
||||
}
|
||||
$configJSON = file_get_contents(filename: $configFile);
|
||||
|
||||
if (json_decode(json: $configJSON) === null) {
|
||||
throw new Exception(message: 'Config file is not valid JSON.');
|
||||
die('Config file is not valid JSON.');
|
||||
}
|
||||
|
||||
$this->config = json_decode(json: $configJSON, associative: true);
|
68
src/Services/Container.php
Normal file
68
src/Services/Container.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace 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")
|
||||
};
|
||||
}
|
||||
}
|
46
src/Services/DatabaseConnection.php
Normal file
46
src/Services/DatabaseConnection.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Mikro24\Services;
|
||||
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Take care of the PDO object.
|
||||
*/
|
||||
class DatabaseConnection
|
||||
{
|
||||
private PDO $dbConnection;
|
||||
|
||||
// Currently no prefixes are used, but could be easily added to config.json.
|
||||
const TABLE_PREFIX = '';
|
||||
const TABLE_USERS = self::TABLE_PREFIX . "users";
|
||||
const TABLE_ADDRESSES = self::TABLE_PREFIX . "addresses";
|
||||
|
||||
public function __construct(private readonly Config $config)
|
||||
{
|
||||
$dbHost = $this->config->getConfig(configKey: 'dbHost');
|
||||
$dbPort = $this->config->getConfig(configKey: 'dbPort');
|
||||
$dbDatabase = $this->config->getConfig(configKey: 'dbDatabase');
|
||||
$dbUser = $this->config->getConfig(configKey: 'dbUser');
|
||||
$dbPassword = $this->config->getConfig(configKey: 'dbPassword');
|
||||
|
||||
$this->dbConnection = new PDO(
|
||||
dsn: "mysql:host=$dbHost;port=$dbPort;charset=utf8mb4;dbname=$dbDatabase",
|
||||
username: $dbUser,
|
||||
password: $dbPassword
|
||||
);
|
||||
$this->dbConnection->setAttribute(attribute: PDO::ATTR_ERRMODE, value: PDO::ERRMODE_EXCEPTION);
|
||||
}
|
||||
|
||||
public function getConnection(): PDO
|
||||
{
|
||||
return $this->dbConnection;
|
||||
}
|
||||
}
|
124
src/Services/Router.php
Normal file
124
src/Services/Router.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Mikro24\Services;
|
||||
|
||||
|
||||
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 maybe support GET as well.
|
||||
*/
|
||||
|
||||
class Router
|
||||
{
|
||||
/*
|
||||
* The easiest way to differentiate between static and dynamic routes is using
|
||||
* two arrays, no need to pollute the class Route with that information
|
||||
*/
|
||||
private array $staticRoutes = [];
|
||||
private array $dynamicRoutes = [];
|
||||
|
||||
public function __construct(private readonly Template $template)
|
||||
{
|
||||
// empty body
|
||||
}
|
||||
|
||||
/*
|
||||
* This method takes a route like /admin/users/{user} and creates a regex to match on call
|
||||
* More complex routes like /posts/{thread}/show/{page} are supported as well.
|
||||
*/
|
||||
function addRoute(string $name, string $route, Closure $callback): void
|
||||
{
|
||||
// check for parameters
|
||||
preg_match_all(pattern: "/(?<={).+?(?=})/", subject: $route, matches: $matches);
|
||||
$parameters = $matches[0];
|
||||
|
||||
// create regex for route:
|
||||
$regex = preg_replace(pattern: '/{.+?}/', replacement: '([a-zA-Z0-9]*)', subject: $route);
|
||||
// escape \ in regex
|
||||
$regex = '/^' . str_replace(search: "/", replace: '\\/', subject: $regex) . '$/i';
|
||||
|
||||
$route = new Route(name: $name, route: $route, regEx: $regex, parameters: $parameters, callback: $callback);
|
||||
|
||||
if ($parameters) {
|
||||
$this->dynamicRoutes[] = $route;
|
||||
} else {
|
||||
$this->staticRoutes[] = $route;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks if there is a known route and executes the callback.
|
||||
*/
|
||||
public function handleRouting(): void
|
||||
{
|
||||
$requestUri = $_SERVER['REQUEST_URI'];
|
||||
|
||||
/*
|
||||
* Static routes have precedence over dynamic ones, so
|
||||
* /admin/user/add to add and
|
||||
* /admin/user/{name} to edit is possible.
|
||||
* A user named "add" of course not :)
|
||||
*
|
||||
* But who wants to call their users "add" or "delete"?
|
||||
* That's as weird as Little Bobby Tables … (https://xkcd.com/327/)
|
||||
*/
|
||||
foreach ($this->staticRoutes as $route) {
|
||||
if (preg_match(pattern: $route->getRegex(), subject: $requestUri, matches: $matches)) {
|
||||
call_user_func(callback: $route->getCallback());
|
||||
|
||||
// We've found our route, bail out.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->dynamicRoutes as $route) {
|
||||
// PHPStorm doesn't know that $parameters are always available,
|
||||
// (as these are dynamic routes) so I init the array just to make PHPStorm happy.
|
||||
$parameters = [];
|
||||
if (preg_match(pattern: $route->getRegex(), subject: $requestUri, matches: $matches)) {
|
||||
foreach ($route->getParameters() as $id => $parameter) {
|
||||
$parameters[$parameter] = $matches[$id + 1];
|
||||
}
|
||||
// PHP is mad about named parameters in call_user_func when adding parameters.
|
||||
// Uncaught Error: Unknown named parameter $args in <sourceFile>
|
||||
// But PHPStorm seems happy without them. So what?
|
||||
call_user_func($route->getCallback(), $parameters);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// if no route is matched, throw a 404
|
||||
$this->template->render(templateName: 'status/404.html.php');
|
||||
}
|
||||
|
||||
public function path(string $routeName, array $vars = [])
|
||||
{
|
||||
foreach (array_merge($this->dynamicRoutes, $this->staticRoutes) as $route) {
|
||||
if ($route->getName() == $routeName) {
|
||||
if ($vars) {
|
||||
// build route for dynamic routes
|
||||
$route = $route->getRoute();
|
||||
// replace placeholder with current values
|
||||
foreach ($vars as $key => $value) {
|
||||
$route = str_replace(search: '{' . $key . '}', replace: $value, subject: $route);
|
||||
}
|
||||
return $route;
|
||||
} else {
|
||||
return $route->getRoute();
|
||||
}
|
||||
}
|
||||
}
|
||||
// no 404, this is reached only if the code is buggy
|
||||
die("Missing Route: $routeName");
|
||||
}
|
||||
}
|
54
src/Services/Template.php
Normal file
54
src/Services/Template.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022 Micha Espey <tracer@24unix.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace 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.
|
||||
*/
|
||||
|
||||
class Template
|
||||
{
|
||||
/*
|
||||
* Just store the information about the template base dir.
|
||||
*/
|
||||
public function __construct(private readonly string $templateDir)
|
||||
{
|
||||
// empty body
|
||||
}
|
||||
|
||||
/*
|
||||
* Add variables to template and throw it out
|
||||
*/
|
||||
public function render(string $templateName, array $vars = []): never
|
||||
{
|
||||
// assign template vars
|
||||
foreach ($vars as $name => $value) {
|
||||
$$name = $value;
|
||||
}
|
||||
|
||||
$templateFile = $this->templateDir . $templateName;
|
||||
if (file_exists(filename: $templateFile)) {
|
||||
include $this->templateDir . $templateName;
|
||||
} else {
|
||||
die("Missing template: $templateFile");
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* For AJAX calls, return json
|
||||
*/
|
||||
public function renderJson(array $results): never
|
||||
{
|
||||
http_response_code(response_code: $results['status']);
|
||||
echo json_encode(value: $results);
|
||||
exit(0);
|
||||
}
|
||||
}
|
7
templates/_footer.html.php
Normal file
7
templates/_footer.html.php
Normal file
@ -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>
|
27
templates/_header.html.php
Normal file
27
templates/_header.html.php
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Address Book
|
||||
<?php if (!empty($user->getNick())): ?>
|
||||
- <?= $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'); ?>">⌂ Home</a>
|
||||
<a href="<?= $router->path('app_admin'); ?>">⚙ Admin</a>
|
||||
<?php if (empty($user) || $user->getAuth() == \App\Enums\UserAuth::AUTH_ANONYMOUS): ?>
|
||||
<a href="<?= $router->path('app_login'); ?>">⎆ Login</a>
|
||||
<?php else: ?>
|
||||
<a href="<?= $router->path('app_logout'); ?>">⎋ Logout</a>
|
||||
<?php endif; ?>
|
||||
<br>
|
||||
<div id="info_box">Info</div>
|
||||
<div id="error_box">
|
||||
<span class="close_button" onclick="closeError()">×</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>
|
35
templates/addressbook/add_address.html.php
Normal file
35
templates/addressbook/add_address.html.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="owner" id="owner" value="<?= $user->getId() ?>">
|
||||
|
||||
<label for="first">First</label>
|
||||
<input type="text" name="first" id="first" required>
|
||||
<br>
|
||||
|
||||
<label for="last">Last</label>
|
||||
<input type="text" name="last" id="last" required>
|
||||
<br>
|
||||
|
||||
<label for="city">City</label>
|
||||
<input type="text" name="city" id="city">
|
||||
<br>
|
||||
|
||||
<label for="zip">Zip</label>
|
||||
<input type="text" name="zip" id="zip">
|
||||
<br>
|
||||
|
||||
<label for="street">Street</label>
|
||||
<input type="text" name="street" id="street">
|
||||
<br>
|
||||
|
||||
<label for="phone">Phone</label>
|
||||
<input type="text" name="phone" id="phone">
|
||||
<br>
|
||||
|
||||
<!-- maybe later -->
|
||||
<!-- <input type="hidden" name="_csrf" value="csrf_token" -->
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>
|
10
templates/admin/index.html.php
Normal file
10
templates/admin/index.html.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
|
||||
|
||||
<br>
|
||||
<h1>Address Book - Admin</h1>
|
||||
<a href="<?= $router->path('app_admin_users'); ?>">👱 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>
|
35
templates/admin/users.html.php
Normal file
35
templates/admin/users.html.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
|
||||
|
||||
<h2>User list</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Nick</th>
|
||||
<th>First</th>
|
||||
<th>Last</th>
|
||||
<th>Is Admin</th>
|
||||
<th colspan="2"> </th>
|
||||
</tr>
|
||||
<?php foreach ($users as $userLine): ?>
|
||||
<tr>
|
||||
<td><?= $userLine->getNick() ?></td>
|
||||
<td><?= $userLine->getFirst() ?></td>
|
||||
<td><?= $userLine->getLast() ?></td>
|
||||
<td style="text-align: center;">
|
||||
<?php if ($userLine->isAdmin()): ?>
|
||||
☑️
|
||||
<?php else: ?>
|
||||
👎
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?= $router->path('app_admin_users_edit', vars: ['nick' => $userLine->getNick()]); ?>">edit</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?= $router->path('app_admin_users_delete', vars: ['nick' => $userLine->getNick()]); ?>">delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<a href="<?= $router->path('app_admin_users_add'); ?>">Add User</a>
|
||||
|
||||
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>
|
29
templates/admin/users_add.html.php
Normal file
29
templates/admin/users_add.html.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
|
||||
|
||||
<form method="POST">
|
||||
<label for="nick">Username</label>
|
||||
<input type="text" name="nick" id="nick" maxlength="20" required>
|
||||
<br>
|
||||
|
||||
<label for="new_password">Password</label>
|
||||
<input type="password" name="new_password" id="new_password" maxlength="40" required>
|
||||
<br>
|
||||
|
||||
<label for="first">First</label>
|
||||
<input type="text" name="first" id="first" maxlength="40" required>
|
||||
<br>
|
||||
|
||||
<label for="last">Last</label>
|
||||
<input type="text" name="last" id="last" maxlength="40" required>
|
||||
<br>
|
||||
|
||||
<label for="is_admin">Is Admin</label>
|
||||
<input type="checkbox" name="is_admin" id="is_admin">
|
||||
<br>
|
||||
|
||||
<!-- maybe later -->
|
||||
<!-- <input type="hidden" name="_csrf" value="csrf_token" -->
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>
|
31
templates/admin/users_edit.html.php
Normal file
31
templates/admin/users_edit.html.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="id" value="<?= $editUser->getId() ?>">
|
||||
|
||||
<label for="nick">Username</label>
|
||||
<input type="text" name="nick" id="nick" value="<?= $editUser->getNick() ?>" maxlength="20" required>
|
||||
<br>
|
||||
|
||||
<label for="new_password">Password (leave empty to keep the current one)</label>
|
||||
<input type="password" name="new_password" id="new_password" maxlength="40">
|
||||
<br>
|
||||
|
||||
<label for="first">First</label>
|
||||
<input type="text" name="first" id="first" value="<?= $editUser->getFirst() ?>" maxlength="40" required>
|
||||
<br>
|
||||
|
||||
<label for="last">Last</label>
|
||||
<input type="text" name="last" id="last" value="<?= $editUser->getLast() ?>" maxlength="40" required>
|
||||
<br>
|
||||
|
||||
<label for="is_admin">Is Admin</label>
|
||||
<input type="checkbox" name="is_admin" id="is_admin" <?= $editUser->isAdmin()?'checked':'' ?>>
|
||||
<br>
|
||||
|
||||
<!-- maybe later -->
|
||||
<!-- <input type="hidden" name="_csrf" value="csrf_token" -->
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>
|
45
templates/index.html.php
Normal file
45
templates/index.html.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php include '_header.html.php' ?>
|
||||
|
||||
<br>
|
||||
<h2>Welcome to Address Book</h2>
|
||||
|
||||
<?php if(!empty($addresses)): ?>
|
||||
<form method="POST">
|
||||
<table id="address_table">
|
||||
<tr>
|
||||
<th id="first" onclick="sortBy('first')">First</th>
|
||||
<th id="last" onclick="sortBy('last')">Last</th>
|
||||
<th id="street" onclick="sortBy('street')">Street</th>
|
||||
<th id="zip" onclick="sortBy('zip')">Zip</th>
|
||||
<th id="city" onclick="sortBy('city')">City</th>
|
||||
<th id="phone" onclick="sortBy('phone')">Phone</th>
|
||||
<th colspan="2"> </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' %}
|
21
templates/security/login.html.php
Normal file
21
templates/security/login.html.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
|
||||
|
||||
<?php if (false): ?>
|
||||
You need Admin rights to access this area.
|
||||
<?php else: ?>
|
||||
<br>
|
||||
|
||||
<form method="POST">
|
||||
<label for="nick">Username</label>
|
||||
<input type="text" name="nick" id="nick">
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" id="password">
|
||||
|
||||
<!-- maybe later -->
|
||||
<!-- <input type="hidden" name="_csrf" value="csrf_token" -->
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>
|
5
templates/status/403.html.php
Normal file
5
templates/status/403.html.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php include dirname(path: __DIR__) . '/_header.html.php'; ?>
|
||||
|
||||
<h2>403 Permission denied</h2>
|
||||
|
||||
<?php include dirname(path: __DIR__) . '/_footer.html.php' ?>
|
3
templates/status/404.html.php
Normal file
3
templates/status/404.html.php
Normal file
@ -0,0 +1,3 @@
|
||||
<h2>404 Page not found</h2>
|
||||
|
||||
The requested URL cannot be found on this server.
|
Reference in New Issue
Block a user