├── plugins └── empty ├── webroot ├── js │ └── empty ├── favicon.ico ├── img │ ├── cake-logo.png │ ├── cake.icon.png │ ├── cake.power.gif │ └── cake.logo.svg ├── font │ ├── cakedingbats-webfont.eot │ ├── cakedingbats-webfont.ttf │ ├── cakedingbats-webfont.woff │ └── cakedingbats-webfont.woff2 ├── .htaccess ├── index.php └── css │ ├── home.css │ └── cake.css ├── src ├── View │ ├── Helper │ │ └── empty │ ├── AppView.php │ └── AjaxView.php ├── Model │ ├── Behavior │ │ └── empty │ ├── Entity │ │ ├── ArticlesTag.php │ │ ├── Tag.php │ │ ├── User.php │ │ └── Article.php │ └── Table │ │ ├── ArticlesTagsTable.php │ │ ├── TagsTable.php │ │ ├── UsersTable.php │ │ └── ArticlesTable.php ├── Controller │ ├── Component │ │ └── empty │ ├── ErrorController.php │ ├── AppController.php │ ├── PagesController.php │ ├── ArticlesController.php │ ├── TagsController.php │ └── UsersController.php ├── Policy │ └── ArticlePolicy.php ├── Application.php └── Console │ └── Installer.php ├── tests ├── TestCase │ ├── View │ │ └── Helper │ │ │ └── empty │ ├── Model │ │ ├── Behavior │ │ │ └── empty │ │ └── Table │ │ │ ├── ArticlesTagsTableTest.php │ │ │ ├── TagsTableTest.php │ │ │ ├── UsersTableTest.php │ │ │ └── ArticlesTableTest.php │ ├── Controller │ │ ├── Component │ │ │ └── empty │ │ ├── UsersControllerTest.php │ │ ├── TagsControllerTest.php │ │ └── PagesControllerTest.php │ └── ApplicationTest.php ├── bootstrap.php └── Fixture │ ├── TagsFixture.php │ ├── ArticlesTagsFixture.php │ ├── UsersFixture.php │ └── ArticlesFixture.php ├── templates ├── element │ └── flash │ │ ├── error.php │ │ ├── success.php │ │ └── default.php ├── layout │ ├── rss │ │ └── default.php │ ├── ajax.php │ ├── email │ │ ├── text │ │ │ └── default.php │ │ └── html │ │ │ └── default.php │ ├── error.php │ └── default.php ├── Articles │ ├── view.php │ ├── add.php │ ├── edit.php │ ├── tags.php │ └── index.php ├── Users │ ├── login.php │ ├── add.php │ ├── edit.php │ ├── index.php │ └── view.php ├── email │ ├── text │ │ └── default.php │ └── html │ │ └── default.php ├── Tags │ ├── add.php │ ├── edit.php │ ├── index.php │ └── view.php ├── Error │ ├── error400.php │ └── error500.php └── Pages │ └── home.php ├── .htaccess ├── .editorconfig ├── bin ├── cake.php ├── cake.bat └── cake ├── config ├── schema │ ├── sessions.sql │ └── i18n.sql ├── bootstrap_cli.php ├── .env.default ├── requirements.php ├── routes.php ├── app_local.example.php ├── paths.php ├── bootstrap.php ├── Migrations │ └── 20221126022125_InitialSchema.php └── app.php ├── phpcs.xml ├── index.php ├── .travis.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── .gitignore ├── .gitattributes ├── phpunit.xml.dist ├── README.md └── composer.json /plugins/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webroot/js/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/View/Helper/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Model/Behavior/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Controller/Component/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/TestCase/View/Helper/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Behavior/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/Component/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/cms-tutorial/HEAD/webroot/favicon.ico -------------------------------------------------------------------------------- /webroot/img/cake-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/cms-tutorial/HEAD/webroot/img/cake-logo.png -------------------------------------------------------------------------------- /webroot/img/cake.icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/cms-tutorial/HEAD/webroot/img/cake.icon.png -------------------------------------------------------------------------------- /webroot/img/cake.power.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/cms-tutorial/HEAD/webroot/img/cake.power.gif -------------------------------------------------------------------------------- /webroot/font/cakedingbats-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/cms-tutorial/HEAD/webroot/font/cakedingbats-webfont.eot -------------------------------------------------------------------------------- /webroot/font/cakedingbats-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/cms-tutorial/HEAD/webroot/font/cakedingbats-webfont.ttf -------------------------------------------------------------------------------- /webroot/font/cakedingbats-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/cms-tutorial/HEAD/webroot/font/cakedingbats-webfont.woff -------------------------------------------------------------------------------- /webroot/font/cakedingbats-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/cms-tutorial/HEAD/webroot/font/cakedingbats-webfont.woff2 -------------------------------------------------------------------------------- /webroot/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule ^ index.php [L] 5 | 6 | -------------------------------------------------------------------------------- /templates/element/flash/error.php: -------------------------------------------------------------------------------- 1 | 6 |
7 | -------------------------------------------------------------------------------- /templates/element/flash/success.php: -------------------------------------------------------------------------------- 1 | 6 |
7 | -------------------------------------------------------------------------------- /templates/layout/rss/default.php: -------------------------------------------------------------------------------- 1 | fetch('title'); 7 | endif; 8 | 9 | echo $this->Rss->document( 10 | $this->Rss->channel([], $channel, $this->fetch('content')) 11 | ); 12 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 4 | # RequestHeader unset Proxy 5 | # 6 | 7 | 8 | RewriteEngine on 9 | RewriteRule ^$ webroot/ [L] 10 | RewriteRule (.*) webroot/$1 [L] 11 | 12 | -------------------------------------------------------------------------------- /templates/element/flash/default.php: -------------------------------------------------------------------------------- 1 | 10 |
11 | -------------------------------------------------------------------------------- /templates/Articles/view.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

title) ?>

4 |

body) ?>

5 |

Tags: tag_string) ?>

6 |

Created: created->format(DATE_RFC850) ?>

7 |

Html->link('Edit', ['action' => 'edit', $article->slug]) ?>

8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at https://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.bat] 14 | end_of_line = crlf 15 | 16 | [*.yml] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /templates/Articles/add.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Add Article

4 | Form->create($article); 6 | echo $this->Form->control('title'); 7 | echo $this->Form->control('body', ['rows' => '3']); 8 | echo $this->Form->control('tag_string', ['type' => 'text']); 9 | echo $this->Form->button(__('Save Article')); 10 | echo $this->Form->end(); 11 | ?> 12 | -------------------------------------------------------------------------------- /templates/Articles/edit.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Edit Article

4 | Form->create($article); 6 | echo $this->Form->control('title'); 7 | echo $this->Form->control('body', ['rows' => '3']); 8 | echo $this->Form->control('tag_string', ['type' => 'text']); 9 | echo $this->Form->button(__('Save Article')); 10 | echo $this->Form->end(); 11 | ?> 12 | -------------------------------------------------------------------------------- /bin/cake.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -q 2 | run($argv)); 13 | -------------------------------------------------------------------------------- /config/schema/sessions.sql: -------------------------------------------------------------------------------- 1 | # Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 2 | # 3 | # Licensed under The MIT License 4 | # For full copyright and license information, please see the LICENSE.txt 5 | # Redistributions of files must retain the above copyright notice. 6 | # MIT License (https://opensource.org/licenses/mit-license.php) 7 | 8 | CREATE TABLE sessions ( 9 | id char(40) NOT NULL, 10 | data text, 11 | expires INT(11) NOT NULL, 12 | PRIMARY KEY (id) 13 | ); 14 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 10 | 11 | 12 | ./src 13 | ./tests 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /templates/Articles/tags.php: -------------------------------------------------------------------------------- 1 |

2 | Articles tagged with 3 | Text->toList(h($tags), 'or') ?> 4 |

5 | 6 |
7 | 8 |
9 | 10 |

Html->link( 11 | $article->title, 12 | ['controller' => 'Articles', 'action' => 'view', $article->slug] 13 | ) ?>

14 | created) ?> 15 |
16 | 17 |
-------------------------------------------------------------------------------- /templates/Users/login.php: -------------------------------------------------------------------------------- 1 |
2 | Flash->render() ?> 3 |

Login

4 | Form->create() ?> 5 |
6 | 7 | Form->control('email', ['required' => true]) ?> 8 | Form->control('password', ['required' => true]) ?> 9 |
10 | Form->submit(__('Login')); ?> 11 | Form->end() ?> 12 | 13 | Html->link("Add User", ['action' => 'add']) ?> 14 |
15 | -------------------------------------------------------------------------------- /templates/email/text/default.php: -------------------------------------------------------------------------------- 1 | fetch('content'); 17 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | fetch('content'); 17 | -------------------------------------------------------------------------------- /config/schema/i18n.sql: -------------------------------------------------------------------------------- 1 | # Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 2 | # 3 | # Licensed under The MIT License 4 | # For full copyright and license information, please see the LICENSE.txt 5 | # Redistributions of files must retain the above copyright notice. 6 | # MIT License (https://opensource.org/licenses/mit-license.php) 7 | 8 | CREATE TABLE i18n ( 9 | id int NOT NULL auto_increment, 10 | locale varchar(6) NOT NULL, 11 | model varchar(255) NOT NULL, 12 | foreign_key int(10) NOT NULL, 13 | field varchar(255) NOT NULL, 14 | content text, 15 | PRIMARY KEY (id), 16 | UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field), 17 | INDEX I18N_FIELD(model, foreign_key, field) 18 | ); 19 | -------------------------------------------------------------------------------- /templates/email/html/default.php: -------------------------------------------------------------------------------- 1 | ' . $line . "

\n"; 20 | endforeach; 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: trusty 4 | 5 | sudo: false 6 | 7 | php: 8 | - 5.6 9 | - 7.0 10 | - 7.1 11 | 12 | matrix: 13 | fast_finish: true 14 | 15 | include: 16 | - php: 7.0 17 | env: PHPCS=1 18 | 19 | before_script: 20 | - if [[ $PHPCS = 1 ]]; then composer require cakephp/cakephp-codesniffer:~2.1; fi 21 | - if [[ $PHPCS != 1 ]]; then composer install; fi 22 | - if [[ $PHPCS != 1 ]]; then composer require phpunit/phpunit:"^5.7|^6.0"; fi 23 | - if [[ $PHPCS != 1 ]]; then composer run-script post-install-cmd --no-interaction; fi 24 | 25 | script: 26 | - if [[ $PHPCS != 1 ]]; then vendor/bin/phpunit; fi 27 | - if [[ $PHPCS = 1 ]]; then vendor/bin/phpcs -p --extensions=php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests ./config ./webroot; fi 28 | 29 | notifications: 30 | email: false 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **PLEASE NOTE:** 2 | 3 | This is only a issue tracker for issues related to the CakePHP Application Skeleton. 4 | For CakePHP Framework issues please use this [issue tracker](https://github.com/cakephp/cakephp/issues). 5 | 6 | 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. 7 | 8 | The best way to propose a feature is to open an issue first and discuss your ideas there before implementing them. 9 | 10 | 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. -------------------------------------------------------------------------------- /templates/layout/email/html/default.php: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | <?= $this->fetch('title') ?> 20 | 21 | 22 | fetch('content') ?> 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # CakePHP specific files # 2 | ########################## 3 | /config/.env 4 | /config/app_local.php 5 | /tmp/* 6 | /logs/* 7 | /tmp/* 8 | /vendor/* 9 | 10 | # OS generated files # 11 | ###################### 12 | .DS_Store 13 | .DS_Store? 14 | ._* 15 | .Spotlight-V100 16 | .Trashes 17 | Icon? 18 | ehthumbs.db 19 | Thumbs.db 20 | 21 | # Tool specific files # 22 | ####################### 23 | *~ # vim 24 | *.swp # vim 25 | *.swo # vim 26 | *.sublime-* # sublime text 27 | *.tmlanguage.cache # sublime text & textmate 28 | *.tmPreferences.cache # sublime text & textmate 29 | *.stTheme.cache # cache files for sublime text 30 | .settings/* # Eclipse 31 | .idea/* # JetBrains, aka PHPStorm, IntelliJ IDEA 32 | nbproject/* # NetBeans 33 | .vscode # Visual Studio Code 34 | .sass-cache/ # Sass preprocessor 35 | .idea 36 | config/Migrations/schema-dump-default.lock 37 | database 38 | cms-tutorial-database.sqlite 39 | -------------------------------------------------------------------------------- /src/Model/Entity/ArticlesTag.php: -------------------------------------------------------------------------------- 1 | true, 30 | 'tag' => true, 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /src/Policy/ArticlePolicy.php: -------------------------------------------------------------------------------- 1 | isAuthor($user, $article); 19 | } 20 | 21 | public function canDelete(IdentityInterface $user, Article $article) 22 | { 23 | // logged in users can delete their own articles. 24 | return $this->isAuthor($user, $article); 25 | } 26 | 27 | protected function isAuthor(IdentityInterface $user, Article $article) 28 | { 29 | return $article->user_id === $user->getIdentifier(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This is a (multiple allowed): 2 | 3 | * [x] bug 4 | * [ ] enhancement 5 | * [ ] feature-discussion (RFC) 6 | 7 | * CakePHP Application Skeleton Version: EXACT RELEASE VERSION OR COMMIT HASH, HERE. 8 | * Platform and Target: YOUR WEB-SERVER, DATABASE AND OTHER RELEVANT INFO AND HOW THE REQUEST IS BEING MADE, HERE. 9 | 10 | ### What you did 11 | EXPLAIN WHAT YOU DID, PREFERABLY WITH CODE EXAMPLES, HERE. 12 | 13 | ### What happened 14 | EXPLAIN WHAT IS ACTUALLY HAPPENING, HERE. 15 | 16 | ### What you expected to happen 17 | EXPLAIN WHAT IS TO BE EXPECTED, HERE. 18 | 19 | P.S. Remember, an issue is not the place to ask questions. You can use [Stack Overflow](https://stackoverflow.com/questions/tagged/cakephp) 20 | for that or join the #cakephp channel on irc.freenode.net, where we will be more 21 | than happy to help answer your questions. 22 | 23 | Before you open an issue, please check if a similar issue already exists or has been closed before. -------------------------------------------------------------------------------- /bin/cake.bat: -------------------------------------------------------------------------------- 1 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 2 | :: 3 | :: Cake is a Windows batch script for invoking CakePHP shell commands 4 | :: 5 | :: CakePHP(tm) : Rapid Development Framework (https://cakephp.org) 6 | :: Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 7 | :: 8 | :: Licensed under The MIT License 9 | :: Redistributions of files must retain the above copyright notice. 10 | :: 11 | :: @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 12 | :: @link https://cakephp.org CakePHP(tm) Project 13 | :: @since 2.0.0 14 | :: @license https://opensource.org/licenses/mit-license.php MIT License 15 | :: 16 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 17 | 18 | @echo off 19 | 20 | SET app=%0 21 | SET lib=%~dp0 22 | 23 | php "%lib%cake.php" %* 24 | 25 | echo. 26 | 27 | exit /B %ERRORLEVEL% 28 | -------------------------------------------------------------------------------- /templates/Users/add.php: -------------------------------------------------------------------------------- 1 | 6 | 14 |
15 | Form->create($user) ?> 16 |
17 | 18 | Form->control('email'); 20 | echo $this->Form->control('password'); 21 | ?> 22 |
23 | Form->button(__('Submit')) ?> 24 | Form->end() ?> 25 |
26 | -------------------------------------------------------------------------------- /src/Model/Entity/Tag.php: -------------------------------------------------------------------------------- 1 | true, 31 | 'created' => true, 32 | 'modified' => true, 33 | 'articles' => true, 34 | ]; 35 | } 36 | -------------------------------------------------------------------------------- /templates/Tags/add.php: -------------------------------------------------------------------------------- 1 | 7 |
8 | 14 |
15 |
16 | Form->create($tag) ?> 17 |
18 | 19 | Form->control('title'); 21 | echo $this->Form->control('articles._ids', ['options' => $articles]); 22 | ?> 23 |
24 | Form->button(__('Submit')) ?> 25 | Form->end() ?> 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Define the line ending behavior of the different file extensions 2 | # Set default behaviour, in case users don't have core.autocrlf set. 3 | * text=auto 4 | * text eol=lf 5 | 6 | # Explicitly declare text files we want to always be normalized and converted 7 | # to native line endings on checkout. 8 | *.php text 9 | *.default text 10 | *.ctp text 11 | *.sql text 12 | *.md text 13 | *.po text 14 | *.js text 15 | *.css text 16 | *.ini text 17 | *.properties text 18 | *.txt text 19 | *.xml text 20 | *.svg text 21 | *.yml text 22 | .htaccess text 23 | 24 | # Declare files that will always have CRLF line endings on checkout. 25 | *.bat eol=crlf 26 | 27 | # Declare files that will always have LF line endings on checkout. 28 | *.pem eol=lf 29 | 30 | # Denote all files that are truly binary and should not be modified. 31 | *.png binary 32 | *.jpg binary 33 | *.gif binary 34 | *.ico binary 35 | *.mo binary 36 | *.pdf binary 37 | *.phar binary 38 | *.woff binary 39 | *.woff2 binary 40 | *.ttf binary 41 | *.otf binary 42 | *.eot binary 43 | -------------------------------------------------------------------------------- /templates/Articles/index.php: -------------------------------------------------------------------------------- 1 | 2 | Html->link('Add Article', ['action' => 'add']) ?> 3 |

Articles

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 21 | 29 | 30 | 31 |
TitleCreatedAction
16 | Html->link($article->title, ['action' => 'view', $article->slug]) ?> 17 | 19 | created->format(DATE_RFC850) ?> 20 | 22 | Html->link('Edit', ['action' => 'edit', $article->slug]) ?> 23 | Form->postLink( 24 | 'Delete', 25 | ['action' => 'delete', $article->slug], 26 | ['confirm' => 'Are you sure?']) 27 | ?> 28 |
32 | -------------------------------------------------------------------------------- /templates/Error/error400.php: -------------------------------------------------------------------------------- 1 | layout = 'error'; 6 | 7 | if (Configure::read('debug')) : 8 | $this->layout = 'dev_error'; 9 | 10 | $this->assign('title', $message); 11 | $this->assign('templateName', 'error400.php'); 12 | 13 | $this->start('file'); 14 | ?> 15 | queryString)) : ?> 16 |

17 | SQL Query: 18 | queryString) ?> 19 |

20 | 21 | params)) : ?> 22 | SQL Query Params: 23 | params) ?> 24 | 25 | element('auto_table_warning') ?> 26 | end(); 32 | endif; 33 | ?> 34 |

35 |

36 | : 37 | '{$url}'") ?> 38 |

39 | -------------------------------------------------------------------------------- /src/View/AppView.php: -------------------------------------------------------------------------------- 1 | loadHelper('Html');` 35 | * 36 | * @return void 37 | */ 38 | public function initialize(): void 39 | { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/bootstrap_cli.php: -------------------------------------------------------------------------------- 1 | 6 | 20 |
21 | Form->create($user) ?> 22 |
23 | 24 | Form->control('email'); 26 | echo $this->Form->control('password'); 27 | ?> 28 |
29 | Form->button(__('Submit')) ?> 30 | Form->end() ?> 31 |
32 | -------------------------------------------------------------------------------- /templates/Tags/edit.php: -------------------------------------------------------------------------------- 1 | 7 |
8 | 19 |
20 |
21 | Form->create($tag) ?> 22 |
23 | 24 | Form->control('title'); 26 | echo $this->Form->control('articles._ids', ['options' => $articles]); 27 | ?> 28 |
29 | Form->button(__('Submit')) ?> 30 | Form->end() ?> 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /templates/Error/error500.php: -------------------------------------------------------------------------------- 1 | layout = 'error'; 6 | 7 | if (Configure::read('debug')) : 8 | $this->layout = 'dev_error'; 9 | 10 | $this->assign('title', $message); 11 | $this->assign('templateName', 'error500.php'); 12 | 13 | $this->start('file'); 14 | ?> 15 | queryString)) : ?> 16 |

17 | SQL Query: 18 | queryString) ?> 19 |

20 | 21 | params)) : ?> 22 | SQL Query Params: 23 | params) ?> 24 | 25 | 26 | Error in: 27 | getFile()), $error->getLine()) ?> 28 | 29 | element('auto_table_warning'); 31 | 32 | if (extension_loaded('xdebug')) : 33 | xdebug_print_function_stack(); 34 | endif; 35 | 36 | $this->end(); 37 | endif; 38 | ?> 39 |

40 |

41 | : 42 | 43 |

44 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./tests/TestCase 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ./src/ 37 | ./plugins/*/src/ 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/View/AjaxView.php: -------------------------------------------------------------------------------- 1 | response = $this->response->withType('ajax'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /templates/layout/error.php: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | Html->charset() ?> 20 | 21 | <?= $this->fetch('title') ?> 22 | 23 | Html->meta('icon') ?> 24 | 25 | Html->css('base.css') ?> 26 | Html->css('cake.css') ?> 27 | 28 | fetch('meta') ?> 29 | fetch('css') ?> 30 | fetch('script') ?> 31 | 32 | 33 |
34 | 37 |
38 | Flash->render() ?> 39 | 40 | fetch('content') ?> 41 |
42 | 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /webroot/index.php: -------------------------------------------------------------------------------- 1 | emit($server->run()); 41 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Table/ArticlesTagsTableTest.php: -------------------------------------------------------------------------------- 1 | getTableLocator()->exists('ArticlesTags') ? [] : ['className' => ArticlesTagsTable::class]; 41 | $this->ArticlesTags = $this->getTableLocator()->get('ArticlesTags', $config); 42 | } 43 | 44 | /** 45 | * tearDown method 46 | * 47 | * @return void 48 | */ 49 | public function tearDown(): void 50 | { 51 | unset($this->ArticlesTags); 52 | 53 | parent::tearDown(); 54 | } 55 | 56 | /** 57 | * Test buildRules method 58 | * 59 | * @return void 60 | */ 61 | public function testBuildRules(): void 62 | { 63 | $this->markTestIncomplete('Not implemented yet.'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/UsersControllerTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('Not implemented yet.'); 31 | } 32 | 33 | /** 34 | * Test view method 35 | * 36 | * @return void 37 | */ 38 | public function testView() 39 | { 40 | $this->markTestIncomplete('Not implemented yet.'); 41 | } 42 | 43 | /** 44 | * Test add method 45 | * 46 | * @return void 47 | */ 48 | public function testAdd() 49 | { 50 | $this->markTestIncomplete('Not implemented yet.'); 51 | } 52 | 53 | /** 54 | * Test edit method 55 | * 56 | * @return void 57 | */ 58 | public function testEdit() 59 | { 60 | $this->markTestIncomplete('Not implemented yet.'); 61 | } 62 | 63 | /** 64 | * Test delete method 65 | * 66 | * @return void 67 | */ 68 | public function testDelete() 69 | { 70 | $this->markTestIncomplete('Not implemented yet.'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Model/Entity/User.php: -------------------------------------------------------------------------------- 1 | true, 33 | 'password' => true, 34 | 'created' => true, 35 | 'modified' => true, 36 | 'articles' => true, 37 | ]; 38 | 39 | /** 40 | * Fields that are excluded from JSON versions of the entity. 41 | * 42 | * @var array 43 | */ 44 | protected array $_hidden = [ 45 | 'password', 46 | ]; 47 | 48 | protected function _setPassword($value) 49 | { 50 | if (strlen($value)) { 51 | $hasher = new DefaultPasswordHasher(); 52 | 53 | return $hasher->hash($value); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP CMS Tutorial 2 | 3 | The completed CMS tutorial application created during 4 | https://book.cakephp.org/5/en/tutorials-and-examples/cms/installation.html 5 | 6 | ## Installation 7 | 8 | 1. Download [Composer](https://getcomposer.org/doc/00-intro.md) or update `composer self-update`. 9 | 2. Download this source code. `git clone https://github.com/cakephp/cms-tutorial.git` 10 | 3. Install dependencies with composer 11 | 12 | ```bash 13 | cd cms-tutorial 14 | composer install 15 | ``` 16 | 17 | ## Configuration 18 | 19 | You can use the default configuration, using a sqlite3 file based database. In that case, no configuration 20 | is required. 21 | If you want to use your own database, update the `'Datasources'` configuration in `config/app.php`. 22 | You'll also need to create a database and run the SQL located in the tutorial. 23 | 24 | * Update the `'Datasources'` configuration by: 25 | * editing `config/app.php` or `config/app_local.php` for your local database or 26 | * edit `config/bootstrap.php` to enable use of a `.env` file (uncomment the section that loads the `.env` file) (and provide the `config/.env` file. 27 | * Create a database if needed. 28 | * Run `bin/cake migrations migrate` to create the database tables. 29 | 30 | 31 | ## Running the project 32 | 33 | ```bash 34 | bin/cake server 35 | ``` 36 | 37 | Then point your browser to http://localhost:8765 38 | 39 | You will be redirected to the `/users/login` page. This is the behavior we configured to request a valid 40 | username and password. The CMS Tutorial user Authentication and Authorization to identify the users. 41 | -------------------------------------------------------------------------------- /bin/cake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | ################################################################################ 3 | # 4 | # Cake is a shell script for invoking CakePHP shell commands 5 | # 6 | # CakePHP(tm) : Rapid Development Framework (https://cakephp.org) 7 | # Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 8 | # 9 | # Licensed under The MIT License 10 | # For full copyright and license information, please see the LICENSE.txt 11 | # Redistributions of files must retain the above copyright notice. 12 | # 13 | # @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 14 | # @link https://cakephp.org CakePHP(tm) Project 15 | # @since 1.2.0 16 | # @license https://opensource.org/licenses/mit-license.php MIT License 17 | # 18 | ################################################################################ 19 | 20 | # Canonicalize by following every symlink of the given name recursively 21 | canonicalize() { 22 | NAME="$1" 23 | if [ -f "$NAME" ] 24 | then 25 | DIR=$(dirname -- "$NAME") 26 | NAME=$(cd -P "$DIR" > /dev/null && pwd -P)/$(basename -- "$NAME") 27 | fi 28 | while [ -h "$NAME" ]; do 29 | DIR=$(dirname -- "$NAME") 30 | SYM=$(readlink "$NAME") 31 | NAME=$(cd "$DIR" > /dev/null && cd $(dirname -- "$SYM") > /dev/null && pwd)/$(basename -- "$SYM") 32 | done 33 | echo "$NAME" 34 | } 35 | 36 | CONSOLE=$(dirname -- "$(canonicalize "$0")") 37 | APP=$(dirname "$CONSOLE") 38 | 39 | if [ $(basename $0) != 'cake' ] 40 | then 41 | exec php "$CONSOLE"/cake.php $(basename $0) "$@" 42 | else 43 | exec php "$CONSOLE"/cake.php "$@" 44 | fi 45 | 46 | exit 47 | -------------------------------------------------------------------------------- /tests/TestCase/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | middleware($middleware); 41 | 42 | $this->assertInstanceOf(ErrorHandlerMiddleware::class, $middleware->get(0)); 43 | $this->assertInstanceOf(AssetMiddleware::class, $middleware->get(1)); 44 | $this->assertInstanceOf(RoutingMiddleware::class, $middleware->get(2)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Table/TagsTableTest.php: -------------------------------------------------------------------------------- 1 | getTableLocator()->exists('Tags') ? [] : ['className' => TagsTable::class]; 40 | $this->Tags = $this->getTableLocator()->get('Tags', $config); 41 | } 42 | 43 | /** 44 | * tearDown method 45 | * 46 | * @return void 47 | */ 48 | public function tearDown(): void 49 | { 50 | unset($this->Tags); 51 | 52 | parent::tearDown(); 53 | } 54 | 55 | /** 56 | * Test validationDefault method 57 | * 58 | * @return void 59 | */ 60 | public function testValidationDefault(): void 61 | { 62 | $this->markTestIncomplete('Not implemented yet.'); 63 | } 64 | 65 | /** 66 | * Test buildRules method 67 | * 68 | * @return void 69 | */ 70 | public function testBuildRules(): void 71 | { 72 | $this->markTestIncomplete('Not implemented yet.'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /config/.env.default: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Used as a default to seed config/.env which 3 | # enables you to use environment variables to configure 4 | # the aspects of your application that vary by 5 | # environment. 6 | # 7 | # To use this file, first copy it into `config/.env`. 8 | # 9 | # In development .env files are parsed by PHP 10 | # and set into the environment. This provides a simpler 11 | # development workflow over standard environment variables. 12 | export APP_NAME="__APP_NAME__" 13 | export DEBUG="true" 14 | export APP_ENCODING="UTF-8" 15 | export APP_DEFAULT_LOCALE="en_US" 16 | export SECURITY_SALT="__SALT__" 17 | 18 | export CACHE_DURATION="+2 minutes" 19 | export CACHE_DEFAULT_URL="file://tmp/cache/?prefix=${APP_NAME}_default&duration=${CACHE_DURATION}" 20 | export CACHE_CAKECORE_URL="file://tmp/cache/persistent?prefix=${APP_NAME}_cake_core&serialize=true&duration=${CACHE_DURATION}" 21 | export CACHE_CAKEMODEL_URL="file://tmp/cache/models?prefix=${APP_NAME}_cake_model&serialize=true&duration=${CACHE_DURATION}" 22 | 23 | export EMAIL_TRANSPORT_DEFAULT_URL="" 24 | 25 | export DATABASE_URL="mysql://my_app:secret@localhost/${APP_NAME}?encoding=utf8&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=false" 26 | export DATABASE_TEST_URL="mysql://my_app:secret@localhost/test_${APP_NAME}?encoding=utf8&timezone=UTC&cacheMetadata=true"eIdentifiers=false&persistent=false" 27 | 28 | # Uncomment these to define logging configuration via environment variables. 29 | #export LOG_DEBUG_URL="file://logs?levels[]=notice&levels[]=info&levels[]=debug&file=debug" 30 | #export LOG_ERROR_URL="file://logs?levels[]=warning&levels[]=error&levels[]=critical&levels[]=alert&levels[]=emergency&file=error" 31 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Table/UsersTableTest.php: -------------------------------------------------------------------------------- 1 | getTableLocator()->exists('Users') ? [] : ['className' => UsersTable::class]; 40 | $this->Users = $this->getTableLocator()->get('Users', $config); 41 | } 42 | 43 | /** 44 | * tearDown method 45 | * 46 | * @return void 47 | */ 48 | public function tearDown(): void 49 | { 50 | unset($this->Users); 51 | 52 | parent::tearDown(); 53 | } 54 | 55 | /** 56 | * Test validationDefault method 57 | * 58 | * @return void 59 | */ 60 | public function testValidationDefault(): void 61 | { 62 | $this->markTestIncomplete('Not implemented yet.'); 63 | } 64 | 65 | /** 66 | * Test buildRules method 67 | * 68 | * @return void 69 | */ 70 | public function testBuildRules(): void 71 | { 72 | $this->markTestIncomplete('Not implemented yet.'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Table/ArticlesTableTest.php: -------------------------------------------------------------------------------- 1 | getTableLocator()->exists('Articles') ? [] : ['className' => ArticlesTable::class]; 41 | $this->Articles = $this->getTableLocator()->get('Articles', $config); 42 | } 43 | 44 | /** 45 | * tearDown method 46 | * 47 | * @return void 48 | */ 49 | public function tearDown(): void 50 | { 51 | unset($this->Articles); 52 | 53 | parent::tearDown(); 54 | } 55 | 56 | /** 57 | * Test validationDefault method 58 | * 59 | * @return void 60 | */ 61 | public function testValidationDefault(): void 62 | { 63 | $this->markTestIncomplete('Not implemented yet.'); 64 | } 65 | 66 | /** 67 | * Test buildRules method 68 | * 69 | * @return void 70 | */ 71 | public function testBuildRules(): void 72 | { 73 | $this->markTestIncomplete('Not implemented yet.'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/TagsControllerTest.php: -------------------------------------------------------------------------------- 1 | markTestIncomplete('Not implemented yet.'); 38 | } 39 | 40 | /** 41 | * Test view method 42 | * 43 | * @return void 44 | */ 45 | public function testView(): void 46 | { 47 | $this->markTestIncomplete('Not implemented yet.'); 48 | } 49 | 50 | /** 51 | * Test add method 52 | * 53 | * @return void 54 | */ 55 | public function testAdd(): void 56 | { 57 | $this->markTestIncomplete('Not implemented yet.'); 58 | } 59 | 60 | /** 61 | * Test edit method 62 | * 63 | * @return void 64 | */ 65 | public function testEdit(): void 66 | { 67 | $this->markTestIncomplete('Not implemented yet.'); 68 | } 69 | 70 | /** 71 | * Test delete method 72 | * 73 | * @return void 74 | */ 75 | public function testDelete(): void 76 | { 77 | $this->markTestIncomplete('Not implemented yet.'); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /config/requirements.php: -------------------------------------------------------------------------------- 1 | = 50.1 is needed to use CakePHP. Please update the `libicu` package of your system.' . PHP_EOL, E_USER_ERROR); 39 | } 40 | 41 | /* 42 | * You can remove this if you are confident you have mbstring installed. 43 | */ 44 | if (!extension_loaded('mbstring')) { 45 | trigger_error('You must enable the mbstring extension to use CakePHP.', E_USER_ERROR); 46 | } 47 | -------------------------------------------------------------------------------- /src/Controller/ErrorController.php: -------------------------------------------------------------------------------- 1 | viewBuilder()->setTemplatePath('Error'); 47 | } 48 | 49 | /** 50 | * afterFilter callback. 51 | * 52 | * @param \Cake\Event\Event $event Event. 53 | * @return \Cake\Http\Response|null|void 54 | */ 55 | public function afterFilter(\Cake\Event\EventInterface $event) 56 | { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Fixture/TagsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], 21 | 'title' => ['type' => 'string', 'length' => 191, 'null' => true, 'default' => null, 'collate' => 'utf8mb4_general_ci', 'comment' => '', 'precision' => null], 22 | 'created' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => true, 'default' => null, 'comment' => ''], 23 | 'modified' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => true, 'default' => null, 'comment' => ''], 24 | '_constraints' => [ 25 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], 26 | 'title' => ['type' => 'unique', 'columns' => ['title'], 'length' => []], 27 | ], 28 | '_options' => [ 29 | 'engine' => 'InnoDB', 30 | 'collation' => 'utf8mb4_general_ci' 31 | ], 32 | ]; 33 | // phpcs:enable 34 | /** 35 | * Init method 36 | * 37 | * @return void 38 | */ 39 | public function init(): void 40 | { 41 | $this->records = [ 42 | [ 43 | 'id' => 1, 44 | 'title' => 'Lorem ipsum dolor sit amet', 45 | 'created' => '2020-08-12 02:48:16', 46 | 'modified' => '2020-08-12 02:48:16', 47 | ], 48 | ]; 49 | parent::init(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Fixture/ArticlesTagsFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null], 21 | 'tag_id' => ['type' => 'integer', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null], 22 | '_indexes' => [ 23 | 'tag_key' => ['type' => 'index', 'columns' => ['tag_id'], 'length' => []], 24 | ], 25 | '_constraints' => [ 26 | 'primary' => ['type' => 'primary', 'columns' => ['article_id', 'tag_id'], 'length' => []], 27 | 'articles_tags_ibfk_2' => ['type' => 'foreign', 'columns' => ['article_id'], 'references' => ['articles', 'id'], 'update' => 'restrict', 'delete' => 'restrict', 'length' => []], 28 | 'articles_tags_ibfk_1' => ['type' => 'foreign', 'columns' => ['tag_id'], 'references' => ['tags', 'id'], 'update' => 'restrict', 'delete' => 'restrict', 'length' => []], 29 | ], 30 | '_options' => [ 31 | 'engine' => 'InnoDB', 32 | 'collation' => 'latin1_swedish_ci' 33 | ], 34 | ]; 35 | // phpcs:enable 36 | /** 37 | * Init method 38 | * 39 | * @return void 40 | */ 41 | public function init(): void 42 | { 43 | $this->records = [ 44 | [ 45 | 'article_id' => 1, 46 | 'tag_id' => 1, 47 | ], 48 | ]; 49 | parent::init(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Controller/AppController.php: -------------------------------------------------------------------------------- 1 | loadComponent('Security');` 37 | * 38 | * @return void 39 | */ 40 | public function initialize(): void 41 | { 42 | parent::initialize(); 43 | $this->loadComponent('Flash'); 44 | $this->loadComponent('Authentication.Authentication'); 45 | $this->loadComponent('Authorization.Authorization'); 46 | } 47 | 48 | public function beforeFilter(EventInterface $event) 49 | { 50 | parent::beforeFilter($event); 51 | // for all controllers in our application, make index and view 52 | // actions public, skipping the authentication check 53 | $this->Authentication->addUnauthenticatedActions(['index', 'view']); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Fixture/UsersFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], 21 | 'email' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'collate' => 'latin1_swedish_ci', 'comment' => '', 'precision' => null], 22 | 'password' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'collate' => 'latin1_swedish_ci', 'comment' => '', 'precision' => null], 23 | 'created' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => true, 'default' => null, 'comment' => ''], 24 | 'modified' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => true, 'default' => null, 'comment' => ''], 25 | '_constraints' => [ 26 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], 27 | ], 28 | '_options' => [ 29 | 'engine' => 'InnoDB', 30 | 'collation' => 'latin1_swedish_ci' 31 | ], 32 | ]; 33 | // phpcs:enable 34 | /** 35 | * Init method 36 | * 37 | * @return void 38 | */ 39 | public function init(): void 40 | { 41 | $this->records = [ 42 | [ 43 | 'id' => 1, 44 | 'email' => 'Lorem ipsum dolor sit amet', 45 | 'password' => 'Lorem ipsum dolor sit amet', 46 | 'created' => '2020-08-12 02:42:27', 47 | 'modified' => '2020-08-12 02:42:27', 48 | ], 49 | ]; 50 | parent::init(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /templates/layout/default.php: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | Html->charset() ?> 22 | 23 | 24 | <?= $cakeDescription ?>: 25 | <?= $this->fetch('title') ?> 26 | 27 | Html->meta('icon') ?> 28 | 29 | Html->css('base.css') ?> 30 | Html->css('cake.css') ?> 31 | 32 | fetch('meta') ?> 33 | fetch('css') ?> 34 | fetch('script') ?> 35 | 36 | 37 | 50 | Flash->render() ?> 51 |
52 | fetch('content') ?> 53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /src/Model/Entity/Article.php: -------------------------------------------------------------------------------- 1 | true, 38 | 'title' => true, 39 | 'slug' => true, 40 | 'body' => true, 41 | 'published' => true, 42 | 'created' => true, 43 | 'modified' => true, 44 | 'user' => true, 45 | 'tags' => true, 46 | 'tag_string' => true, 47 | ]; 48 | 49 | protected function _setTitle(string $title): string 50 | { 51 | $this->slug = Text::slug($title); 52 | 53 | return $title; 54 | } 55 | 56 | protected function _getTagString(): string 57 | { 58 | if (isset($this->_fields['tag_string'])) { 59 | return $this->_fields['tag_string']; 60 | } 61 | if (empty($this->tags)) { 62 | return ''; 63 | } 64 | $tags = new Collection($this->tags); 65 | $str = $tags->reduce(function ($string, $tag) { 66 | return $string . $tag->title . ', '; 67 | }, ''); 68 | 69 | return trim($str, ', '); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /templates/Tags/index.php: -------------------------------------------------------------------------------- 1 | 7 |
8 | Html->link(__('New Tag'), ['action' => 'add'], ['class' => 'button float-right']) ?> 9 |

10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 36 |
Paginator->sort('id') ?>Paginator->sort('title') ?>Paginator->sort('created') ?>Paginator->sort('modified') ?>
Number->format($tag->id) ?>title) ?>created) ?>modified) ?> 29 | Html->link(__('View'), ['action' => 'view', $tag->id]) ?> 30 | Html->link(__('Edit'), ['action' => 'edit', $tag->id]) ?> 31 | Form->postLink(__('Delete'), ['action' => 'delete', $tag->id], ['confirm' => __('Are you sure you want to delete # {0}?', $tag->id)]) ?> 32 |
37 |
38 |
39 |
    40 | Paginator->first('<< ' . __('first')) ?> 41 | Paginator->prev('< ' . __('previous')) ?> 42 | Paginator->numbers() ?> 43 | Paginator->next(__('next') . ' >') ?> 44 | Paginator->last(__('last') . ' >>') ?> 45 |
46 |

Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?>

47 |
48 |
49 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakephp/app", 3 | "description": "CakePHP skeleton app", 4 | "homepage": "https://cakephp.org", 5 | "type": "project", 6 | "license": "MIT", 7 | "require": { 8 | "php": ">=8.1", 9 | "cakephp/cakephp": "^5.0", 10 | "cakephp/migrations": "~4.0", 11 | "cakephp/authorization": "~3.0", 12 | "cakephp/authentication": "~3.0", 13 | "cakephp/plugin-installer": "~2.0", 14 | "josegonzalez/dotenv": "2.*" 15 | }, 16 | "require-dev": { 17 | "psy/psysh": "@stable", 18 | "cakephp/debug_kit": "^5.0", 19 | "cakephp/bake": "^3.0", 20 | "cakephp/repl": "^2.0", 21 | "cakephp/cakephp-codesniffer": "^5.0" 22 | }, 23 | "suggest": { 24 | "markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.", 25 | "dereuromark/cakephp-ide-helper": "After baking your code, this keeps your annotations in sync with the code evolving from there on for maximum IDE and PHPStan compatibility.", 26 | "phpunit/phpunit": "Allows automated tests to be run without system-wide install.", 27 | "cakephp/cakephp-codesniffer": "Allows to check the code against the coding standards used in CakePHP." 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "App\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "App\\Test\\": "tests", 37 | "Cake\\Test\\": "./vendor/cakephp/cakephp/tests" 38 | } 39 | }, 40 | "scripts": { 41 | "post-install-cmd": "App\\Console\\Installer::postInstall", 42 | "post-create-project-cmd": "App\\Console\\Installer::postInstall", 43 | "check": [ 44 | "@test", 45 | "@cs-check" 46 | ], 47 | "cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/", 48 | "cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/", 49 | "stan": "phpstan analyse src/", 50 | "test": "phpunit --colors=always" 51 | }, 52 | "minimum-stability": "dev", 53 | "prefer-stable": true, 54 | "config": { 55 | "allow-plugins": { 56 | "dealerdirect/phpcodesniffer-composer-installer": true, 57 | "cakephp/plugin-installer": true 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /config/routes.php: -------------------------------------------------------------------------------- 1 | scope('/', function (RouteBuilder $routes) { 48 | // Register scoped middleware for in scopes. 49 | $routes->registerMiddleware('csrf', new CsrfProtectionMiddleware([ 50 | 'httponly' => true 51 | ])); 52 | $routes->applyMiddleware('csrf'); 53 | $routes->connect('/', ['controller' => 'Articles', 'action' => 'index']); 54 | $routes->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']); 55 | 56 | 57 | // New route we're adding for our tagged action. 58 | // The trailing `*` tells CakePHP that this action has 59 | // passed parameters. 60 | $routes->scope('/articles', function ($routes) { 61 | $routes->connect('/tagged/*', ['controller' => 'Articles', 'action' => 'tags']); 62 | }); 63 | 64 | /** 65 | * Connect catchall routes for all controllers. 66 | */ 67 | $routes->fallbacks(DashedRoute::class); 68 | }); 69 | -------------------------------------------------------------------------------- /config/app_local.example.php: -------------------------------------------------------------------------------- 1 | filter_var(env('DEBUG', true), FILTER_VALIDATE_BOOLEAN), 22 | 23 | /* 24 | * Security and encryption configuration 25 | * 26 | * - salt - A random string used in security hashing methods. 27 | * The salt value is also used as the encryption key. 28 | * You should treat it as extremely sensitive data. 29 | */ 30 | 'Security' => [ 31 | 'salt' => env('SECURITY_SALT', '__SALT__'), 32 | ], 33 | 34 | /* 35 | * Connection information used by the ORM to connect 36 | * to your application's datastores. 37 | * 38 | * See app.php for more configuration options. 39 | */ 40 | 'Datasources' => [ 41 | 'default' => [ 42 | 'driver' => Cake\Database\Driver\Sqlite::class, 43 | 'database' => ROOT . DS . 'cms-tutorial-database.sqlite', 44 | 'encoding' => 'utf8', 45 | 'url' => env('DATABASE_URL'), 46 | ], 47 | 48 | /* 49 | * The test connection is used during the test suite. 50 | */ 51 | 'test' => [ 52 | 'host' => 'localhost', 53 | //'port' => 'non_standard_port_number', 54 | 'username' => 'my_app', 55 | 'password' => 'secret', 56 | 'database' => 'test_myapp', 57 | //'schema' => 'myapp', 58 | 'url' => env('DATABASE_TEST_URL', 'sqlite://127.0.0.1/tmp/tests.sqlite'), 59 | ], 60 | ], 61 | 62 | /* 63 | * Email configuration. 64 | * 65 | * Host and credential configuration in case you are using SmtpTransport 66 | * 67 | * See app.php for more configuration options. 68 | */ 69 | 'EmailTransport' => [ 70 | 'default' => [ 71 | 'host' => 'localhost', 72 | 'port' => 25, 73 | 'username' => null, 74 | 'password' => null, 75 | 'client' => null, 76 | 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null), 77 | ], 78 | ], 79 | ]; 80 | -------------------------------------------------------------------------------- /src/Controller/PagesController.php: -------------------------------------------------------------------------------- 1 | Authorization->skipAuthorization(); 38 | } 39 | 40 | /** 41 | * Displays a view 42 | * 43 | * @param array ...$path Path segments. 44 | * @return \Cake\Http\Response|null 45 | * @throws \Cake\Network\Exception\ForbiddenException When a directory traversal attempt. 46 | * @throws \Cake\Network\Exception\NotFoundException When the view file could not 47 | * be found or \Cake\View\Exception\MissingTemplateException in debug mode. 48 | */ 49 | public function display(...$path) 50 | { 51 | $count = count($path); 52 | if (!$count) { 53 | return $this->redirect('/'); 54 | } 55 | if (in_array('..', $path, true) || in_array('.', $path, true)) { 56 | throw new ForbiddenException(); 57 | } 58 | $page = $subpage = null; 59 | 60 | if (!empty($path[0])) { 61 | $page = $path[0]; 62 | } 63 | if (!empty($path[1])) { 64 | $subpage = $path[1]; 65 | } 66 | $this->set(compact('page', 'subpage')); 67 | 68 | try { 69 | $this->render(implode('/', $path)); 70 | } catch (MissingTemplateException $exception) { 71 | if (Configure::read('debug')) { 72 | throw $exception; 73 | } 74 | throw new NotFoundException(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /config/paths.php: -------------------------------------------------------------------------------- 1 | 7 | 15 |
16 |

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 |
Paginator->sort('id') ?>Paginator->sort('email') ?>Paginator->sort('password') ?>Paginator->sort('created') ?>Paginator->sort('modified') ?>
Number->format($user->id) ?>email) ?>password) ?>created) ?>modified) ?> 37 | Html->link(__('View'), ['action' => 'view', $user->id]) ?> 38 | Html->link(__('Edit'), ['action' => 'edit', $user->id]) ?> 39 | Form->postLink(__('Delete'), ['action' => 'delete', $user->id], ['confirm' => __('Are you sure you want to delete # {0}?', $user->id)]) ?> 40 |
45 |
46 |
    47 | Paginator->first('<< ' . __('first')) ?> 48 | Paginator->prev('< ' . __('previous')) ?> 49 | Paginator->numbers() ?> 50 | Paginator->next(__('next') . ' >') ?> 51 | Paginator->last(__('last') . ' >>') ?> 52 |
53 |

Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?>

54 |
55 |
56 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/PagesControllerTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 38 | $this->assertResponseOk(); 39 | $this->get('/'); 40 | $this->assertResponseOk(); 41 | } 42 | 43 | /** 44 | * testDisplay method 45 | * 46 | * @return void 47 | */ 48 | public function testDisplay() 49 | { 50 | $this->get('/pages/home'); 51 | $this->assertResponseOk(); 52 | $this->assertResponseContains('CakePHP'); 53 | $this->assertResponseContains(''); 54 | } 55 | 56 | /** 57 | * Test that missing template renders 404 page in production 58 | * 59 | * @return void 60 | */ 61 | public function testMissingTemplate() 62 | { 63 | Configure::write('debug', false); 64 | $this->get('/pages/not_existing'); 65 | 66 | $this->assertResponseError(); 67 | $this->assertResponseContains('Error'); 68 | } 69 | 70 | /** 71 | * Test that missing template in debug mode renders missing_template error page 72 | * 73 | * @return void 74 | */ 75 | public function testMissingTemplateInDebug() 76 | { 77 | Configure::write('debug', true); 78 | $this->get('/pages/not_existing'); 79 | 80 | $this->assertResponseFailure(); 81 | $this->assertResponseContains('Missing Template'); 82 | $this->assertResponseContains('Stacktrace'); 83 | $this->assertResponseContains('not_existing.php'); 84 | } 85 | 86 | /** 87 | * Test directory traversal protection 88 | * 89 | * @return void 90 | */ 91 | public function testDirectoryTraversalProtection() 92 | { 93 | $this->get('/pages/../Layout/ajax'); 94 | $this->assertResponseCode(403); 95 | $this->assertResponseContains('Forbidden'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Model/Table/ArticlesTagsTable.php: -------------------------------------------------------------------------------- 1 | setTable('articles_tags'); 44 | $this->setDisplayField('article_id'); 45 | $this->setPrimaryKey(['article_id', 'tag_id']); 46 | 47 | $this->belongsTo('Articles', [ 48 | 'foreignKey' => 'article_id', 49 | 'joinType' => 'INNER', 50 | ]); 51 | $this->belongsTo('Tags', [ 52 | 'foreignKey' => 'tag_id', 53 | 'joinType' => 'INNER', 54 | ]); 55 | } 56 | 57 | /** 58 | * Returns a rules checker object that will be used for validating 59 | * application integrity. 60 | * 61 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 62 | * @return \Cake\ORM\RulesChecker 63 | */ 64 | public function buildRules(RulesChecker $rules): RulesChecker 65 | { 66 | $rules->add($rules->existsIn(['article_id'], 'Articles'), ['errorField' => 'article_id']); 67 | $rules->add($rules->existsIn(['tag_id'], 'Tags'), ['errorField' => 'tag_id']); 68 | 69 | return $rules; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Fixture/ArticlesFixture.php: -------------------------------------------------------------------------------- 1 | ['type' => 'integer', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null], 21 | 'user_id' => ['type' => 'integer', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null], 22 | 'title' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'collate' => 'utf8mb4_general_ci', 'comment' => '', 'precision' => null], 23 | 'slug' => ['type' => 'string', 'length' => 191, 'null' => false, 'default' => null, 'collate' => 'utf8mb4_general_ci', 'comment' => '', 'precision' => null], 24 | 'body' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'collate' => 'utf8mb4_general_ci', 'comment' => '', 'precision' => null], 25 | 'published' => ['type' => 'boolean', 'length' => null, 'null' => true, 'default' => '0', 'comment' => '', 'precision' => null], 26 | 'created' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => true, 'default' => null, 'comment' => ''], 27 | 'modified' => ['type' => 'datetime', 'length' => null, 'precision' => null, 'null' => true, 'default' => null, 'comment' => ''], 28 | '_indexes' => [ 29 | 'user_key' => ['type' => 'index', 'columns' => ['user_id'], 'length' => []], 30 | ], 31 | '_constraints' => [ 32 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []], 33 | 'slug' => ['type' => 'unique', 'columns' => ['slug'], 'length' => []], 34 | 'articles_ibfk_1' => ['type' => 'foreign', 'columns' => ['user_id'], 'references' => ['users', 'id'], 'update' => 'restrict', 'delete' => 'restrict', 'length' => []], 35 | ], 36 | '_options' => [ 37 | 'engine' => 'InnoDB', 38 | 'collation' => 'utf8mb4_general_ci' 39 | ], 40 | ]; 41 | // phpcs:enable 42 | /** 43 | * Init method 44 | * 45 | * @return void 46 | */ 47 | public function init(): void 48 | { 49 | $this->records = [ 50 | [ 51 | 'id' => 1, 52 | 'user_id' => 1, 53 | 'title' => 'Lorem ipsum dolor sit amet', 54 | 'slug' => 'Lorem ipsum dolor sit amet', 55 | 'body' => 'Lorem ipsum dolor sit amet, aliquet feugiat. Convallis morbi fringilla gravida, phasellus feugiat dapibus velit nunc, pulvinar eget sollicitudin venenatis cum nullam, vivamus ut a sed, mollitia lectus. Nulla vestibulum massa neque ut et, id hendrerit sit, feugiat in taciti enim proin nibh, tempor dignissim, rhoncus duis vestibulum nunc mattis convallis.', 56 | 'published' => 1, 57 | 'created' => '2020-08-12 02:42:19', 58 | 'modified' => '2020-08-12 02:42:19', 59 | ], 60 | ]; 61 | parent::init(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Model/Table/TagsTable.php: -------------------------------------------------------------------------------- 1 | setTable('tags'); 45 | $this->setDisplayField('title'); 46 | $this->setPrimaryKey('id'); 47 | 48 | $this->addBehavior('Timestamp'); 49 | 50 | $this->belongsToMany('Articles', [ 51 | 'foreignKey' => 'tag_id', 52 | 'targetForeignKey' => 'article_id', 53 | 'joinTable' => 'articles_tags', 54 | ]); 55 | } 56 | 57 | /** 58 | * Default validation rules. 59 | * 60 | * @param \Cake\Validation\Validator $validator Validator instance. 61 | * @return \Cake\Validation\Validator 62 | */ 63 | public function validationDefault(Validator $validator): Validator 64 | { 65 | $validator 66 | ->integer('id') 67 | ->allowEmptyString('id', null, 'create'); 68 | 69 | $validator 70 | ->scalar('title') 71 | ->maxLength('title', 191) 72 | ->allowEmptyString('title') 73 | ->add('title', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']); 74 | 75 | return $validator; 76 | } 77 | 78 | /** 79 | * Returns a rules checker object that will be used for validating 80 | * application integrity. 81 | * 82 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 83 | * @return \Cake\ORM\RulesChecker 84 | */ 85 | public function buildRules(RulesChecker $rules): RulesChecker 86 | { 87 | $rules->add($rules->isUnique(['title']), ['errorField' => 'title']); 88 | 89 | return $rules; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Model/Table/UsersTable.php: -------------------------------------------------------------------------------- 1 | setTable('users'); 45 | $this->setDisplayField('id'); 46 | $this->setPrimaryKey('id'); 47 | 48 | $this->addBehavior('Timestamp'); 49 | 50 | $this->hasMany('Articles', [ 51 | 'foreignKey' => 'user_id', 52 | ]); 53 | } 54 | 55 | /** 56 | * Default validation rules. 57 | * 58 | * @param \Cake\Validation\Validator $validator Validator instance. 59 | * @return \Cake\Validation\Validator 60 | */ 61 | public function validationDefault(Validator $validator): Validator 62 | { 63 | $validator 64 | ->integer('id') 65 | ->allowEmptyString('id', null, 'create'); 66 | 67 | $validator 68 | ->email('email') 69 | ->requirePresence('email', 'create') 70 | ->notEmptyString('email'); 71 | 72 | $validator 73 | ->scalar('password') 74 | ->maxLength('password', 255) 75 | ->requirePresence('password', 'create') 76 | ->notEmptyString('password'); 77 | 78 | return $validator; 79 | } 80 | 81 | /** 82 | * Returns a rules checker object that will be used for validating 83 | * application integrity. 84 | * 85 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 86 | * @return \Cake\ORM\RulesChecker 87 | */ 88 | public function buildRules(RulesChecker $rules): RulesChecker 89 | { 90 | $rules->add($rules->isUnique(['email']), ['errorField' => 'email']); 91 | 92 | return $rules; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Controller/ArticlesController.php: -------------------------------------------------------------------------------- 1 | Authorization->skipAuthorization(); 13 | $articles = $this->paginate($this->Articles->find()); 14 | $this->set(compact('articles')); 15 | } 16 | 17 | public function view($slug) 18 | { 19 | $this->Authorization->skipAuthorization(); 20 | $article = $this->Articles 21 | ->findBySlug($slug) 22 | ->contain('Tags') 23 | ->firstOrFail(); 24 | $this->set(compact('article')); 25 | } 26 | 27 | public function add() 28 | { 29 | $article = $this->Articles->newEmptyEntity(); 30 | $this->Authorization->authorize($article); 31 | 32 | if ($this->request->is('post')) { 33 | $article = $this->Articles->patchEntity($article, $this->request->getData()); 34 | 35 | // Added: Set the user_id from the session. 36 | $article->user_id = $this->request->getAttribute('identity')->getIdentifier(); 37 | 38 | if ($this->Articles->save($article)) { 39 | $this->Flash->success(__('Your article has been saved.')); 40 | return $this->redirect(['action' => 'index']); 41 | } 42 | $this->Flash->error(__('Unable to add your article.')); 43 | } 44 | $this->set('article', $article); 45 | } 46 | 47 | public function edit($slug) 48 | { 49 | $article = $this->Articles 50 | ->findBySlug($slug) 51 | ->contain('Tags') // load associated Tags 52 | ->firstOrFail(); 53 | $this->Authorization->authorize($article); 54 | 55 | if ($this->request->is(['post', 'put'])) { 56 | $this->Articles->patchEntity($article, $this->request->getData(), [ 57 | // Added: Disable modification of user_id. 58 | 'accessibleFields' => ['user_id' => false] 59 | ]); 60 | if ($this->Articles->save($article)) { 61 | $this->Flash->success(__('Your article has been updated.')); 62 | return $this->redirect(['action' => 'index']); 63 | } 64 | $this->Flash->error(__('Unable to update your article.')); 65 | } 66 | 67 | // Get a list of tags. 68 | $tags = $this->Articles->Tags->find('list'); 69 | 70 | // Set article & tags to the view context 71 | $this->set('tags', $tags); 72 | $this->set('article', $article); 73 | } 74 | 75 | public function delete($slug) 76 | { 77 | $this->request->allowMethod(['post', 'delete']); 78 | 79 | $article = $this->Articles->findBySlug($slug)->firstOrFail(); 80 | $this->Authorization->authorize($article); 81 | 82 | if ($this->Articles->delete($article)) { 83 | $this->Flash->success(__('The {0} article has been deleted.', $article->title)); 84 | return $this->redirect(['action' => 'index']); 85 | } 86 | } 87 | 88 | public function tags(array $tags = []) 89 | { 90 | $this->Authorization->skipAuthorization(); 91 | 92 | // Use the ArticlesTable to find tagged articles. 93 | $articles = $this->Articles->find('tagged', tags: $tags); 94 | 95 | // Pass variables into the view template context. 96 | $this->set([ 97 | 'articles' => $articles, 98 | 'tags' => $tags 99 | ]); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /webroot/img/cake.logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | 12 | 13 | 16 | 21 | 22 | 24 | 25 | 29 | 32 | 33 | 35 | 36 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Controller/TagsController.php: -------------------------------------------------------------------------------- 1 | paginate($this->Tags); 22 | 23 | $this->set(compact('tags')); 24 | } 25 | 26 | /** 27 | * View method 28 | * 29 | * @param string|null $id Tag id. 30 | * @return \Cake\Http\Response|null|void Renders view 31 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 32 | */ 33 | public function view($id = null) 34 | { 35 | $tag = $this->Tags->get($id, contain: ['Articles']); 36 | 37 | $this->set(compact('tag')); 38 | } 39 | 40 | /** 41 | * Add method 42 | * 43 | * @return \Cake\Http\Response|null|void Redirects on successful add, renders view otherwise. 44 | */ 45 | public function add() 46 | { 47 | $tag = $this->Tags->newEmptyEntity(); 48 | if ($this->request->is('post')) { 49 | $tag = $this->Tags->patchEntity($tag, $this->request->getData()); 50 | if ($this->Tags->save($tag)) { 51 | $this->Flash->success(__('The tag has been saved.')); 52 | 53 | return $this->redirect(['action' => 'index']); 54 | } 55 | $this->Flash->error(__('The tag could not be saved. Please, try again.')); 56 | } 57 | $articles = $this->Tags->Articles->find('list', ['limit' => 200]); 58 | $this->set(compact('tag', 'articles')); 59 | } 60 | 61 | /** 62 | * Edit method 63 | * 64 | * @param string|null $id Tag id. 65 | * @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise. 66 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 67 | */ 68 | public function edit($id = null) 69 | { 70 | $tag = $this->Tags->get($id, contain: ['Articles']); 71 | if ($this->request->is(['patch', 'post', 'put'])) { 72 | $tag = $this->Tags->patchEntity($tag, $this->request->getData()); 73 | if ($this->Tags->save($tag)) { 74 | $this->Flash->success(__('The tag has been saved.')); 75 | 76 | return $this->redirect(['action' => 'index']); 77 | } 78 | $this->Flash->error(__('The tag could not be saved. Please, try again.')); 79 | } 80 | $articles = $this->Tags->Articles->find('list', ['limit' => 200]); 81 | $this->set(compact('tag', 'articles')); 82 | } 83 | 84 | /** 85 | * Delete method 86 | * 87 | * @param string|null $id Tag id. 88 | * @return \Cake\Http\Response|null|void Redirects to index. 89 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 90 | */ 91 | public function delete($id = null) 92 | { 93 | $this->request->allowMethod(['post', 'delete']); 94 | $tag = $this->Tags->get($id); 95 | if ($this->Tags->delete($tag)) { 96 | $this->Flash->success(__('The tag has been deleted.')); 97 | } else { 98 | $this->Flash->error(__('The tag could not be deleted. Please, try again.')); 99 | } 100 | 101 | return $this->redirect(['action' => 'index']); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /templates/Users/view.php: -------------------------------------------------------------------------------- 1 | 7 | 18 |
19 |

id) ?>

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
email) ?>
password) ?>
Number->format($user->id) ?>
created) ?>
modified) ?>
42 | 77 |
78 | -------------------------------------------------------------------------------- /templates/Tags/view.php: -------------------------------------------------------------------------------- 1 | 7 |
8 | 17 |
18 |
19 |

title) ?>

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
title) ?>
Number->format($tag->id) ?>
created) ?>
modified) ?>
38 | 75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /src/Controller/UsersController.php: -------------------------------------------------------------------------------- 1 | Authentication->addUnauthenticatedActions(['login', 'add']); 21 | } 22 | 23 | /** 24 | * Index method 25 | * 26 | * @return \Cake\Http\Response|void 27 | */ 28 | public function index() 29 | { 30 | $users = $this->paginate($this->Users); 31 | 32 | $this->set(compact('users')); 33 | $this->set('_serialize', ['users']); 34 | } 35 | 36 | public function login() { 37 | $this->Authorization->skipAuthorization(); 38 | 39 | $this->request->allowMethod(['get', 'post']); 40 | $result = $this->Authentication->getResult(); 41 | // regardless of POST or GET, redirect if user is logged in 42 | if ($result->isValid()) { 43 | // redirect to /articles after login success 44 | $redirect = $this->request->getQuery('redirect', [ 45 | 'controller' => 'Articles', 46 | 'action' => 'index', 47 | ]); 48 | 49 | return $this->redirect($redirect); 50 | } 51 | // display error if user submitted and authentication failed 52 | if ($this->request->is('post') && !$result->isValid()) { 53 | $this->Flash->error(__('Invalid username or password')); 54 | } 55 | } 56 | 57 | public function logout() 58 | { 59 | $this->Authorization->skipAuthorization(); 60 | 61 | $result = $this->Authentication->getResult(); 62 | // regardless of POST or GET, redirect if user is logged in 63 | if ($result->isValid()) { 64 | $this->Authentication->logout(); 65 | return $this->redirect(['controller' => 'Users', 'action' => 'login']); 66 | } 67 | } 68 | 69 | /** 70 | * View method 71 | * 72 | * @param string|null $id User id. 73 | * @return \Cake\Http\Response|void 74 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 75 | */ 76 | public function view($id = null) 77 | { 78 | $user = $this->Users->get($id, contain: ['Articles']); 79 | 80 | $this->set('user', $user); 81 | $this->set('_serialize', ['user']); 82 | } 83 | 84 | /** 85 | * Add method 86 | * 87 | * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise. 88 | */ 89 | public function add() 90 | { 91 | $this->Authorization->skipAuthorization(); 92 | 93 | $user = $this->Users->newEmptyEntity(); 94 | if ($this->request->is('post')) { 95 | $user = $this->Users->patchEntity($user, $this->request->getData()); 96 | if ($this->Users->save($user)) { 97 | $this->Flash->success(__('The user has been saved.')); 98 | 99 | return $this->redirect(['action' => 'login']); 100 | } 101 | $this->Flash->error(__('The user could not be saved. Please, try again.')); 102 | } 103 | $this->set(compact('user')); 104 | $this->set('_serialize', ['user']); 105 | } 106 | 107 | /** 108 | * Edit method 109 | * 110 | * @param string|null $id User id. 111 | * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. 112 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 113 | */ 114 | public function edit($id = null) 115 | { 116 | $user = $this->Users->get($id, contain: []); 117 | if ($this->request->is(['patch', 'post', 'put'])) { 118 | $user = $this->Users->patchEntity($user, $this->request->getData()); 119 | if ($this->Users->save($user)) { 120 | $this->Flash->success(__('The user has been saved.')); 121 | 122 | return $this->redirect(['action' => 'index']); 123 | } 124 | $this->Flash->error(__('The user could not be saved. Please, try again.')); 125 | } 126 | $this->set(compact('user')); 127 | $this->set('_serialize', ['user']); 128 | } 129 | 130 | /** 131 | * Delete method 132 | * 133 | * @param string|null $id User id. 134 | * @return \Cake\Http\Response|null Redirects to index. 135 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 136 | */ 137 | public function delete($id = null) 138 | { 139 | $this->request->allowMethod(['post', 'delete']); 140 | $user = $this->Users->get($id); 141 | if ($this->Users->delete($user)) { 142 | $this->Flash->success(__('The user has been deleted.')); 143 | } else { 144 | $this->Flash->error(__('The user could not be deleted. Please, try again.')); 145 | } 146 | 147 | return $this->redirect(['action' => 'index']); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /webroot/css/home.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'cakefont'; 3 | src: url('../font/cakedingbats-webfont.eot'); 4 | src: url('../font/cakedingbats-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('../font/cakedingbats-webfont.woff2') format('woff2'), 6 | url('../font/cakedingbats-webfont.woff') format('woff'), 7 | url('../font/cakedingbats-webfont.ttf') format('truetype'), 8 | url('../font/cakedingbats-webfont.svg#cake_dingbatsregular') format('svg'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | .home { 14 | font-family: 'Roboto', sans-serif; 15 | font-size: 14px; 16 | line-height: 27px; 17 | color: #404041; 18 | } 19 | 20 | a { 21 | color: #0071BC; 22 | -webkit-transition: all 0.2s; 23 | -moz-transition: all 0.2s; 24 | -ms-transition: all 0.2s; 25 | -o-transition: all 0.2s; 26 | transition: all 0.2s; 27 | } 28 | 29 | a:hover, a:active { 30 | color: #d33d44; 31 | -webkit-transition: all 0.2s; 32 | -moz-transition: all 0.2s; 33 | -ms-transition: all 0.2s; 34 | -o-transition: all 0.2s; 35 | transition: all 0.2s; 36 | } 37 | 38 | ul, ol, dl, p { 39 | font-size: 0.85rem; 40 | } 41 | 42 | p { 43 | line-height: 2; 44 | } 45 | 46 | header { 47 | height: auto; 48 | line-height: 1em; 49 | padding: 0; 50 | box-shadow: none; 51 | } 52 | 53 | header.row { 54 | margin-bottom: 30px; 55 | } 56 | 57 | header .header-image { 58 | text-align: center; 59 | padding: 64px 0; 60 | } 61 | 62 | header .header-title { 63 | padding: 0; 64 | display: block; 65 | background: #404041; 66 | text-align: center; 67 | } 68 | 69 | header .header-title h1 { 70 | font-family: 'Raleway', sans-serif; 71 | margin: 0; 72 | font-style: italic; 73 | font-size: 18px; 74 | font-weight: 500; 75 | padding: 18px 30px; 76 | color: #DEDED5; 77 | } 78 | 79 | header h1 { 80 | color: #fff; 81 | } 82 | 83 | h3, h4 { 84 | font-family: 'Roboto', sans-serif; 85 | font-size: 27px; 86 | line-height: 30px; 87 | font-weight: 300; 88 | -webkit-font-smoothing: antialiased; 89 | margin-top: 0; 90 | margin-bottom: 20px; 91 | } 92 | 93 | .more { 94 | color: #ffffff; 95 | background-color: #d33d44; 96 | padding: 15px; 97 | margin-top: 10px; 98 | } 99 | 100 | .row { 101 | max-width: 1000px; 102 | } 103 | 104 | .alert { 105 | background-color: #fff9e1; 106 | font-size: 12px; 107 | text-align: center; 108 | display: block; 109 | padding: 12px; 110 | border-bottom: 2px solid #ffcf06; 111 | } 112 | 113 | .alert { 114 | background-color: #fff9e1; 115 | font-size: 12px; 116 | display: block; 117 | padding: 12px; 118 | border-bottom: 2px solid #ffcf06; 119 | margin-bottom: 30px; 120 | color: #404041; 121 | } 122 | 123 | .alert p { 124 | margin: 0; 125 | font-size: 12px; 126 | line-height: 1.4; 127 | } 128 | 129 | .alert p:before { 130 | color: #ffcf06; 131 | content: "\0055"; 132 | font-family: 'cakefont', sans-serif; 133 | font-size: 21px; 134 | margin-left: -0.8em; 135 | width: 2.3em; 136 | -webkit-font-smoothing: antialiased; 137 | -moz-osx-font-smoothing: grayscale; 138 | padding: 0 10px 0 15px; 139 | vertical-align: -2px; 140 | } 141 | 142 | .alert ul { 143 | margin: 0; 144 | font-size: 12px; 145 | } 146 | 147 | .alert.url-rewriting { 148 | background-color: #F0F0F0; 149 | border-color: #cccccc; 150 | display: none; 151 | } 152 | 153 | .text-center { 154 | text-align: center; 155 | } 156 | 157 | ul { 158 | list-style-type: none; 159 | margin: 0 0 30px 0; 160 | } 161 | 162 | li { 163 | padding-left: 1.8em; 164 | } 165 | 166 | ul li ul, ul li ul li { 167 | margin: 0; 168 | padding: 0; 169 | } 170 | 171 | .bullet:before { 172 | font-family: 'cakefont', sans-serif; 173 | font-size: 18px; 174 | display: inline-block; 175 | margin-left: -1.3em; 176 | width: 1.2em; 177 | -webkit-font-smoothing: antialiased; 178 | -moz-osx-font-smoothing: grayscale; 179 | vertical-align: -1px; 180 | } 181 | 182 | .success:before { 183 | color: #88c671; 184 | content: "\0056"; 185 | } 186 | 187 | .problem:before { 188 | color: #d33d44; 189 | content: "\0057"; 190 | } 191 | 192 | .cutlery:before { 193 | color: #404041; 194 | content: "\0059"; 195 | } 196 | 197 | .book:before { 198 | color: #404041; 199 | content: "\0042"; 200 | width: 1.7em; 201 | } 202 | 203 | hr { 204 | border-bottom: 1px solid #e7e7e7; 205 | border-top: 0; 206 | margin-bottom: 35px; 207 | margin-left: 30px; 208 | margin-right: 30px; 209 | } 210 | 211 | 212 | .icon { 213 | color: #404041; 214 | font-style: normal; 215 | font-family: 'cakefont', sans-serif; 216 | -webkit-font-smoothing: antialiased; 217 | -moz-osx-font-smoothing: grayscale; 218 | } 219 | .icon.support { 220 | font-size: 60px; 221 | } 222 | .icon.docs { 223 | font-size: 57px; 224 | } 225 | .icon.training { 226 | font-size: 39px; 227 | } 228 | 229 | @media (min-width: 768px) { 230 | .columns { 231 | padding-left: 30px; 232 | padding-right: 30px; 233 | } 234 | } 235 | 236 | @media (min-width: 992px) { 237 | header.row { 238 | max-width: 940px; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | parse() 67 | // ->putenv() 68 | // ->toEnv() 69 | // ->toServer(); 70 | // } 71 | 72 | /* 73 | * Read configuration file and inject configuration into various 74 | * CakePHP classes. 75 | * 76 | * By default there is only one configuration file. It is often a good 77 | * idea to create multiple configuration files, and separate the configuration 78 | * that changes from configuration that does not. This makes deployment simpler. 79 | */ 80 | try { 81 | Configure::config('default', new PhpConfig()); 82 | Configure::load('app', 'default', false); 83 | } catch (\Exception $e) { 84 | exit($e->getMessage() . "\n"); 85 | } 86 | 87 | /* 88 | * Load an environment local configuration file to provide overrides to your configuration. 89 | * Notice: For security reasons app_local.php **should not** be included in your git repo. 90 | */ 91 | if (file_exists(CONFIG . 'app_local.php')) { 92 | Configure::load('app_local', 'default'); 93 | } 94 | 95 | /* 96 | * When debug = true the metadata cache should only last 97 | * for a short time. 98 | */ 99 | if (Configure::read('debug')) { 100 | Configure::write('Cache._cake_model_.duration', '+2 minutes'); 101 | Configure::write('Cache._cake_core_.duration', '+2 minutes'); 102 | // disable router cache during development 103 | Configure::write('Cache._cake_routes_.duration', '+2 seconds'); 104 | } 105 | 106 | /* 107 | * Set the default server timezone. Using UTC makes time calculations / conversions easier. 108 | * Check https://php.net/manual/en/timezones.php for list of valid timezone strings. 109 | */ 110 | date_default_timezone_set(Configure::read('App.defaultTimezone')); 111 | 112 | /* 113 | * Configure the mbstring extension to use the correct encoding. 114 | */ 115 | mb_internal_encoding(Configure::read('App.encoding')); 116 | 117 | /* 118 | * Set the default locale. This controls how dates, number and currency is 119 | * formatted and sets the default language to use for translations. 120 | */ 121 | ini_set('intl.default_locale', Configure::read('App.defaultLocale')); 122 | 123 | /* 124 | * Register application error and exception handlers. 125 | */ 126 | (new ErrorTrap())->register(); 127 | (new ExceptionTrap())->register(); 128 | 129 | /* 130 | * Include the CLI bootstrap overrides. 131 | */ 132 | if (PHP_SAPI == 'cli') { 133 | require __DIR__ . '/bootstrap_cli.php'; 134 | } 135 | 136 | /* 137 | * Set the full base URL. 138 | * This URL is used as the base of all absolute links. 139 | */ 140 | $fullBaseUrl = Configure::read('App.fullBaseUrl'); 141 | if (!$fullBaseUrl) { 142 | $s = null; 143 | if (env('HTTPS')) { 144 | $s = 's'; 145 | } 146 | 147 | $httpHost = env('HTTP_HOST'); 148 | if (isset($httpHost)) { 149 | $fullBaseUrl = 'http' . $s . '://' . $httpHost; 150 | } 151 | unset($httpHost, $s); 152 | } 153 | if ($fullBaseUrl) { 154 | Router::fullBaseUrl($fullBaseUrl); 155 | } 156 | unset($fullBaseUrl); 157 | 158 | Cache::setConfig(Configure::consume('Cache')); 159 | ConnectionManager::setConfig(Configure::consume('Datasources')); 160 | TransportFactory::setConfig(Configure::consume('EmailTransport')); 161 | Mailer::setConfig(Configure::consume('Email')); 162 | Log::setConfig(Configure::consume('Log')); 163 | Security::setSalt(Configure::consume('Security.salt')); 164 | 165 | /* 166 | * Setup detectors for mobile and tablet. 167 | */ 168 | ServerRequest::addDetector('mobile', function ($request) { 169 | $detector = new \Detection\MobileDetect(); 170 | 171 | return $detector->isMobile(); 172 | }); 173 | ServerRequest::addDetector('tablet', function ($request) { 174 | $detector = new \Detection\MobileDetect(); 175 | 176 | return $detector->isTablet(); 177 | }); 178 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | bootstrapCli(); 59 | } 60 | 61 | /* 62 | * Only try to load DebugKit in development mode 63 | * Debug Kit should not be installed on a production system 64 | */ 65 | if (Configure::read('debug')) { 66 | $this->addPlugin('DebugKit'); 67 | } 68 | $this->addPlugin('Authentication'); 69 | $this->addPlugin('Authorization'); 70 | } 71 | 72 | /** 73 | * Bootrapping for CLI application. 74 | * 75 | * That is when running commands. 76 | * 77 | * @return void 78 | */ 79 | protected function bootstrapCli(): void 80 | { 81 | try { 82 | $this->addPlugin('Bake'); 83 | } catch (MissingPluginException $e) { 84 | // Do not halt if the plugin is missing 85 | } 86 | 87 | $this->addPlugin('Migrations'); 88 | 89 | // Load more plugins here 90 | } 91 | 92 | /** 93 | * Setup the middleware queue your application will use. 94 | * 95 | * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to setup. 96 | * @return \Cake\Http\MiddlewareQueue The updated middleware queue. 97 | */ 98 | public function middleware($middlewareQueue): \Cake\Http\MiddlewareQueue 99 | { 100 | $middlewareQueue 101 | // Catch any exceptions in the lower layers, 102 | // and make an error page/response 103 | ->add(ErrorHandlerMiddleware::class) 104 | 105 | // Handle plugin/theme assets like CakePHP normally does. 106 | ->add(AssetMiddleware::class) 107 | 108 | // Add routing middleware. 109 | ->add(new RoutingMiddleware($this)) 110 | 111 | // add Authentication after RoutingMiddleware 112 | ->add(new AuthenticationMiddleware($this)) 113 | 114 | // Add authorization **after** authentication 115 | ->add(new AuthorizationMiddleware($this)); 116 | 117 | if (Configure::read('debug')) { 118 | Configure::write('DebugKit.ignoreAuthorization', true); 119 | } 120 | 121 | return $middlewareQueue; 122 | } 123 | 124 | public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface 125 | { 126 | $service = new AuthenticationService(); 127 | 128 | // Define where users should be redirected to when they are not authenticated 129 | $service->setConfig([ 130 | 'unauthenticatedRedirect' => [ 131 | 'prefix' => false, 132 | 'plugin' => false, 133 | 'controller' => 'Users', 134 | 'action' => 'login', 135 | ], 136 | 'queryParam' => 'redirect', 137 | ]); 138 | 139 | // Define identifiers 140 | $fields = [ 141 | AbstractIdentifier::CREDENTIAL_USERNAME => 'email', 142 | AbstractIdentifier::CREDENTIAL_PASSWORD => 'password' 143 | ]; 144 | $passwordIdentifier = [ 145 | 'Authentication.Password' => [ 146 | 'fields' => $fields, 147 | ], 148 | ]; 149 | 150 | // Load the authenticators. Session should be first. 151 | $service->loadAuthenticator('Authentication.Session', [ 152 | 'identifier' => $passwordIdentifier, 153 | ]); 154 | $service->loadAuthenticator('Authentication.Form', [ 155 | 'identifier' => $passwordIdentifier, 156 | 'fields' => $fields, 157 | 'loginUrl' => Router::url([ 158 | 'prefix' => false, 159 | 'plugin' => null, 160 | 'controller' => 'Users', 161 | 'action' => 'login', 162 | ]), 163 | ]); 164 | 165 | return $service; 166 | } 167 | 168 | public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface 169 | { 170 | $resolver = new OrmResolver(); 171 | 172 | return new AuthorizationService($resolver); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /config/Migrations/20221126022125_InitialSchema.php: -------------------------------------------------------------------------------- 1 | table('articles') 18 | ->addColumn('user_id', 'integer', [ 19 | 'default' => null, 20 | 'limit' => null, 21 | 'null' => false, 22 | 'signed' => false, 23 | ]) 24 | ->addColumn('title', 'string', [ 25 | 'default' => null, 26 | 'limit' => 255, 27 | 'null' => false, 28 | ]) 29 | ->addColumn('slug', 'string', [ 30 | 'default' => null, 31 | 'limit' => 191, 32 | 'null' => false, 33 | ]) 34 | ->addColumn('body', 'text', [ 35 | 'default' => null, 36 | 'limit' => null, 37 | 'null' => true, 38 | ]) 39 | ->addColumn('published', 'boolean', [ 40 | 'default' => false, 41 | 'limit' => null, 42 | 'null' => true, 43 | ]) 44 | ->addColumn('created', 'datetime', [ 45 | 'default' => null, 46 | 'limit' => null, 47 | 'null' => true, 48 | ]) 49 | ->addColumn('modified', 'datetime', [ 50 | 'default' => null, 51 | 'limit' => null, 52 | 'null' => true, 53 | ]) 54 | ->addIndex( 55 | [ 56 | 'slug', 57 | ], 58 | ['unique' => true] 59 | ) 60 | ->addIndex( 61 | [ 62 | 'user_id', 63 | ] 64 | ) 65 | ->create(); 66 | 67 | $this->table('articles_tags', ['id' => false, 'primary_key' => ['article_id', 'tag_id']]) 68 | ->addColumn('article_id', 'integer', [ 69 | 'default' => null, 70 | 'limit' => null, 71 | 'null' => false, 72 | 'signed' => false, 73 | ]) 74 | ->addColumn('tag_id', 'integer', [ 75 | 'default' => null, 76 | 'limit' => null, 77 | 'null' => false, 78 | 'signed' => false, 79 | ]) 80 | ->addIndex( 81 | [ 82 | 'tag_id', 83 | ] 84 | ) 85 | ->addIndex( 86 | [ 87 | 'article_id', 88 | ] 89 | ) 90 | ->create(); 91 | 92 | $this->table('tags') 93 | ->addColumn('title', 'string', [ 94 | 'default' => null, 95 | 'limit' => 191, 96 | 'null' => true, 97 | ]) 98 | ->addColumn('created', 'datetime', [ 99 | 'default' => null, 100 | 'limit' => null, 101 | 'null' => true, 102 | ]) 103 | ->addColumn('modified', 'datetime', [ 104 | 'default' => null, 105 | 'limit' => null, 106 | 'null' => true, 107 | ]) 108 | ->addIndex( 109 | [ 110 | 'title', 111 | ], 112 | ['unique' => true] 113 | ) 114 | ->create(); 115 | 116 | $this->table('users') 117 | ->addColumn('email', 'string', [ 118 | 'default' => null, 119 | 'limit' => 255, 120 | 'null' => false, 121 | ]) 122 | ->addColumn('password', 'string', [ 123 | 'default' => null, 124 | 'limit' => 255, 125 | 'null' => false, 126 | ]) 127 | ->addColumn('created', 'datetime', [ 128 | 'default' => null, 129 | 'limit' => null, 130 | 'null' => true, 131 | ]) 132 | ->addColumn('modified', 'datetime', [ 133 | 'default' => null, 134 | 'limit' => null, 135 | 'null' => true, 136 | ]) 137 | ->create(); 138 | 139 | $this->table('articles') 140 | ->addForeignKey( 141 | 'user_id', 142 | 'users', 143 | 'id', 144 | [ 145 | 'update' => 'NO_ACTION', 146 | 'delete' => 'NO_ACTION', 147 | ] 148 | ) 149 | ->update(); 150 | 151 | $this->table('articles_tags') 152 | ->addForeignKey( 153 | 'tag_id', 154 | 'tags', 155 | 'id', 156 | [ 157 | 'update' => 'NO_ACTION', 158 | 'delete' => 'NO_ACTION', 159 | ] 160 | ) 161 | ->addForeignKey( 162 | 'article_id', 163 | 'articles', 164 | 'id', 165 | [ 166 | 'update' => 'NO_ACTION', 167 | 'delete' => 'NO_ACTION', 168 | ] 169 | ) 170 | ->update(); 171 | } 172 | 173 | /** 174 | * Down Method. 175 | * 176 | * More information on this method is available here: 177 | * https://book.cakephp.org/phinx/0/en/migrations.html#the-down-method 178 | * @return void 179 | */ 180 | public function down() 181 | { 182 | $this->table('articles') 183 | ->dropForeignKey( 184 | 'user_id' 185 | )->save(); 186 | 187 | $this->table('articles_tags') 188 | ->dropForeignKey( 189 | 'tag_id' 190 | ) 191 | ->dropForeignKey( 192 | 'article_id' 193 | )->save(); 194 | 195 | $this->table('articles')->drop()->save(); 196 | $this->table('articles_tags')->drop()->save(); 197 | $this->table('tags')->drop()->save(); 198 | $this->table('users')->drop()->save(); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Model/Table/ArticlesTable.php: -------------------------------------------------------------------------------- 1 | setTable('articles'); 46 | $this->setDisplayField('title'); 47 | $this->setPrimaryKey('id'); 48 | 49 | $this->addBehavior('Timestamp'); 50 | 51 | $this->belongsTo('Users', [ 52 | 'foreignKey' => 'user_id', 53 | 'joinType' => 'INNER', 54 | ]); 55 | $this->belongsToMany('Tags', [ 56 | 'foreignKey' => 'article_id', 57 | 'targetForeignKey' => 'tag_id', 58 | 'joinTable' => 'articles_tags', 59 | ]); 60 | } 61 | 62 | /** 63 | * Default validation rules. 64 | * 65 | * @param \Cake\Validation\Validator $validator Validator instance. 66 | * @return \Cake\Validation\Validator 67 | */ 68 | public function validationDefault(Validator $validator): Validator 69 | { 70 | $validator 71 | ->integer('id') 72 | ->allowEmptyString('id', null, 'create'); 73 | 74 | $validator 75 | ->scalar('title') 76 | ->maxLength('title', 255) 77 | ->requirePresence('title', 'create') 78 | ->notEmptyString('title'); 79 | 80 | $validator 81 | ->scalar('slug') 82 | ->maxLength('slug', 191) 83 | ->requirePresence('slug', 'create') 84 | ->notEmptyString('slug') 85 | ->add('slug', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']); 86 | 87 | $validator 88 | ->scalar('body') 89 | ->allowEmptyString('body'); 90 | 91 | $validator 92 | ->boolean('published') 93 | ->allowEmptyString('published'); 94 | 95 | return $validator; 96 | } 97 | 98 | /** 99 | * Returns a rules checker object that will be used for validating 100 | * application integrity. 101 | * 102 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 103 | * @return \Cake\ORM\RulesChecker 104 | */ 105 | public function buildRules(RulesChecker $rules): RulesChecker 106 | { 107 | $rules->add($rules->isUnique(['slug']), ['errorField' => 'slug']); 108 | $rules->add($rules->existsIn(['user_id'], 'Users'), ['errorField' => 'user_id']); 109 | 110 | return $rules; 111 | } 112 | 113 | public function beforeSave(EventInterface $event, $entity, ArrayObject $options) 114 | { 115 | if ($entity->tag_string) { 116 | $entity->tags = $this->_buildTags($entity->tag_string); 117 | } 118 | 119 | // Other code 120 | } 121 | 122 | protected function _buildTags($tagString) 123 | { 124 | // Trim tags 125 | $newTags = array_map('trim', explode(',', $tagString)); 126 | // Remove all empty tags 127 | $newTags = array_filter($newTags); 128 | // Reduce duplicated tags 129 | $newTags = array_unique($newTags); 130 | 131 | $out = []; 132 | $tags = $this->Tags->find() 133 | ->where(['Tags.title IN' => $newTags]) 134 | ->all(); 135 | 136 | // Remove existing tags from the list of new tags. 137 | foreach ($tags->extract('title') as $existing) { 138 | $index = array_search($existing, $newTags); 139 | if ($index !== false) { 140 | unset($newTags[$index]); 141 | } 142 | } 143 | // Add existing tags. 144 | foreach ($tags as $tag) { 145 | $out[] = $tag; 146 | } 147 | // Add new tags. 148 | foreach ($newTags as $tag) { 149 | $out[] = $this->Tags->newEntity(['title' => $tag]); 150 | } 151 | return $out; 152 | } 153 | 154 | // The $query argument is a query builder instance. 155 | // The $options array will contain the 'tags' option we passed 156 | // to find('tagged') in our controller action. 157 | public function findTagged(SelectQuery $query, array $tags = []): SelectQuery 158 | { 159 | $columns = [ 160 | 'Articles.id', 'Articles.user_id', 'Articles.title', 161 | 'Articles.body', 'Articles.published', 'Articles.created', 162 | 'Articles.slug', 163 | ]; 164 | $query = $query 165 | ->select($columns) 166 | ->distinct($columns); 167 | 168 | if (empty($tags)) { 169 | // If there are no tags provided, find articles that have no tags. 170 | $query->leftJoinWith('Tags') 171 | ->where(['Tags.title IS' => null]); 172 | } else { 173 | // Find articles that have one or more of the provided tags. 174 | $query->innerJoinWith('Tags') 175 | ->where(['Tags.title IN' => $tags]); 176 | } 177 | 178 | return $query->groupBy(['Articles.id']); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Console/Installer.php: -------------------------------------------------------------------------------- 1 | getIO(); 57 | 58 | $rootDir = dirname(dirname(__DIR__)); 59 | 60 | static::createAppLocalConfig($rootDir, $io); 61 | static::createWritableDirectories($rootDir, $io); 62 | 63 | static::setFolderPermissions($rootDir, $io); 64 | static::setSecuritySalt($rootDir, $io); 65 | 66 | $class = 'Cake\Codeception\Console\Installer'; 67 | if (class_exists($class)) { 68 | $class::customizeCodeceptionBinary($event); 69 | } 70 | } 71 | 72 | /** 73 | * Create config/app_local.php file if it does not exist. 74 | * 75 | * @param string $dir The application's root directory. 76 | * @param \Composer\IO\IOInterface $io IO interface to write to console. 77 | * @return void 78 | */ 79 | public static function createAppLocalConfig($dir, $io) 80 | { 81 | $appLocalConfig = $dir . '/config/app_local.php'; 82 | $appLocalConfigTemplate = $dir . '/config/app_local.example.php'; 83 | if (!file_exists($appLocalConfig)) { 84 | copy($appLocalConfigTemplate, $appLocalConfig); 85 | $io->write('Created `config/app_local.php` file'); 86 | } 87 | } 88 | 89 | /** 90 | * Create the `logs` and `tmp` directories. 91 | * 92 | * @param string $dir The application's root directory. 93 | * @param \Composer\IO\IOInterface $io IO interface to write to console. 94 | * @return void 95 | */ 96 | public static function createWritableDirectories($dir, $io) 97 | { 98 | foreach (static::WRITABLE_DIRS as $path) { 99 | $path = $dir . '/' . $path; 100 | if (!file_exists($path)) { 101 | mkdir($path); 102 | $io->write('Created `' . $path . '` directory'); 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Set globally writable permissions on the "tmp" and "logs" directory. 109 | * 110 | * This is not the most secure default, but it gets people up and running quickly. 111 | * 112 | * @param string $dir The application's root directory. 113 | * @param \Composer\IO\IOInterface $io IO interface to write to console. 114 | * @return void 115 | */ 116 | public static function setFolderPermissions($dir, $io) 117 | { 118 | // ask if the permissions should be changed 119 | if ($io->isInteractive()) { 120 | $validator = function ($arg) { 121 | if (in_array($arg, ['Y', 'y', 'N', 'n'])) { 122 | return $arg; 123 | } 124 | throw new Exception('This is not a valid answer. Please choose Y or n.'); 125 | }; 126 | $setFolderPermissions = $io->askAndValidate( 127 | 'Set Folder Permissions ? (Default to Y) [Y,n]? ', 128 | $validator, 129 | 10, 130 | 'Y' 131 | ); 132 | 133 | if (in_array($setFolderPermissions, ['n', 'N'])) { 134 | return; 135 | } 136 | } 137 | 138 | // Change the permissions on a path and output the results. 139 | $changePerms = function ($path) use ($io) { 140 | $currentPerms = fileperms($path) & 0777; 141 | $worldWritable = $currentPerms | 0007; 142 | if ($worldWritable == $currentPerms) { 143 | return; 144 | } 145 | 146 | $res = chmod($path, $worldWritable); 147 | if ($res) { 148 | $io->write('Permissions set on ' . $path); 149 | } else { 150 | $io->write('Failed to set permissions on ' . $path); 151 | } 152 | }; 153 | 154 | $walker = function ($dir) use (&$walker, $changePerms) { 155 | $files = array_diff(scandir($dir), ['.', '..']); 156 | foreach ($files as $file) { 157 | $path = $dir . '/' . $file; 158 | 159 | if (!is_dir($path)) { 160 | continue; 161 | } 162 | 163 | $changePerms($path); 164 | $walker($path); 165 | } 166 | }; 167 | 168 | $walker($dir . '/tmp'); 169 | $changePerms($dir . '/tmp'); 170 | $changePerms($dir . '/logs'); 171 | } 172 | 173 | /** 174 | * Set the security.salt value in the application's config file. 175 | * 176 | * @param string $dir The application's root directory. 177 | * @param \Composer\IO\IOInterface $io IO interface to write to console. 178 | * @return void 179 | */ 180 | public static function setSecuritySalt($dir, $io) 181 | { 182 | $newKey = hash('sha256', Security::randomBytes(64)); 183 | static::setSecuritySaltInFile($dir, $io, $newKey, 'app_local.php'); 184 | } 185 | 186 | /** 187 | * Set the security.salt value in a given file 188 | * 189 | * @param string $dir The application's root directory. 190 | * @param \Composer\IO\IOInterface $io IO interface to write to console. 191 | * @param string $newKey key to set in the file 192 | * @param string $file A path to a file relative to the application's root 193 | * @return void 194 | */ 195 | public static function setSecuritySaltInFile($dir, $io, $newKey, $file) 196 | { 197 | $config = $dir . '/config/' . $file; 198 | $content = file_get_contents($config); 199 | 200 | $content = str_replace('__SALT__', $newKey, $content, $count); 201 | 202 | if ($count == 0) { 203 | $io->write('No Security.salt placeholder to replace.'); 204 | 205 | return; 206 | } 207 | 208 | $result = file_put_contents($config, $content); 209 | if ($result) { 210 | $io->write('Updated Security.salt value in config/' . $file); 211 | 212 | return; 213 | } 214 | $io->write('Unable to update Security.salt value.'); 215 | } 216 | 217 | /** 218 | * Set the APP_NAME value in a given file 219 | * 220 | * @param string $dir The application's root directory. 221 | * @param \Composer\IO\IOInterface $io IO interface to write to console. 222 | * @param string $appName app name to set in the file 223 | * @param string $file A path to a file relative to the application's root 224 | * @return void 225 | */ 226 | public static function setAppNameInFile($dir, $io, $appName, $file) 227 | { 228 | $config = $dir . '/config/' . $file; 229 | $content = file_get_contents($config); 230 | $content = str_replace('__APP_NAME__', $appName, $content, $count); 231 | 232 | if ($count == 0) { 233 | $io->write('No __APP_NAME__ placeholder to replace.'); 234 | 235 | return; 236 | } 237 | 238 | $result = file_put_contents($config, $content); 239 | if ($result) { 240 | $io->write('Updated __APP_NAME__ value in config/' . $file); 241 | 242 | return; 243 | } 244 | $io->write('Unable to update __APP_NAME__ value.'); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /webroot/css/cake.css: -------------------------------------------------------------------------------- 1 | .disabled a, 2 | a.disabled { 3 | pointer-events: none; 4 | } 5 | 6 | a:hover { 7 | color: #15848F; 8 | } 9 | 10 | a { 11 | color: #1798A5; 12 | } 13 | 14 | .side-nav li a:not(.button) { 15 | color: #15848F; 16 | } 17 | 18 | .side-nav li a:not(.button):hover { 19 | color: #15848F; 20 | } 21 | 22 | header { 23 | background-color: #D33C44; 24 | color: #ffffff; 25 | font-size: 30px; 26 | height: 84px; 27 | line-height: 64px; 28 | padding: 16px 0px; 29 | box-shadow: 0px 1px rgba(0, 0, 0, 0.24); 30 | } 31 | 32 | header .header-title { 33 | padding-left:80px 34 | } 35 | 36 | legend { 37 | color:#15848F; 38 | } 39 | 40 | .row { 41 | max-width: 80rem; 42 | } 43 | 44 | .actions.columns { 45 | margin-top:1rem; 46 | border-left: 5px solid #15848F; 47 | padding-left: 15px; 48 | padding: 32px 20px; 49 | } 50 | 51 | .actions.columns h3 { 52 | color:#15848F; 53 | } 54 | 55 | .related table { 56 | border: 0; 57 | width: 100%; 58 | table-layout: fixed; 59 | } 60 | 61 | .index table thead { 62 | height: 3.5rem; 63 | } 64 | 65 | .header-help { 66 | float: right; 67 | margin-right:2rem; 68 | margin-top: -80px; 69 | font-size:16px; 70 | } 71 | 72 | .header-help span { 73 | font-weight: normal; 74 | text-align: center; 75 | text-decoration: none; 76 | line-height: 1; 77 | white-space: nowrap; 78 | display: inline-block; 79 | padding: 0.25rem 0.5rem 0.375rem; 80 | font-size: 0.8rem; 81 | background-color: #0097a7; 82 | color: #FFF; 83 | border-radius: 1000px; 84 | } 85 | 86 | .header-help a { 87 | color: #fff; 88 | } 89 | 90 | ul.pagination li a { 91 | color: rgba(0, 0 ,0 , 0.54); 92 | } 93 | 94 | ul.pagination li.active a { 95 | background-color: #DCE47E; 96 | color: #FFF; 97 | font-weight: bold; 98 | cursor: default; 99 | } 100 | ul.pagination .disabled:hover a { 101 | background: none; 102 | } 103 | 104 | .paginator { 105 | text-align: center; 106 | } 107 | 108 | .paginator ul.pagination li { 109 | float: none; 110 | display: inline-block; 111 | } 112 | 113 | .paginator p { 114 | text-align: right; 115 | color: rgba(0, 0 ,0 , 0.54); 116 | } 117 | 118 | .asc:after { 119 | content: " \2193"; 120 | } 121 | .desc:after { 122 | content: " \2191"; 123 | } 124 | 125 | .form .error-message { 126 | display: block; 127 | padding: 0.375rem 0.5625rem 0.5625rem; 128 | margin-top: -1px; 129 | margin-bottom: 1rem; 130 | font-size: 0.75rem; 131 | font-weight: normal; 132 | font-style: italic; 133 | color: rgba(0, 0, 0, 0.54); 134 | } 135 | 136 | .required > label { 137 | font-weight: bold; 138 | } 139 | .required > label:after { 140 | content: ' *'; 141 | color: #C3232D; 142 | } 143 | 144 | select[multiple] { 145 | min-height:150px; 146 | background: none; 147 | } 148 | input[type=checkbox], 149 | input[type=radio] { 150 | margin-right: 0.5em; 151 | } 152 | 153 | .date select, 154 | .time select, 155 | .datetime select { 156 | display: inline; 157 | width: auto; 158 | margin-right: 10px; 159 | } 160 | 161 | .error label, 162 | .error label.error { 163 | color: #C3232D; 164 | } 165 | 166 | .view h2 { 167 | color: #6F6F6F; 168 | } 169 | 170 | .view .columns.strings { 171 | border-radius: 3px; 172 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 173 | margin-right:0.7rem; 174 | } 175 | 176 | .view .numbers { 177 | background-color: #B7E3EC; 178 | color: #FFF; 179 | border-radius: 3px; 180 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 181 | margin-right: 0.7rem; 182 | } 183 | 184 | .view .columns.dates { 185 | border-radius: 3px; 186 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 187 | margin-right:0.7rem; 188 | background-color:#DCE47E; 189 | color: #fff; 190 | } 191 | 192 | .view .columns.booleans { 193 | border-radius: 3px; 194 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 195 | margin-right:0.7rem; 196 | background-color: #8D6E65; 197 | color: #fff; 198 | } 199 | 200 | .view .strings p { 201 | border-bottom: 1px solid #eee; 202 | } 203 | .view .numbers .subheader, .view .dates .subheader { 204 | color:#747474; 205 | } 206 | .view .booleans .subheader { 207 | color: #E9E9E9 208 | } 209 | 210 | .view .texts .columns { 211 | margin-top:1.2rem; 212 | border-bottom: 1px solid #eee; 213 | } 214 | 215 | /** Notices and Errors **/ 216 | .cake-error, 217 | .cake-debug, 218 | .notice, 219 | p.error, 220 | p.notice { 221 | display: block; 222 | clear: both; 223 | background-repeat: repeat-x; 224 | margin-bottom: 18px; 225 | padding: 7px 14px; 226 | border-radius: 3px; 227 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 228 | } 229 | 230 | .cake-debug, 231 | .notice, 232 | p.notice { 233 | color: #000000; 234 | background: #ffcc00; 235 | } 236 | 237 | .cake-error, 238 | p.error { 239 | color: #fff; 240 | background: #C3232D; 241 | } 242 | 243 | pre { 244 | background: none repeat scroll 0% 0% #FFF; 245 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 246 | margin: 15px 0px; 247 | color: rgba(0, 0 ,0 , 0.74); 248 | padding:5px; 249 | } 250 | 251 | .cake-error .cake-stack-trace { 252 | margin-top:10px; 253 | } 254 | 255 | .cake-stack-trace code { 256 | background: inherit; 257 | border:0; 258 | } 259 | 260 | .cake-code-dump .code-highlight { 261 | display: block; 262 | background-color: #FFC600; 263 | } 264 | 265 | .cake-error a, 266 | .cake-error a:hover { 267 | color:#fff; 268 | text-decoration: underline; 269 | } 270 | 271 | .checks { 272 | padding:30px; 273 | color: #626262; 274 | background-color: #B7E3EC; 275 | border-radius: 3px; 276 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 277 | margin-bottom: 2em; 278 | } 279 | 280 | .checks h4 { 281 | margin-bottom: 1.5rem; 282 | } 283 | 284 | .checks hr { 285 | border: 0; 286 | height: 0; 287 | border-top: 1px solid rgba(0, 0, 0, 0.1); 288 | border-bottom: 1px solid rgba(255, 255, 255, 0.3); 289 | } 290 | 291 | .checks .success, 292 | .checks .problem { 293 | margin-left: 10px; 294 | } 295 | .checks .success:before, 296 | .checks .problem:before { 297 | line-height: 0px; 298 | font-size: 28px; 299 | height: 12px; 300 | width: 12px; 301 | border-radius: 15px; 302 | text-align: center; 303 | vertical-align: middle; 304 | display: inline-block; 305 | position: relative; 306 | left: -11px; 307 | } 308 | 309 | .checks .success:before { 310 | content: "✓"; 311 | color: green; 312 | margin-right: 9px; 313 | } 314 | 315 | .checks .problem:before { 316 | content: "✘"; 317 | color: red; 318 | margin-right: 9px; 319 | } 320 | 321 | .top-bar.expanded .title-area { 322 | background: #01545b; 323 | } 324 | 325 | .top-bar.expanded, .top-bar,.top-bar-section ul li,.top-bar-section li:not(.has-form) a:not(.button) { 326 | background: #116d76; 327 | } 328 | 329 | .top-bar-section li:not(.has-form) a:not(.button):hover { 330 | background-color: #308e97; 331 | background: #308e97; 332 | } 333 | 334 | .side-nav li.heading { 335 | color: #1798A5; 336 | font-size: 0.875rem; 337 | font-weight: bold; 338 | text-transform: uppercase; 339 | padding: 0.4375rem 0.875rem; 340 | } 341 | 342 | #actions-sidebar { 343 | background: #fafafa; 344 | } 345 | 346 | .index table { 347 | margin-top: 0rem; 348 | border: 0; 349 | width: 100%; 350 | table-layout: fixed; 351 | } 352 | 353 | table { 354 | background: #fff; 355 | margin-bottom: 1.25rem; 356 | border: none; 357 | table-layout: fixed; 358 | width: 100%; 359 | } 360 | 361 | table thead { 362 | background: none; 363 | } 364 | 365 | table tr { 366 | border-bottom: 1px solid #ebebec; 367 | } 368 | 369 | table thead tr { 370 | border-bottom: 1px solid #1798A5; 371 | } 372 | 373 | table tr th { 374 | padding: 0.5625rem 0.625rem; 375 | font-size: 0.875rem; 376 | color: #1798A5; 377 | text-align: left; 378 | border-bottom: 2px solid #1798A5; 379 | } 380 | 381 | table tr:nth-of-type(even) { 382 | background: none; 383 | } 384 | 385 | fieldset { 386 | border: none; 387 | padding: 1.25rem; 388 | margin: 1.125rem 0; 389 | } 390 | 391 | fieldset legend { 392 | border-bottom: 2px solid #1798A5; 393 | width: 100%; 394 | line-height: 2rem; 395 | } 396 | 397 | .form button[type="submit"] { 398 | float: right; 399 | text-transform: uppercase; 400 | box-shadow: none; 401 | } 402 | 403 | .form button:hover, .form button:focus { 404 | background: #BE840B; 405 | box-shadow: none; 406 | } 407 | 408 | button { 409 | background: #966600; 410 | } 411 | 412 | div.message { 413 | text-align: center; 414 | cursor: pointer; 415 | display: block; 416 | font-weight: normal; 417 | padding: 0 1.5rem 0 1.5rem; 418 | transition: height 300ms ease-out 0s; 419 | background-color: #a0d3e8; 420 | color: #626262; 421 | top: 15px; 422 | right: 15px; 423 | z-index: 999; 424 | overflow: hidden; 425 | height: 50px; 426 | line-height: 2.5em; 427 | box-radius: 5px; 428 | } 429 | 430 | div.message:before { 431 | line-height: 0px; 432 | font-size: 20px; 433 | height: 12px; 434 | width: 12px; 435 | border-radius: 15px; 436 | text-align: center; 437 | vertical-align: middle; 438 | display: inline-block; 439 | position: relative; 440 | left: -11px; 441 | background-color: #FFF; 442 | padding: 12px 14px 12px 10px; 443 | content: "i"; 444 | color: #a0d3e8; 445 | } 446 | 447 | div.message.error { 448 | background-color: #C3232D; 449 | color: #FFF; 450 | } 451 | 452 | div.message.error:before { 453 | padding: 11px 16px 14px 7px; 454 | color: #C3232D; 455 | content: "x"; 456 | } 457 | div.message.hidden { 458 | height: 0; 459 | } 460 | 461 | 462 | .vertical-table th { 463 | padding: 0.5625rem 0.625rem; 464 | font-size: 0.875rem; 465 | color: #1798A5; 466 | border: none; 467 | text-align: left; 468 | } 469 | 470 | .vertical-table { 471 | vertical-align: middle; 472 | } 473 | 474 | .vertical-table td { 475 | text-align: right; 476 | } 477 | 478 | .content { 479 | padding: 2rem; 480 | } 481 | 482 | /* Use 'one true layout' methods to get equal height columns */ 483 | .container { 484 | overflow: hidden; 485 | min-height: 92%; /* full height almost always */ 486 | } 487 | 488 | /* Force equal height by overflowing */ 489 | .content, 490 | #actions-sidebar { 491 | margin-bottom: -99999px; 492 | padding-bottom: 99999px; 493 | } 494 | @media(max-width: 640px) { 495 | #actions-sidebar { 496 | padding-bottom: 2rem; 497 | margin-bottom: 0; 498 | } 499 | } 500 | 501 | .content h3 { 502 | color: #be140b; 503 | padding-bottom: 0.5rem; 504 | margin-bottom: 20px; 505 | } 506 | 507 | .content h4 { 508 | color: #be140b; 509 | padding-bottom: 0.5rem; 510 | margin-bottom: 20px; 511 | border-bottom: 2px solid #be140b; 512 | } 513 | 514 | .content .related h4 { 515 | color: #4d8f97; 516 | padding-bottom: 0.5rem; 517 | margin-top: 20px; 518 | margin-bottom: 10px; 519 | border-bottom: 0px; 520 | } 521 | 522 | table td { 523 | vertical-align: top; 524 | word-break: break-all; 525 | } 526 | -------------------------------------------------------------------------------- /templates/Pages/home.php: -------------------------------------------------------------------------------- 1 | disableAutoLayout(); 25 | 26 | $checkConnection = function (string $name) { 27 | $error = null; 28 | $connected = false; 29 | try { 30 | ConnectionManager::get($name)->getDriver()->connect(); 31 | // No exception means success 32 | $connected = true; 33 | } catch (Exception $connectionError) { 34 | $error = $connectionError->getMessage(); 35 | if (method_exists($connectionError, 'getAttributes')) { 36 | $attributes = $connectionError->getAttributes(); 37 | if (isset($attributes['message'])) { 38 | $error .= '
' . $attributes['message']; 39 | } 40 | } 41 | if ($name === 'debug_kit') { 42 | $error = 'Try adding your current top level domain to the 43 | DebugKit.safeTld 44 | config and reload.'; 45 | if (!in_array('sqlite', \PDO::getAvailableDrivers())) { 46 | $error .= '
You need to install the PHP extension pdo_sqlite so DebugKit can work properly.'; 47 | } 48 | } 49 | } 50 | 51 | return compact('connected', 'error'); 52 | }; 53 | 54 | if (!Configure::read('debug')) : 55 | throw new NotFoundException( 56 | 'Please replace templates/Pages/home.php with your own version or re-enable debug mode.' 57 | ); 58 | endif; 59 | 60 | ?> 61 | 62 | 63 | 64 | Html->charset() ?> 65 | 66 | 67 | CakePHP: the rapid development PHP framework: 68 | <?= $this->fetch('title') ?> 69 | 70 | Html->meta('icon') ?> 71 | 72 | Html->css(['normalize.min', 'milligram.min', 'fonts', 'cake', 'home']) ?> 73 | 74 | fetch('meta') ?> 75 | fetch('css') ?> 76 | fetch('script') ?> 77 | 78 | 79 |
80 |
81 | 82 | CakePHP 83 | 84 |

85 | Welcome to CakePHP Chiffon (🍰) 86 |

87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | Please be aware that this page will not be shown if you turn off debug mode unless you replace templates/Pages/home.php with your own version. 96 |
97 |
98 | 105 |
106 | 107 |
108 |
109 |
110 |
111 |

Environment

112 |
    113 | =')) : ?> 114 |
  • Your version of PHP is 8.1.0 or higher (detected ).
  • 115 | 116 |
  • Your version of PHP is too low. You need PHP 8.1.0 or higher to use CakePHP (detected ).
  • 117 | 118 | 119 | 120 |
  • Your version of PHP has the mbstring extension loaded.
  • 121 | 122 |
  • Your version of PHP does NOT have the mbstring extension loaded.
  • 123 | 124 | 125 | 126 |
  • Your version of PHP has the openssl extension loaded.
  • 127 | 128 |
  • Your version of PHP does NOT have the openssl extension loaded.
  • 129 | 130 | 131 | 132 |
  • Your version of PHP has the intl extension loaded.
  • 133 | 134 |
  • Your version of PHP does NOT have the intl extension loaded.
  • 135 | 136 | 137 | 138 |
  • You should set zend.assertions to 1 in your php.ini for your development environment.
  • 139 | 140 |
141 |
142 |
143 |

Filesystem

144 |
    145 | 146 |
  • Your tmp directory is writable.
  • 147 | 148 |
  • Your tmp directory is NOT writable.
  • 149 | 150 | 151 | 152 |
  • Your logs directory is writable.
  • 153 | 154 |
  • Your logs directory is NOT writable.
  • 155 | 156 | 157 | 158 | 159 |
  • The is being used for core caching. To change the config edit config/app.php
  • 160 | 161 |
  • Your cache is NOT working. Please check the settings in config/app.php
  • 162 | 163 |
164 |
165 |
166 |
167 |
168 |
169 |

Database

170 | 173 |
    174 | 175 |
  • CakePHP is able to connect to the database.
  • 176 | 177 |
  • CakePHP is NOT able to connect to the database.
  • 178 | 179 |
180 |
181 |
182 |

DebugKit

183 |
    184 | 185 |
  • DebugKit is loaded.
  • 186 | 189 | 190 |
  • DebugKit can connect to the database.
  • 191 | 192 |
  • There are configuration problems present which need to be fixed:
  • 193 | 194 | 195 |
  • DebugKit is not loaded.
  • 196 | 197 |
198 |
199 |
200 |
201 |
202 | 207 |
208 |
209 |
210 | 216 |
217 |
218 |
219 | 229 |
230 |
231 |
232 | 237 |
238 |
239 |
240 |
241 | 242 | 243 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | filter_var(env('DEBUG', false), FILTER_VALIDATE_BOOLEAN), 22 | 23 | /* 24 | * Configure basic information about the application. 25 | * 26 | * - namespace - The namespace to find app classes under. 27 | * - defaultLocale - The default locale for translation, formatting currencies and numbers, date and time. 28 | * - encoding - The encoding used for HTML + database connections. 29 | * - base - The base directory the app resides in. If false this 30 | * will be auto-detected. 31 | * - dir - Name of app directory. 32 | * - webroot - The webroot directory. 33 | * - wwwRoot - The file path to webroot. 34 | * - baseUrl - To configure CakePHP to *not* use mod_rewrite and to 35 | * use CakePHP pretty URLs, remove these .htaccess 36 | * files: 37 | * /.htaccess 38 | * /webroot/.htaccess 39 | * And uncomment the baseUrl key below. 40 | * - fullBaseUrl - A base URL to use for absolute links. When set to false (default) 41 | * CakePHP generates required value based on `HTTP_HOST` environment variable. 42 | * However, you can define it manually to optimize performance or if you 43 | * are concerned about people manipulating the `Host` header. 44 | * - imageBaseUrl - Web path to the public images/ directory under webroot. 45 | * - cssBaseUrl - Web path to the public css/ directory under webroot. 46 | * - jsBaseUrl - Web path to the public js/ directory under webroot. 47 | * - paths - Configure paths for non class-based resources. Supports the 48 | * `plugins`, `templates`, `locales` subkeys, which allow the definition of 49 | * paths for plugins, view templates and locale files respectively. 50 | */ 51 | 'App' => [ 52 | 'namespace' => 'App', 53 | 'encoding' => env('APP_ENCODING', 'UTF-8'), 54 | 'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'), 55 | 'defaultTimezone' => env('APP_DEFAULT_TIMEZONE', 'UTC'), 56 | 'base' => false, 57 | 'dir' => 'src', 58 | 'webroot' => 'webroot', 59 | 'wwwRoot' => WWW_ROOT, 60 | //'baseUrl' => env('SCRIPT_NAME'), 61 | 'fullBaseUrl' => false, 62 | 'imageBaseUrl' => 'img/', 63 | 'cssBaseUrl' => 'css/', 64 | 'jsBaseUrl' => 'js/', 65 | 'paths' => [ 66 | 'plugins' => [ROOT . DS . 'plugins' . DS], 67 | 'templates' => [ROOT . DS . 'templates' . DS], 68 | 'locales' => [RESOURCES . 'locales' . DS], 69 | ], 70 | ], 71 | 72 | /* 73 | * Security and encryption configuration 74 | * 75 | * - salt - A random string used in security hashing methods. 76 | * The salt value is also used as the encryption key. 77 | * You should treat it as extremely sensitive data. 78 | */ 79 | 'Security' => [ 80 | 'salt' => env('SECURITY_SALT'), 81 | ], 82 | 83 | /* 84 | * Apply timestamps with the last modified time to static assets (js, css, images). 85 | * Will append a querystring parameter containing the time the file was modified. 86 | * This is useful for busting browser caches. 87 | * 88 | * Set to true to apply timestamps when debug is true. Set to 'force' to always 89 | * enable timestamping regardless of debug value. 90 | */ 91 | 'Asset' => [ 92 | //'timestamp' => true, 93 | // 'cacheTime' => '+1 year' 94 | ], 95 | 96 | /* 97 | * Configure the cache adapters. 98 | */ 99 | 'Cache' => [ 100 | 'default' => [ 101 | 'className' => FileEngine::class, 102 | 'path' => CACHE, 103 | 'url' => env('CACHE_DEFAULT_URL', null), 104 | ], 105 | 106 | /* 107 | * Configure the cache used for general framework caching. 108 | * Translation cache files are stored with this configuration. 109 | * Duration will be set to '+2 minutes' in bootstrap.php when debug = true 110 | * If you set 'className' => 'Null' core cache will be disabled. 111 | */ 112 | '_cake_translations_' => [ 113 | 'className' => FileEngine::class, 114 | 'prefix' => 'myapp_cake_translations_', 115 | 'path' => CACHE . 'persistent' . DS, 116 | 'serialize' => true, 117 | 'duration' => '+1 years', 118 | 'url' => env('CACHE_CAKECORE_URL', null), 119 | ], 120 | 121 | /* 122 | * Configure the cache for model and datasource caches. This cache 123 | * configuration is used to store schema descriptions, and table listings 124 | * in connections. 125 | * Duration will be set to '+2 minutes' in bootstrap.php when debug = true 126 | */ 127 | '_cake_model_' => [ 128 | 'className' => FileEngine::class, 129 | 'prefix' => 'myapp_cake_model_', 130 | 'path' => CACHE . 'models' . DS, 131 | 'serialize' => true, 132 | 'duration' => '+1 years', 133 | 'url' => env('CACHE_CAKEMODEL_URL', null), 134 | ], 135 | ], 136 | 137 | /* 138 | * Configure the Error and Exception handlers used by your application. 139 | * 140 | * By default errors are displayed using Debugger, when debug is true and logged 141 | * by Cake\Log\Log when debug is false. 142 | * 143 | * In CLI environments exceptions will be printed to stderr with a backtrace. 144 | * In web environments an HTML page will be displayed for the exception. 145 | * With debug true, framework errors like Missing Controller will be displayed. 146 | * When debug is false, framework errors will be coerced into generic HTTP errors. 147 | * 148 | * Options: 149 | * 150 | * - `errorLevel` - int - The level of errors you are interested in capturing. 151 | * - `trace` - boolean - Whether backtraces should be included in 152 | * logged errors/exceptions. 153 | * - `log` - boolean - Whether you want exceptions logged. 154 | * - `exceptionRenderer` - string - The class responsible for rendering uncaught exceptions. 155 | * The chosen class will be used for both CLI and web environments. If you want different 156 | * classes used in CLI and web environments you'll need to write that conditional logic as well. 157 | * The conventional location for custom renderers is in `src/Error`. Your exception renderer needs to 158 | * implement the `render()` method and return either a string or Http\Response. 159 | * `errorRenderer` - string - The class responsible for rendering PHP errors. The selected 160 | * class will be used for both web and CLI contexts. If you want different classes for each environment 161 | * you'll need to write that conditional logic as well. Error renderers need to 162 | * to implement the `Cake\Error\ErrorRendererInterface`. 163 | * - `skipLog` - array - List of exceptions to skip for logging. Exceptions that 164 | * extend one of the listed exceptions will also be skipped for logging. 165 | * E.g.: 166 | * `'skipLog' => ['Cake\Http\Exception\NotFoundException', 'Cake\Http\Exception\UnauthorizedException']` 167 | * - `extraFatalErrorMemory` - int - The number of megabytes to increase the memory limit by 168 | * when a fatal error is encountered. This allows 169 | * breathing room to complete logging or error handling. 170 | * - `ignoredDeprecationPaths` - array - A list of glob-compatible file paths that deprecations 171 | * should be ignored in. Use this to ignore deprecations for plugins or parts of 172 | * your application that still emit deprecations. 173 | */ 174 | 'Error' => [ 175 | 'errorLevel' => E_ALL, 176 | 'skipLog' => [], 177 | 'log' => true, 178 | 'trace' => true, 179 | 'ignoredDeprecationPaths' => [], 180 | ], 181 | 182 | /* 183 | * Debugger configuration 184 | * 185 | * Define development error values for Cake\Error\Debugger 186 | * 187 | * - `editor` Set the editor URL format you want to use. 188 | * By default atom, emacs, macvim, phpstorm, sublime, textmate, and vscode are 189 | * available. You can add additional editor link formats using 190 | * `Debugger::addEditor()` during your application bootstrap. 191 | * - `outputMask` A mapping of `key` to `replacement` values that 192 | * `Debugger` should replace in dumped data and logs generated by `Debugger`. 193 | */ 194 | 'Debugger' => [ 195 | 'editor' => 'phpstorm', 196 | ], 197 | 198 | /* 199 | * Email configuration. 200 | * 201 | * By defining transports separately from delivery profiles you can easily 202 | * re-use transport configuration across multiple profiles. 203 | * 204 | * You can specify multiple configurations for production, development and 205 | * testing. 206 | * 207 | * Each transport needs a `className`. Valid options are as follows: 208 | * 209 | * Mail - Send using PHP mail function 210 | * Smtp - Send using SMTP 211 | * Debug - Do not send the email, just return the result 212 | * 213 | * You can add custom transports (or override existing transports) by adding the 214 | * appropriate file to src/Mailer/Transport. Transports should be named 215 | * 'YourTransport.php', where 'Your' is the name of the transport. 216 | */ 217 | 'EmailTransport' => [ 218 | 'default' => [ 219 | 'className' => MailTransport::class, 220 | /* 221 | * The keys host, port, timeout, username, password, client and tls 222 | * are used in SMTP transports 223 | */ 224 | 'host' => 'localhost', 225 | 'port' => 25, 226 | 'timeout' => 30, 227 | /* 228 | * It is recommended to set these options through your environment or app_local.php 229 | */ 230 | //'username' => null, 231 | //'password' => null, 232 | 'client' => null, 233 | 'tls' => false, 234 | 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null), 235 | ], 236 | ], 237 | 238 | /* 239 | * Email delivery profiles 240 | * 241 | * Delivery profiles allow you to predefine various properties about email 242 | * messages from your application and give the settings a name. This saves 243 | * duplication across your application and makes maintenance and development 244 | * easier. Each profile accepts a number of keys. See `Cake\Mailer\Mailer` 245 | * for more information. 246 | */ 247 | 'Email' => [ 248 | 'default' => [ 249 | 'transport' => 'default', 250 | 'from' => 'you@localhost', 251 | /* 252 | * Will by default be set to config value of App.encoding, if that exists otherwise to UTF-8. 253 | */ 254 | //'charset' => 'utf-8', 255 | //'headerCharset' => 'utf-8', 256 | ], 257 | ], 258 | 259 | /* 260 | * Connection information used by the ORM to connect 261 | * to your application's datastores. 262 | * 263 | * ### Notes 264 | * - Drivers include Mysql Postgres Sqlite Sqlserver 265 | * See vendor\cakephp\cakephp\src\Database\Driver for the complete list 266 | * - Do not use periods in database name - it may lead to errors. 267 | * See https://github.com/cakephp/cakephp/issues/6471 for details. 268 | * - 'encoding' is recommended to be set to full UTF-8 4-Byte support. 269 | * E.g set it to 'utf8mb4' in MariaDB and MySQL and 'utf8' for any 270 | * other RDBMS. 271 | */ 272 | 'Datasources' => [ 273 | /* 274 | * These configurations should contain permanent settings used 275 | * by all environments. 276 | * 277 | * The values in app_local.php will override any values set here 278 | * and should be used for local and per-environment configurations. 279 | * 280 | * Environment variable-based configurations can be loaded here or 281 | * in app_local.php depending on the application's needs. 282 | */ 283 | 'default' => [ 284 | 'className' => Connection::class, 285 | 'driver' => Mysql::class, 286 | 'persistent' => false, 287 | 'timezone' => 'UTC', 288 | 289 | /* 290 | * For MariaDB/MySQL the internal default changed from utf8 to utf8mb4, aka full utf-8 support 291 | */ 292 | 'encoding' => 'utf8mb4', 293 | 294 | /* 295 | * If your MySQL server is configured with `skip-character-set-client-handshake` 296 | * then you MUST use the `flags` config to set your charset encoding. 297 | * For e.g. `'flags' => [\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4']` 298 | */ 299 | 'flags' => [], 300 | 'cacheMetadata' => true, 301 | 'log' => false, 302 | 303 | /* 304 | * Set identifier quoting to true if you are using reserved words or 305 | * special characters in your table or column names. Enabling this 306 | * setting will result in queries built using the Query Builder having 307 | * identifiers quoted when creating SQL. It should be noted that this 308 | * decreases performance because each query needs to be traversed and 309 | * manipulated before being executed. 310 | */ 311 | 'quoteIdentifiers' => false, 312 | 313 | /* 314 | * During development, if using MySQL < 5.6, uncommenting the 315 | * following line could boost the speed at which schema metadata is 316 | * fetched from the database. It can also be set directly with the 317 | * mysql configuration directive 'innodb_stats_on_metadata = 0' 318 | * which is the recommended value in production environments 319 | */ 320 | //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'], 321 | ], 322 | 323 | /* 324 | * The test connection is used during the test suite. 325 | */ 326 | 'test' => [ 327 | 'className' => Connection::class, 328 | 'driver' => Mysql::class, 329 | 'persistent' => false, 330 | 'timezone' => 'UTC', 331 | 'encoding' => 'utf8mb4', 332 | 'flags' => [], 333 | 'cacheMetadata' => true, 334 | 'quoteIdentifiers' => false, 335 | 'log' => false, 336 | //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'], 337 | ], 338 | ], 339 | 340 | /* 341 | * Configures logging options 342 | */ 343 | 'Log' => [ 344 | 'debug' => [ 345 | 'className' => FileLog::class, 346 | 'path' => LOGS, 347 | 'file' => 'debug', 348 | 'url' => env('LOG_DEBUG_URL', null), 349 | 'scopes' => null, 350 | 'levels' => ['notice', 'info', 'debug'], 351 | ], 352 | 'error' => [ 353 | 'className' => FileLog::class, 354 | 'path' => LOGS, 355 | 'file' => 'error', 356 | 'url' => env('LOG_ERROR_URL', null), 357 | 'scopes' => null, 358 | 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], 359 | ], 360 | // To enable this dedicated query log, you need to set your datasource's log flag to true 361 | 'queries' => [ 362 | 'className' => FileLog::class, 363 | 'path' => LOGS, 364 | 'file' => 'queries', 365 | 'url' => env('LOG_QUERIES_URL', null), 366 | 'scopes' => ['cake.database.queries'], 367 | ], 368 | ], 369 | 370 | /* 371 | * Session configuration. 372 | * 373 | * Contains an array of settings to use for session configuration. The 374 | * `defaults` key is used to define a default preset to use for sessions, any 375 | * settings declared here will override the settings of the default config. 376 | * 377 | * ## Options 378 | * 379 | * - `cookie` - The name of the cookie to use. Defaults to value set for `session.name` php.ini config. 380 | * Avoid using `.` in cookie names, as PHP will drop sessions from cookies with `.` in the name. 381 | * - `cookiePath` - The url path for which session cookie is set. Maps to the 382 | * `session.cookie_path` php.ini config. Defaults to base path of app. 383 | * - `timeout` - The time in minutes a session can be 'idle'. If no request is received in 384 | * this duration, the session will be expired and rotated. Pass 0 to disable idle timeout checks. 385 | * - `defaults` - The default configuration set to use as a basis for your session. 386 | * There are four built-in options: php, cake, cache, database. 387 | * - `handler` - Can be used to enable a custom session handler. Expects an 388 | * array with at least the `engine` key, being the name of the Session engine 389 | * class to use for managing the session. CakePHP bundles the `CacheSession` 390 | * and `DatabaseSession` engines. 391 | * - `ini` - An associative array of additional 'session.*` ini values to set. 392 | * 393 | * Within the `ini` key, you will likely want to define: 394 | * 395 | * - `session.cookie_lifetime` - The number of seconds that cookies are valid for. This 396 | * should be longer than `Session.timeout`. 397 | * - `session.gc_maxlifetime` - The number of seconds after which a session is considered 'garbage' 398 | * that can be deleted by PHP's session cleanup behavior. This value should be greater than both 399 | * `Sesssion.timeout` and `session.cookie_lifetime`. 400 | * 401 | * The built-in `defaults` options are: 402 | * 403 | * - 'php' - Uses settings defined in your php.ini. 404 | * - 'cake' - Saves session files in CakePHP's /tmp directory. 405 | * - 'database' - Uses CakePHP's database sessions. 406 | * - 'cache' - Use the Cache class to save sessions. 407 | * 408 | * To define a custom session handler, save it at src/Http/Session/.php. 409 | * Make sure the class implements PHP's `SessionHandlerInterface` and set 410 | * Session.handler to 411 | * 412 | * To use database sessions, load the SQL file located at config/schema/sessions.sql 413 | */ 414 | 'Session' => [ 415 | 'defaults' => 'php', 416 | ], 417 | 418 | /** 419 | * DebugKit configuration. 420 | * 421 | * Contains an array of configurations to apply to the DebugKit plugin, if loaded. 422 | * Documentation: https://book.cakephp.org/debugkit/5/en/index.html#configuration 423 | * 424 | * ## Options 425 | * 426 | * - `panels` - Enable or disable panels. The key is the panel name, and the value is true to enable, 427 | * or false to disable. 428 | * - `includeSchemaReflection` - Set to true to enable logging of schema reflection queries. Disabled by default. 429 | * - `safeTld` - Set an array of whitelisted TLDs for local development. 430 | * - `forceEnable` - Force DebugKit to display. Careful with this, it is usually safer to simply whitelist 431 | * your local TLDs. 432 | * - `ignorePathsPattern` - Regex pattern (including delimiter) to ignore paths. 433 | * DebugKit won’t save data for request URLs that match this regex. 434 | * - `ignoreAuthorization` - Set to true to ignore Cake Authorization plugin for DebugKit requests. 435 | * Disabled by default. 436 | * - `maxDepth` - Defines how many levels of nested data should be shown in general for debug output. 437 | * Default is 5. WARNING: Increasing the max depth level can lead to an out of memory error. 438 | * - `variablesPanelMaxDepth` - Defines how many levels of nested data should be shown in the variables tab. 439 | * Default is 5. WARNING: Increasing the max depth level can lead to an out of memory error. 440 | */ 441 | 'DebugKit' => [ 442 | 'forceEnable' => filter_var(env('DEBUG_KIT_FORCE_ENABLE', false), FILTER_VALIDATE_BOOLEAN), 443 | 'safeTld' => env('DEBUG_KIT_SAFE_TLD', null), 444 | 'ignoreAuthorization' => env('DEBUG_KIT_IGNORE_AUTHORIZATION', false), 445 | ], 446 | 447 | /** 448 | * TestSuite configuration. 449 | * 450 | * ## Options 451 | * 452 | * - `errorLevel` - Defaults to `E_ALL`. Can be set to `false` to disable overwrite error level. 453 | * - `fixtureStrategy` - Defaults to TruncateStrategy. Can be set to any class implementing FixtureStrategyInterface. 454 | */ 455 | 'TestSuite' => [ 456 | 'errorLevel' => null, 457 | 'fixtureStrategy' => null, 458 | ], 459 | ]; 460 | --------------------------------------------------------------------------------