├── .editorconfig
├── .gitattributes
├── .gitignore
├── .htaccess
├── .travis.yml
├── LICENSE.txt
├── bin
├── cake
├── cake.bat
└── cake.php
├── composer.json
├── composer.lock
├── config
├── Migrations
│ ├── 20170428140000_initial_users.php
│ ├── 20170428153210_initial_schema.php
│ ├── 20170428153225_create_tags.php
│ └── schema-dump-default.lock
├── Seeds
│ └── RandomSchemaSeed.php
├── api.php
├── app.default.php
├── bootstrap.php
├── bootstrap_cli.php
├── paths.php
├── requirements.php
├── routes.php
└── schema
│ ├── i18n.sql
│ └── sessions.sql
├── index.php
├── logo.png
├── phpunit.xml.dist
├── readme.md
├── src
├── Application.php
├── Authentication
│ └── Authenticator
│ │ └── AppFormAuthenticator.php
├── Console
│ └── Installer.php
├── Controller
│ ├── AppController.php
│ ├── ErrorController.php
│ └── PagesController.php
├── Model
│ ├── Entity
│ │ ├── Article.php
│ │ ├── Comment.php
│ │ ├── Favorite.php
│ │ ├── Follow.php
│ │ ├── Tag.php
│ │ └── User.php
│ ├── Factory
│ │ ├── ArticlesFactory.php
│ │ ├── CommentsFactory.php
│ │ ├── TagsFactory.php
│ │ └── UsersFactory.php
│ └── Table
│ │ ├── ArticlesTable.php
│ │ ├── CommentsTable.php
│ │ ├── FavoritesTable.php
│ │ ├── FollowsTable.php
│ │ ├── TagsTable.php
│ │ └── UsersTable.php
├── Service
│ ├── Action
│ │ ├── Article
│ │ │ ├── ArticleAddAction.php
│ │ │ ├── ArticleDeleteAction.php
│ │ │ ├── ArticleEditAction.php
│ │ │ ├── ArticleFavoriteAction.php
│ │ │ ├── ArticleFeedAction.php
│ │ │ ├── ArticleIndexAction.php
│ │ │ ├── ArticleIndexBase.php
│ │ │ ├── ArticleUnfavoriteAction.php
│ │ │ └── ArticleViewAction.php
│ │ ├── ChildArticleAction.php
│ │ ├── Comment
│ │ │ ├── CommentAddAction.php
│ │ │ ├── CommentDeleteAction.php
│ │ │ └── CommentIndexAction.php
│ │ ├── DummyAction.php
│ │ ├── Extension
│ │ │ └── AppPaginateExtension.php
│ │ ├── Profile
│ │ │ ├── ProfileFollowAction.php
│ │ │ ├── ProfileUnfollowAction.php
│ │ │ └── ProfileViewAction.php
│ │ ├── Tag
│ │ │ └── TagIndexAction.php
│ │ └── User
│ │ │ ├── LoginAction.php
│ │ │ ├── RegisterAction.php
│ │ │ ├── UserEditAction.php
│ │ │ └── UserViewAction.php
│ ├── ArticlesService.php
│ ├── CommentsService.php
│ ├── ProfilesService.php
│ ├── Renderer
│ │ └── AppJsonRenderer.php
│ ├── TagsService.php
│ ├── UserService.php
│ └── UsersService.php
├── Shell
│ └── ConsoleShell.php
├── Template
│ ├── Element
│ │ └── Flash
│ │ │ ├── default.ctp
│ │ │ ├── error.ctp
│ │ │ └── success.ctp
│ ├── Error
│ │ ├── error400.ctp
│ │ └── error500.ctp
│ ├── Layout
│ │ ├── Email
│ │ │ ├── html
│ │ │ │ └── default.ctp
│ │ │ └── text
│ │ │ │ └── default.ctp
│ │ ├── ajax.ctp
│ │ ├── default.ctp
│ │ ├── error.ctp
│ │ └── rss
│ │ │ └── default.ctp
│ └── Pages
│ │ └── home.ctp
├── Utility
│ └── Formatter.php
└── View
│ ├── AjaxView.php
│ └── AppView.php
├── tests
├── Fixture
│ ├── ArticlesFixture.php
│ ├── CommentsFixture.php
│ ├── FavoritesFixture.php
│ ├── FollowsFixture.php
│ ├── SocialAccountsFixture.php
│ ├── TaggedFixture.php
│ ├── TagsFixture.php
│ └── UsersFixture.php
├── FixturesTrait.php
├── TestCase
│ ├── IntegrationTestCase.php
│ └── Service
│ │ ├── ArticlesServiceTest.php
│ │ ├── CommentsServiceTest.php
│ │ ├── FavoritesServiceTest.php
│ │ ├── LoginTest.php
│ │ ├── ProfilesServiceTest.php
│ │ ├── RegisterTest.php
│ │ ├── TagsServiceTest.php
│ │ └── UserServiceTest.php
└── bootstrap.php
└── webroot
├── .htaccess
├── css
├── base.css
├── cake.css
└── home.css
├── favicon.ico
├── font
├── cakedingbats-webfont.eot
├── cakedingbats-webfont.svg
├── cakedingbats-webfont.ttf
├── cakedingbats-webfont.woff
└── cakedingbats-webfont.woff2
├── img
├── cake-logo.png
├── cake.icon.png
├── cake.logo.svg
└── cake.power.gif
├── index.php
└── js
└── empty
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; This file is for unifying the coding style for different editors and IDEs.
2 | ; More information at http://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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # app
2 | /config/app.php
3 | /tmp/*
4 | /logs/*
5 | /config/app_local.php
6 | /config/bootstrap.local.php
7 |
8 |
9 | # dependencies
10 | /vendor/*
11 | /node_modules
12 | /bower_components
13 |
14 | # IDEs and editors
15 | /.idea
16 | pmip
17 | .project
18 | .classpath
19 | *.launch
20 | .settings/
21 |
22 |
23 | #System Files
24 | .DS_Store
25 | Thumbs.db
26 |
--------------------------------------------------------------------------------
/.htaccess:
--------------------------------------------------------------------------------
1 | # Uncomment the following to prevent the httpoxy vulnerability
2 | # See: https://httpoxy.org/
3 | #
4 | # RequestHeader unset Proxy
5 | #
6 |
7 |
8 | RewriteEngine on
9 | RewriteRule ^$ webroot/ [L]
10 | RewriteRule (.*) webroot/$1 [L]
11 |
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | sudo: false
4 |
5 | dist: xenial
6 |
7 | php:
8 | - 7.3
9 | - 7.4
10 |
11 | services:
12 | - postgresql
13 | - mysql
14 |
15 | cache:
16 | directories:
17 | - vendor
18 | - $HOME/.composer/cache
19 |
20 | env:
21 | matrix:
22 | - DB=mysql DATABASE_TEST_URL='mysql://root:@127.0.0.1/cakephp_test?init[]=SET sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"'
23 | - DB=pgsql DATABASE_TEST_URL='postgres://postgres@127.0.0.1/cakephp_test'
24 | - DB=sqlite DATABASE_TEST_URL='sqlite:///:memory:'
25 |
26 | global:
27 | - DEFAULT=1
28 |
29 | matrix:
30 | fast_finish: true
31 |
32 | include:
33 | - php: 7.3
34 | env: PHPCS=1 DEFAULT=0
35 |
36 | before_script:
37 | - composer self-update
38 | - composer install --prefer-dist --no-interaction
39 | - if [[ $DB == 'mysql' ]]; then mysql -u root -e 'CREATE DATABASE cakephp_test;'; fi
40 | - if [[ $DB == 'pgsql' ]]; then psql -c 'CREATE DATABASE cakephp_test;' -U postgres; fi
41 | - if [[ $PHPSTAN = 1 ]]; then composer stan-setup; fi
42 |
43 | script:
44 | - if [[ $DEFAULT = 1 ]]; then composer test; fi
45 | - if [[ $COVERAGE = 1 ]]; then composer coverage-test; fi
46 | - if [[ $PHPCS = 1 ]]; then composer cs-check; fi
47 | - if [[ $PHPSTAN = 1 ]]; then composer stan; fi
48 |
49 | after_success:
50 | - if [[ $COVERAGE = 1 ]]; then bash <(curl -s https://codecov.io/bash); fi
51 |
52 | notifications:
53 | email: false
54 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright 2009-2016
4 | Cake Development Corporation
5 | 1785 E. Sahara Avenue, Suite 490-423
6 | Las Vegas, Nevada 89104
7 | Phone: +1 702 425 5085
8 | https://cakedc.com
9 |
10 | Permission is hereby granted, free of charge, to any person obtaining a
11 | copy of this software and associated documentation files (the "Software"),
12 | to deal in the Software without restriction, including without limitation
13 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 | and/or sell copies of the Software, and to permit persons to whom the
15 | Software is furnished to do so, subject to the following conditions:
16 |
17 | The above copyright notice and this permission notice shall be included in
18 | all copies or substantial portions of the Software.
19 |
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 | DEALINGS IN THE SOFTWARE.
27 |
--------------------------------------------------------------------------------
/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 (http://cakephp.org)
7 | # Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
14 | # @link http://cakephp.org CakePHP(tm) Project
15 | # @since 1.2.0
16 | # @license http://www.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 |
--------------------------------------------------------------------------------
/bin/cake.bat:
--------------------------------------------------------------------------------
1 | ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
2 | ::
3 | :: Cake is a Windows batch script for invoking CakePHP shell commands
4 | ::
5 | :: CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
6 | :: Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
12 | :: @link http://cakephp.org CakePHP(tm) Project
13 | :: @since 2.0.0
14 | :: @license http://www.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 |
--------------------------------------------------------------------------------
/bin/cake.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/php -q
2 | run($argv));
13 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cakephp/app",
3 | "description": "CakePHP skeleton app",
4 | "homepage": "http://cakephp.org",
5 | "type": "project",
6 | "license": "MIT",
7 | "require": {
8 | "php": ">=5.6",
9 | "cakephp/cakephp": "3.8.*",
10 | "mobiledetect/mobiledetectlib": "2.*",
11 | "cakephp/migrations": "^2.0.0",
12 | "cakedc/cakephp-api": "7.0.*",
13 | "cakedc/users": "^8.0",
14 | "cakephp/authentication": "*",
15 | "firebase/php-jwt": "~4.0",
16 | "muffin/slug": "1.*",
17 | "muffin/tags": "dev-master",
18 | "skie/cakephp-factory-muffin": "*",
19 | "cakephp/plugin-installer": "*"
20 | },
21 | "require-dev": {
22 | "psy/psysh": "@stable",
23 | "cakephp\/debug_kit": "^3.17.0",
24 | "phpunit/phpunit": "6.*",
25 | "cakephp\/cakephp-codesniffer": "^3.0",
26 | "cakephp\/bake": "^1.9.0"
27 | },
28 | "suggest": {
29 | "markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification."
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "App\\": "src"
34 | }
35 | },
36 | "autoload-dev": {
37 | "psr-4": {
38 | "App\\Test\\": "tests",
39 | "Cake\\Test\\": "./vendor/cakephp/cakephp/tests"
40 | }
41 | },
42 | "scripts": {
43 | "post-install-cmd": "App\\Console\\Installer::postInstall",
44 | "post-create-project-cmd": "App\\Console\\Installer::postInstall",
45 | "post-autoload-dump": "Cake\\Composer\\Installer\\PluginInstaller::postAutoloadDump",
46 | "check": [
47 | "@test",
48 | "@cs-check"
49 | ],
50 | "cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests",
51 | "cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests",
52 | "test": "phpunit --colors=always"
53 | },
54 | "config": {
55 | "minimum-stability": "dev"
56 | },
57 | "repositories": [
58 | {
59 | "type": "vcs",
60 | "url": "git@github.com:skie/Tags-1.git"
61 | }
62 | ],
63 | "minimum-stability": "dev",
64 | "prefer-stable": true
65 | }
66 |
--------------------------------------------------------------------------------
/config/Migrations/20170428153210_initial_schema.php:
--------------------------------------------------------------------------------
1 | table('articles', ['id' => false, 'primary_key' => ['id']])
26 | ->addColumn('id', 'uuid', [
27 | 'null' => false,
28 | ])
29 | ->addColumn('title', 'string', [
30 | 'limit' => 255,
31 | 'null' => false,
32 | ])
33 | ->addColumn('slug', 'string', [
34 | 'default' => null,
35 | 'limit' => 255,
36 | 'null' => true,
37 | ])
38 | ->addColumn('description', 'text', [
39 | 'null' => true,
40 | ])
41 | ->addColumn('body', 'text', [
42 | 'null' => true,
43 | ])
44 | ->addColumn('author_id', 'uuid', [
45 | 'default' => null,
46 | 'null' => false,
47 | ])
48 | ->addColumn('favorites_count', 'integer', [
49 | 'default' => 0,
50 | 'null' => false,
51 | 'limit' => 11,
52 | ])
53 | ->addColumn('tag_count', 'integer', [
54 | 'default' => 0,
55 | 'null' => false,
56 | 'limit' => 11,
57 | ])
58 | ->addColumn('created', 'datetime', [
59 | 'null' => false,
60 | ])
61 | ->addColumn('modified', 'datetime', [
62 | 'null' => false,
63 | ])
64 | ->addIndex(['author_id'])
65 | ->addIndex(['slug'], [
66 | 'unique' => true,
67 | ])
68 | ->create();
69 |
70 | $this->table('comments', ['id' => false, 'primary_key' => ['id']])
71 | ->addColumn('id', 'uuid', [
72 | 'null' => false,
73 | ])
74 | ->addColumn('body', 'text', [
75 | 'null' => true,
76 | ])
77 | ->addColumn('article_id', 'uuid', [
78 | 'default' => null,
79 | 'null' => false,
80 | ])
81 | ->addColumn('author_id', 'uuid', [
82 | 'default' => null,
83 | 'null' => false,
84 | ])
85 | ->addColumn('created', 'datetime', [
86 | 'null' => false,
87 | ])
88 | ->addColumn('modified', 'datetime', [
89 | 'null' => false,
90 | ])
91 | ->addIndex(['article_id'])
92 | ->create();
93 |
94 | $this->table('follows', ['id' => false, 'primary_key' => ['id']])
95 | ->addColumn('id', 'uuid', [
96 | 'null' => false,
97 | ])
98 | ->addColumn('followable_id', 'uuid', [
99 | 'null' => false,
100 | ])
101 | ->addColumn('follower_id', 'uuid', [
102 | 'null' => false,
103 | ])
104 | ->addColumn('blocked', 'boolean', [
105 | 'null' => false,
106 | ])
107 | ->addColumn('created', 'datetime', [
108 | 'null' => false,
109 | ])
110 | ->addColumn('modified', 'datetime', [
111 | 'null' => false,
112 | ])
113 | ->addIndex(['followable_id'])
114 | ->addIndex(['follower_id', 'followable_id'])
115 | ->create();
116 |
117 | $this->table('favorites', ['id' => false, 'primary_key' => ['id']])
118 | ->addColumn('id', 'uuid', [
119 | 'null' => false,
120 | ])
121 | ->addColumn('user_id', 'uuid', [
122 | 'null' => false,
123 | ])
124 | ->addColumn('article_id', 'uuid', [
125 | 'null' => false,
126 | ])
127 | ->addColumn('created', 'datetime', [
128 | 'null' => false,
129 | ])
130 | ->addColumn('modified', 'datetime', [
131 | 'null' => false,
132 | ])
133 | ->addIndex(['article_id', 'user_id'])
134 | ->addIndex(['user_id'])
135 | ->create();
136 | }
137 | }
138 | // @codingStandardsIgnoreEnd
139 |
--------------------------------------------------------------------------------
/config/Migrations/20170428153225_create_tags.php:
--------------------------------------------------------------------------------
1 | table('tags_tags', ['id' => false, 'primary_key' => ['id']]);
26 | $table->addColumn('id', 'uuid', [
27 | 'null' => false,
28 | ]);
29 | $table->addColumn('namespace', 'string', [
30 | 'default' => null,
31 | 'limit' => 255,
32 | 'null' => true,
33 | ]);
34 | $table->addColumn('slug', 'string', [
35 | 'default' => null,
36 | 'limit' => 255,
37 | 'null' => false,
38 | ]);
39 | $table->addColumn('label', 'string', [
40 | 'default' => null,
41 | 'limit' => 255,
42 | 'null' => false,
43 | ]);
44 | $table->addColumn('counter', 'integer', [
45 | 'default' => 0,
46 | 'length' => 11,
47 | 'null' => false,
48 | 'signed' => false,
49 | ]);
50 | $table->addColumn('created', 'datetime', [
51 | 'default' => null,
52 | 'null' => true,
53 | ]);
54 | $table->addColumn('modified', 'datetime', [
55 | 'default' => null,
56 | 'null' => true,
57 | ]);
58 | $table->create();
59 |
60 | $table = $this->table('tags_tagged', ['id' => false, 'primary_key' => ['id']]);
61 | $table->addColumn('id', 'uuid', [
62 | 'null' => false,
63 | ]);
64 | $table->addColumn('tag_id', 'uuid', [
65 | 'default' => null,
66 | 'null' => true,
67 | ]);
68 | $table->addColumn('fk_id', 'uuid', [
69 | 'default' => null,
70 | 'null' => true,
71 | ]);
72 | $table->addColumn('fk_table', 'string', [
73 | 'default' => null,
74 | 'limit' => 255,
75 | 'null' => false,
76 | ]);
77 | $table->addColumn('created', 'datetime', [
78 | 'default' => null,
79 | 'null' => true,
80 | ]);
81 | $table->addColumn('modified', 'datetime', [
82 | 'default' => null,
83 | 'null' => true,
84 | ]);
85 | $table->create();
86 |
87 | $table = $this->table('tags_tags');
88 | $table->addColumn('tag_key', 'string', [
89 | 'default' => null,
90 | 'limit' => 255,
91 | 'null' => false,
92 | ]);
93 | $table->addIndex(['tag_key', 'label', 'namespace'], ['unique' => true]);
94 | $table->update();
95 | $table = $this->table('tags_tagged');
96 | $table->addIndex(['tag_id', 'fk_id', 'fk_table'], ['unique' => true]);
97 | $table->update();
98 | }
99 | }
100 | // @codingStandardsIgnoreEnd
101 |
--------------------------------------------------------------------------------
/config/Seeds/RandomSchemaSeed.php:
--------------------------------------------------------------------------------
1 | totalTags, 'Tags');
84 | $users = FactoryLoader::seed($this->totalUsers, 'Users');
85 |
86 | collection($users)->filter(function ($item) {
87 | return rand(1, $this->totalUsers) < $this->userWithArticleRatio * $this->totalUsers;
88 | })->each(function ($user) {
89 | $articles = FactoryLoader::seed(rand(1, $this->maxArticlesByUser), 'Articles', ['author_id' => $user->id]);
90 | collection($articles)->each(function ($article) {
91 | $comments = FactoryLoader::seed(rand(1, $this->maxCommentsInArticle), 'Comments', [
92 | 'article_id' => $article->id,
93 | ]);
94 | });
95 | });
96 |
97 | $articles = TableRegistry::getTableLocator()->get('Articles')->find()->select(['id'])->all();
98 |
99 | $favoritesCount = count($users) * $this->usersWithFavoritesRatio;
100 | $Favorites = TableRegistry::getTableLocator()->get('Favorites');
101 | for ($i = 1; $i < $favoritesCount; $i++) {
102 | $user = $users[$i];
103 | $articles->shuffle()->take(rand(1, floor($articles->count() / 2)))->each(
104 | function ($article) use ($Favorites, $user) {
105 | $Favorites->save($Favorites->newEntity([
106 | 'user_id' => $user['id'],
107 | 'article_id' => $article['id'],
108 | ]));
109 | }
110 | );
111 | }
112 |
113 | $followingCount = count($users) * $this->usersWithFollowingRatio;
114 | $Follows = \Cake\ORM\TableRegistry::get('Follows');
115 | for ($i = 1; $i < $followingCount; $i++) {
116 | $user = $users[$i];
117 | collection($users)
118 | ->reject(function ($item) use ($user) {
119 | return $item->id === $user->id;
120 | })
121 | ->shuffle()
122 | ->take(rand(1, (int)(count($users) - 1) * 0.2))
123 | ->each(
124 | function ($follow) use ($Follows, $user) {
125 | $Follows->save($Follows->newEntity([
126 | 'follower_id' => $user['id'],
127 | 'followable_id' => $follow['id'],
128 | ]));
129 | }
130 | );
131 | }
132 | }
133 | }
134 | // @codingStandardsIgnoreEnd
135 |
--------------------------------------------------------------------------------
/config/api.php:
--------------------------------------------------------------------------------
1 | [
14 | 'ServiceFallback' => '\\CakeDC\\Api\\Service\\FallbackService',
15 | 'renderer' => 'AppJson',
16 | 'parser' => 'CakeDC/Api.Form',
17 |
18 | 'useVersioning' => false,
19 | 'versionPrefix' => 'v',
20 |
21 | 'Auth' => [
22 | 'Crud' => [
23 | 'default' => 'auth'
24 | ],
25 | ],
26 |
27 | 'Service' => [
28 | 'default' => [
29 | 'options' => [
30 | 'Extension' => [
31 | 'CakeDC/Api.OptionsHandler'
32 | ],
33 | ],
34 | 'Action' => [
35 | 'default' => [
36 | 'Auth' => [
37 | 'authenticate' => [
38 | 'CakeDC/Api.Psr7'
39 | ],
40 | ],
41 | 'Extension' => [
42 | 'CakeDC/Api.Cors',
43 | ]
44 | ],
45 | ],
46 | ],
47 | ],
48 | ]
49 | ];
50 |
--------------------------------------------------------------------------------
/config/bootstrap_cli.php:
--------------------------------------------------------------------------------
1 | connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
44 |
45 | /**
46 | * ...and connect the rest of 'Pages' controller's URLs.
47 | */
48 | $routes->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
49 |
50 | /**
51 | * Connect catchall routes for all controllers.
52 | *
53 | * Using the argument `DashedRoute`, the `fallbacks` method is a shortcut for
54 | * `$routes->connect('/:controller', ['action' => 'index'], ['routeClass' => 'DashedRoute']);`
55 | * `$routes->connect('/:controller/:action/*', [], ['routeClass' => 'DashedRoute']);`
56 | *
57 | * Any route class can be used with this method, such as:
58 | * - DashedRoute
59 | * - InflectedRoute
60 | * - Route
61 | * - Or your own route class
62 | *
63 | * You can remove these routes once you've connected the
64 | * routes you want in your application.
65 | */
66 | $routes->fallbacks(DashedRoute::class);
67 | });
68 |
--------------------------------------------------------------------------------
/config/schema/i18n.sql:
--------------------------------------------------------------------------------
1 | # Copyright (c) Cake Software Foundation, Inc. (http://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 (http://www.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 |
--------------------------------------------------------------------------------
/config/schema/sessions.sql:
--------------------------------------------------------------------------------
1 | # Copyright (c) Cake Software Foundation, Inc. (http://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 (http://www.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 |
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # 
2 |
3 | [](https://travis-ci.org/gothinkster/cakephp-realworld-example-app) [](https://github.com/gothinkster/cakephp-realworld-example-app/stargazers) [](https://raw.githubusercontent.com/gothinkster/cakephp-realworld-example-app/master/LICENSE.txt)
4 |
5 | > ### Example CakePHP codebase containing real world examples (CRUD, auth, advanced patterns and more) that adheres to the [RealWorld](https://github.com/gothinkster/realworld-example-apps) spec and API.
6 |
7 | This repo is functionality complete — PRs and issues welcome!
8 |
9 | ----------
10 |
11 | # Getting started
12 |
13 | ## Installation
14 |
15 | Please check the official cakephp installation guide for server requirements before you start. [Official Documentation](https://book.cakephp.org/3.0/en/installation.html)
16 |
17 | Clone the repository
18 |
19 | git clone git@github.com:gothinkster/cakephp-realworld-example-app.git
20 |
21 | Switch to the repo folder
22 |
23 | cd cakephp-realworld-example-app
24 |
25 | Install all the dependencies using composer
26 |
27 | composer install
28 |
29 | Configure your database settings in the `config/app.php` file(See: Datasource/default)
30 |
31 | vi config/app.php
32 |
33 | Run the database migrations (**Set the database connection in app.php**)
34 |
35 | bin/cake migrations migrate
36 |
37 | Start the local development server
38 |
39 | bin/cake server
40 |
41 | You can now access the server at http://localhost:8765
42 |
43 | ## Database seeding
44 |
45 | **Populate the database with seed data with relationships which includes users, articles, comments, tags, favorites and follows. This can help you to quickly start testing the api or couple a frontend and start using it with ready content.**
46 |
47 | Run the database seeder and you're done
48 |
49 | bin/cake migrations seed
50 |
51 | ## API Specification
52 |
53 | This application adheres to the api specifications set by the [Thinkster](https://github.com/gothinkster) team. This helps mix and match any backend with any other frontend without conflicts.
54 |
55 | > [Full API Spec](https://github.com/gothinkster/realworld/tree/master/api)
56 |
57 | For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.
58 |
59 |
60 | # How it works
61 |
62 | ----------
63 |
64 | # Code overview
65 |
66 | ## Dependencies
67 |
68 | - [cakedc/cakephp-api](https://github.com/cakedc/cakephp-api) - For easy REST api implementation.
69 | - [muffin/slug](https://github.com/UseMuffin/Slug) - For auto generation of unique slugs.
70 | - [muffin/tags](https://github.com/UseMuffin/Tags) - For tags managements.
71 | - [cakephp/authentication](https://github.com/cakephp/authentication) - For authentication using JSON Web Tokens
72 |
73 | ## Folders
74 |
75 | - `src` - Contains all the application logic.
76 | - `config` - Contains all the application configuration files.
77 | - `src/Model/Entity` - Contains all cakephp ORM entites.
78 | - `src/Model/Table` - Contains all cakephp ORM tables.
79 | - `src/Service` - Contains application services that represents root api endpoints.
80 | - `src/Service/Action` - Contains application endpoints logic logic.
81 | - `src/Service/Renderer` - Contains the final api response formatter.
82 | - `/config/Migrations` - Contains all the database migrations.
83 |
84 | ## Environment configuration
85 |
86 | - `config/app.php` - Configuration settings can be set in this file
87 |
88 | ***Note*** : You can quickly set the database information and other variables in this file and have the application fully working.
89 |
90 | ----------
91 |
92 | # Testing API
93 |
94 | Run the cakephp development server
95 |
96 | bin/cake server
97 |
98 | The api can now be accessed at
99 |
100 | http://localhost:8765/api
101 |
102 | Request headers
103 |
104 | | **Required** | **Key** | **Value** |
105 | |---------- |------------------ |------------------ |
106 | | Yes | Content-Type | application/json |
107 | | Yes | X-Requested-With | XMLHttpRequest |
108 | | Optional | Authorization | Token {JWT} |
109 |
110 | Refer the [api specification](#api-specification) for more info.
111 |
112 | ----------
113 |
114 | # Authentication
115 |
116 | This applications uses JSON Web Token (JWT) to handle authentication. The token is passed with each request using the `Authorization` header with `Token` scheme. The cakephp authenticate middleware configured for handling JWT authentication and validation and authentication of the token. Please check the following sources to learn more about JWT.
117 |
118 | - https://jwt.io/introduction/
119 | - https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
120 |
121 | ----------
122 |
123 | # Cross-Origin Resource Sharing (CORS)
124 |
125 | This applications has CORS enabled by default on all API endpoints. Please check the following sources to learn more about CORS.
126 |
127 | - https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
128 | - https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
129 | - https://www.w3.org/TR/cors
130 |
--------------------------------------------------------------------------------
/src/Application.php:
--------------------------------------------------------------------------------
1 | addPlugin('Bake');
43 | } catch (MissingPluginException $e) {
44 | // Do not halt if the plugin is missing
45 | }
46 | }
47 |
48 | $this->addPlugin('Migrations');
49 |
50 | $this->addPlugin('Muffin/Slug');
51 | $this->addPlugin('Muffin/Tags');
52 |
53 | if (Configure::read('debug')) {
54 | $this->addPlugin('DebugKit', ['bootstrap' => true]);
55 | }
56 |
57 | $this->addPlugin('CakeDC/Api', ['bootstrap' => false, 'routes' => true]);
58 | }
59 |
60 | /**
61 | * Setup the middleware your application will use.
62 | *
63 | * @param \Cake\Http\MiddlewareQueue $middleware The middleware queue to setup.
64 | * @return \Cake\Http\MiddlewareQueue The updated middleware.
65 | */
66 | public function middleware($middleware)
67 | {
68 | $service = new AuthenticationService();
69 |
70 | $service->loadIdentifier('Authentication.JwtSubject', [
71 | 'dataField' => 'sub',
72 | ]);
73 | $service->loadAuthenticator('Authentication.Jwt', [
74 | 'tokenPrefix' => 'Token',
75 | 'returnPayload' => false,
76 | ]);
77 |
78 | $service->loadAuthenticator(AppFormAuthenticator::class, [
79 | 'baseModel' => 'user',
80 | 'fields' => [
81 | 'username' => 'email',
82 | 'password' => 'password',
83 | ],
84 | ]);
85 | $service->loadIdentifier(\Authentication\Identifier\PasswordIdentifier::class, [
86 | 'fields' => [
87 | 'username' => 'email',
88 | 'password' => 'password',
89 | ],
90 | ]);
91 |
92 | // Add it to the authentication middleware
93 | $authentication = new AuthenticationMiddleware($service);
94 |
95 | $middleware
96 | // Catch any exceptions in the lower layers,
97 | // and make an error page/response
98 | ->add(ErrorHandlerMiddleware::class)
99 |
100 | ->add(new RequestHandlerMiddleware())
101 | ->add($authentication)
102 | ->add(new ApiMiddleware())
103 |
104 | // Handle plugin/theme assets like CakePHP normally does.
105 | ->add(AssetMiddleware::class)
106 |
107 | // Apply routing
108 | ->add(RoutingMiddleware::class);
109 |
110 | return $middleware;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/Authentication/Authenticator/AppFormAuthenticator.php:
--------------------------------------------------------------------------------
1 | _checkUrl($request)) {
40 | $errors = [
41 | sprintf(
42 | 'Login URL %s did not match %s',
43 | $request->getUri()->getPath(),
44 | $this->getConfig('loginUrl')
45 | ),
46 | ];
47 |
48 | return new Result(null, Result::FAILURE_CREDENTIALS_INVALID, $errors);
49 | }
50 |
51 | $data = $this->_getData($request);
52 | if ($data === null) {
53 | return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, [
54 | 'Login credentials not found',
55 | ]);
56 | }
57 |
58 | $user = $this->getIdentifier()->identify($data);
59 |
60 | if (empty($user)) {
61 | return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, $this->getIdentifier()->getErrors());
62 | }
63 |
64 | return new Result($user, Result::SUCCESS);
65 | }
66 |
67 | /**
68 | * Checks the fields to ensure they are supplied.
69 | *
70 | * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
71 | * @return array|null Username and password retrieved from a request body.
72 | */
73 | protected function _getData(ServerRequestInterface $request)
74 | {
75 | $fields = $this->_config['fields'];
76 | $body = Hash::get($request->getParsedBody(), $this->getConfig('baseModel'));
77 |
78 | $data = [];
79 | foreach ($fields as $key => $field) {
80 | if (!isset($body[$field])) {
81 | return null;
82 | }
83 |
84 | $value = $body[$field];
85 | if (!is_string($value) || !strlen($value)) {
86 | return null;
87 | }
88 |
89 | $data[$key] = $value;
90 | }
91 |
92 | return $data;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Controller/AppController.php:
--------------------------------------------------------------------------------
1 | loadComponent('Security');`
37 | *
38 | * @return void
39 | */
40 | public function initialize()
41 | {
42 | parent::initialize();
43 |
44 | $this->loadComponent('RequestHandler');
45 | $this->loadComponent('Flash');
46 |
47 | /*
48 | * Enable the following components for recommended CakePHP security settings.
49 | * see http://book.cakephp.org/3.0/en/controllers/components/security.html
50 | */
51 | //$this->loadComponent('Security');
52 | //$this->loadComponent('Csrf');
53 | }
54 |
55 | /**
56 | * Before render callback.
57 | *
58 | * @param \Cake\Event\Event $event The beforeRender event.
59 | * @return \Cake\Http\Response|null|void
60 | */
61 | public function beforeRender(Event $event)
62 | {
63 | if (
64 | !array_key_exists('_serialize', $this->viewVars) &&
65 | in_array($this->response->type(), ['application/json', 'application/xml'])
66 | ) {
67 | $this->set('_serialize', true);
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Controller/ErrorController.php:
--------------------------------------------------------------------------------
1 | loadComponent('RequestHandler');
34 | }
35 |
36 | /**
37 | * beforeFilter callback.
38 | *
39 | * @param \Cake\Event\Event $event Event.
40 | * @return \Cake\Http\Response|null|void
41 | */
42 | public function beforeFilter(Event $event)
43 | {
44 | }
45 |
46 | /**
47 | * beforeRender callback.
48 | *
49 | * @param \Cake\Event\Event $event Event.
50 | * @return \Cake\Http\Response|null|void
51 | */
52 | public function beforeRender(Event $event)
53 | {
54 | parent::beforeRender($event);
55 |
56 | $this->viewBuilder()->setTemplatePath('Error');
57 | }
58 |
59 | /**
60 | * afterFilter callback.
61 | *
62 | * @param \Cake\Event\Event $event Event.
63 | * @return \Cake\Http\Response|null|void
64 | */
65 | public function afterFilter(Event $event)
66 | {
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Controller/PagesController.php:
--------------------------------------------------------------------------------
1 | redirect('/');
46 | }
47 | if (in_array('..', $path, true) || in_array('.', $path, true)) {
48 | throw new ForbiddenException();
49 | }
50 | $page = $subpage = null;
51 |
52 | if (!empty($path[0])) {
53 | $page = $path[0];
54 | }
55 | if (!empty($path[1])) {
56 | $subpage = $path[1];
57 | }
58 | $this->set(compact('page', 'subpage'));
59 |
60 | try {
61 | $this->render(implode('/', $path));
62 | } catch (MissingTemplateException $e) {
63 | if (Configure::read('debug')) {
64 | throw $e;
65 | }
66 | throw new NotFoundException();
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Model/Entity/Article.php:
--------------------------------------------------------------------------------
1 | true,
46 | 'id' => false,
47 | ];
48 | }
49 |
--------------------------------------------------------------------------------
/src/Model/Entity/Comment.php:
--------------------------------------------------------------------------------
1 | true,
43 | 'id' => false,
44 | ];
45 | }
46 |
--------------------------------------------------------------------------------
/src/Model/Entity/Favorite.php:
--------------------------------------------------------------------------------
1 | true,
42 | 'id' => false,
43 | ];
44 | }
45 |
--------------------------------------------------------------------------------
/src/Model/Entity/Follow.php:
--------------------------------------------------------------------------------
1 | true,
40 | 'id' => false,
41 | ];
42 | }
43 |
--------------------------------------------------------------------------------
/src/Model/Entity/Tag.php:
--------------------------------------------------------------------------------
1 | true,
41 | 'id' => false,
42 | ];
43 |
44 | /**
45 | * Label getter.
46 | *
47 | * @return string
48 | */
49 | // @codingStandardsIgnoreStart
50 | public function _getTag()
51 | {
52 | return $this->get('label');
53 | }
54 | // @codingStandardsIgnoreEnd
55 | }
56 |
--------------------------------------------------------------------------------
/src/Model/Entity/User.php:
--------------------------------------------------------------------------------
1 | true,
57 | 'id' => false,
58 | ];
59 |
60 | /**
61 | * Fields that are excluded from JSON versions of the entity.
62 | *
63 | * @var array
64 | */
65 | protected $_hidden = [
66 | 'password',
67 | 'token',
68 | ];
69 |
70 | /**
71 | * @param string $password password that will be set.
72 | * @return bool|string
73 | */
74 | protected function _setPassword($password)
75 | {
76 | return $this->hashPassword($password);
77 | }
78 |
79 | /**
80 | * Hash a password using the configured password hasher,
81 | * use DefaultPasswordHasher if no one was configured
82 | *
83 | * @param string $password password to be hashed
84 | * @return mixed
85 | */
86 | public function hashPassword($password)
87 | {
88 | $PasswordHasher = $this->getPasswordHasher();
89 |
90 | return $PasswordHasher->hash($password);
91 | }
92 |
93 | /**
94 | * Return the configured Password Hasher
95 | *
96 | * @return mixed
97 | */
98 | public function getPasswordHasher()
99 | {
100 | $passwordHasher = Configure::read('Users.passwordHasher');
101 | if (!class_exists($passwordHasher)) {
102 | $passwordHasher = '\Cake\Auth\DefaultPasswordHasher';
103 | }
104 |
105 | return new $passwordHasher();
106 | }
107 |
108 | /**
109 | * Builds jwt token.
110 | *
111 | * @return string
112 | */
113 | // @codingStandardsIgnoreStart
114 | public function _getToken()
115 | {
116 | $content = [
117 | 'id' => $this->get('id'),
118 | 'sub' => $this->get('id'),
119 | 'exp' => time() + 60 * DAY
120 | ];
121 | return JWT::encode($content, Security::getSalt());
122 | }
123 | // @codingStandardsIgnoreEnd
124 | }
125 |
--------------------------------------------------------------------------------
/src/Model/Factory/ArticlesFactory.php:
--------------------------------------------------------------------------------
1 | true,
21 | 'title' => Faker::sentence(),
22 | 'description_length' => Faker::numberBetween(3, 7),
23 | 'description' => function ($item) {
24 | $paragraphs = Faker::paragraphs($item['description_length'], true);
25 |
26 | return $paragraphs();
27 | },
28 | 'body_length' => Faker::numberBetween(2, 5),
29 | 'body' => function ($item) {
30 | $paragraphs = Faker::paragraphs($item['body_length'], true);
31 |
32 | return $paragraphs();
33 | },
34 | 'created' => Faker::dateTimeBetween('-2 year', 'now'),
35 | 'modified' => Faker::dateTimeBetween('-2 year', 'now'),
36 | 'tagList' => function ($item) {
37 | $tags = TableRegistry::getTableLocator()->get('Tags')->find()->select(['label'])
38 | ->all()
39 | ->shuffle()
40 | ->take(5)
41 | ->map(function ($i) {
42 | return $i['label'];
43 | })
44 | ->reduce(function ($accum, $item) {
45 | return ($accum ? $item . ' ' . $accum : $item);
46 | });
47 |
48 | return $tags;
49 | },
50 | ];
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Model/Factory/CommentsFactory.php:
--------------------------------------------------------------------------------
1 | Faker::numberBetween(2, 5),
21 | 'body' => function ($item) {
22 | $paragraphs = Faker::paragraphs($item['body_length'], true);
23 |
24 | return $paragraphs();
25 | },
26 | 'created' => Faker::dateTimeBetween('-2 year', 'now'),
27 | 'modified' => Faker::dateTimeBetween('-2 year', 'now'),
28 | // 'author_id' => function ($item) {
29 | // $users = TableRegistry::get('Users')->find()->select(['id'])->all()->toArray();
30 | //
31 | // return $users[rand(0, count($users) - 1)]->id;
32 | // }
33 | 'author_id' => 'factory|' . UsersFactory::class,
34 | ];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Model/Factory/TagsFactory.php:
--------------------------------------------------------------------------------
1 | Faker::unique()->word(),
20 | 'tag_key' => function ($item) {
21 | return $item['label'];
22 | },
23 | ];
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Model/Factory/UsersFactory.php:
--------------------------------------------------------------------------------
1 | Faker::firstName(),
20 | 'last_name' => Faker::lastName(),
21 | 'username' => Faker::unique()->firstName(),
22 | 'active' => true,
23 | 'password' => 'passwd',
24 | 'email' => Faker::unique()->safeEmail(),
25 | 'bio' => Faker::sentence(),
26 | 'image' => Faker::imageUrl(100, 100, 'people'),
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Model/Table/CommentsTable.php:
--------------------------------------------------------------------------------
1 | setTable('comments');
51 | $this->setDisplayField('id');
52 | $this->setPrimaryKey('id');
53 |
54 | $this->addBehavior('Timestamp');
55 |
56 | $this->belongsTo('Articles', [
57 | 'foreignKey' => 'article_id',
58 | 'joinType' => 'INNER',
59 | ]);
60 | $this->belongsTo('Authors', [
61 | 'className' => 'Users',
62 | 'foreignKey' => 'author_id',
63 | 'joinType' => 'INNER',
64 | ]);
65 | }
66 |
67 | /**
68 | * Default validation rules.
69 | *
70 | * @param \Cake\Validation\Validator $validator Validator instance.
71 | * @return \Cake\Validation\Validator
72 | */
73 | public function validationDefault(Validator $validator)
74 | {
75 | $validator
76 | ->uuid('id')
77 | ->allowEmpty('id', 'create');
78 |
79 | $validator
80 | ->allowEmpty('body');
81 |
82 | return $validator;
83 | }
84 |
85 | /**
86 | * Returns a rules checker object that will be used for validating
87 | * application integrity.
88 | *
89 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
90 | * @return \Cake\ORM\RulesChecker
91 | */
92 | public function buildRules(RulesChecker $rules)
93 | {
94 | $rules->add($rules->existsIn(['article_id'], 'Articles'));
95 | $rules->add($rules->existsIn(['author_id'], 'Authors'));
96 |
97 | return $rules;
98 | }
99 |
100 | /**
101 | * Api finder and endpoint formatter.
102 | *
103 | * @param \Cake\ORM\Query $query Query object.
104 | * @param array $options Query options.
105 | * @return \Cake\ORM\Query The query builder.
106 | */
107 | public function findApiFormat(Query $query, array $options)
108 | {
109 | return $query
110 | ->select(['id', 'body', 'created', 'modified', 'author_id'])
111 | ->order(['Comments.created' => 'desc'])
112 | ->formatResults(function ($results) use ($options) {
113 | return $results->map(function ($row) use ($options) {
114 | if ($row === null) {
115 | return $row;
116 | }
117 | $row = Formatter::dateFormat($row);
118 | $row['author'] = TableRegistry::getTableLocator()->get('Users')->getFormatted($row['author_id'], $options);
119 | unset($row['author_id']);
120 |
121 | return $row;
122 | });
123 | });
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/Model/Table/FavoritesTable.php:
--------------------------------------------------------------------------------
1 | setTable('favorites');
49 | $this->setDisplayField('id');
50 | $this->setPrimaryKey('id');
51 |
52 | $this->addBehavior('Timestamp');
53 | $this->addBehavior('CounterCache', [
54 | 'Articles' => ['favorites_count'],
55 | ]);
56 |
57 | $this->belongsTo('Users', [
58 | 'foreignKey' => 'user_id',
59 | 'joinType' => 'INNER',
60 | ]);
61 | $this->belongsTo('Articles', [
62 | 'foreignKey' => 'article_id',
63 | 'joinType' => 'INNER',
64 | ]);
65 | }
66 |
67 | /**
68 | * Default validation rules.
69 | *
70 | * @param \Cake\Validation\Validator $validator Validator instance.
71 | * @return \Cake\Validation\Validator
72 | */
73 | public function validationDefault(Validator $validator)
74 | {
75 | $validator
76 | ->uuid('id')
77 | ->allowEmpty('id', 'create');
78 |
79 | return $validator;
80 | }
81 |
82 | /**
83 | * Returns a rules checker object that will be used for validating
84 | * application integrity.
85 | *
86 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
87 | * @return \Cake\ORM\RulesChecker
88 | */
89 | public function buildRules(RulesChecker $rules)
90 | {
91 | $rules->add($rules->existsIn(['user_id'], 'Users'));
92 | $rules->add($rules->existsIn(['article_id'], 'Articles'));
93 |
94 | return $rules;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Model/Table/FollowsTable.php:
--------------------------------------------------------------------------------
1 | setTable('follows');
46 | $this->setDisplayField('id');
47 | $this->setPrimaryKey('id');
48 |
49 | $this->addBehavior('Timestamp');
50 | $this->belongsTo('Followers', [
51 | 'className' => 'Users',
52 | 'foreignKey' => 'follower_id',
53 | 'joinType' => 'INNER',
54 | ]);
55 | $this->belongsTo('Followables', [
56 | 'className' => 'Users',
57 | 'foreignKey' => 'followable_id',
58 | 'joinType' => 'INNER',
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)
69 | {
70 | $validator
71 | ->uuid('id')
72 | ->allowEmpty('id', 'create');
73 |
74 | $validator
75 | ->requirePresence('followable_id', 'create')
76 | ->notEmpty('followable_id');
77 |
78 | $validator
79 | ->requirePresence('follower_id', 'create')
80 | ->notEmpty('follower_id');
81 |
82 | $validator
83 | ->requirePresence('blocked', 'create');
84 |
85 | return $validator;
86 | }
87 |
88 | /**
89 | * Checks if user following another user.
90 | *
91 | * @param string $followerId Follower User id.
92 | * @param string $followableId Followable User id.
93 | * @return bool
94 | */
95 | public function following($followerId, $followableId)
96 | {
97 | return $this->find()
98 | ->where([
99 | 'follower_id' => $followerId,
100 | 'followable_id' => $followableId,
101 | 'blocked' => false,
102 | ])
103 | ->count() > 0;
104 | }
105 |
106 | /**
107 | * Makes one user following another user.
108 | *
109 | * @param string $followerId Follower User id.
110 | * @param string $followableId Followable User id.
111 | * @return bool
112 | */
113 | public function follow($followerId, $followableId)
114 | {
115 | $exists = $this->find()
116 | ->where([
117 | 'follower_id' => $followerId,
118 | 'followable_id' => $followableId,
119 | 'blocked' => false,
120 | ])
121 | ->first();
122 | if (!$exists) {
123 | return $this->save($this->newEntity([
124 | 'follower_id' => $followerId,
125 | 'followable_id' => $followableId,
126 | 'blocked' => false,
127 | ]));
128 | }
129 |
130 | return true;
131 | }
132 |
133 | /**
134 | * Makes one user not following another user.
135 | *
136 | * @param string $followerId Follower User id.
137 | * @param string $followableId Followable User id.
138 | * @return bool
139 | */
140 | public function unfollow($followerId, $followableId)
141 | {
142 | $exists = $this->find()
143 | ->where([
144 | 'follower_id' => $followerId,
145 | 'followable_id' => $followableId,
146 | 'blocked' => false,
147 | ])
148 | ->first();
149 | if ($exists) {
150 | return $this->delete($exists);
151 | }
152 |
153 | return true;
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/Model/Table/TagsTable.php:
--------------------------------------------------------------------------------
1 | setTable('tags_tags');
48 | $this->setPrimaryKey('id');
49 | }
50 |
51 | /**
52 | * Default validation rules.
53 | *
54 | * @param \Cake\Validation\Validator $validator Validator instance.
55 | * @return \Cake\Validation\Validator
56 | */
57 | public function validationDefault(Validator $validator)
58 | {
59 | $validator
60 | ->uuid('id')
61 | ->allowEmpty('id', 'create');
62 |
63 | return $validator;
64 | }
65 |
66 | /**
67 | * Api finder and endpoint formatter.
68 | *
69 | * @param \Cake\ORM\Query $query Query object.
70 | * @param array $options Query options.
71 | * @return \Cake\ORM\Query The query builder.
72 | */
73 | public function findApiFormat(Query $query, array $options)
74 | {
75 | return $query
76 | ->order(['counter' => 'desc'])
77 | ->limit(20)
78 | ->formatResults(function ($results) use ($options) {
79 | return $results->map(function ($row) {
80 | if ($row === null) {
81 | return $row;
82 | }
83 |
84 | return Formatter::dateFormat($row);
85 | });
86 | });
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Service/Action/Article/ArticleAddAction.php:
--------------------------------------------------------------------------------
1 | getTable()->getValidator();
27 | $data = $this->getData();
28 | if (!array_key_exists('article', $data)) {
29 | throw new ValidationException(__('Validation failed'), 0, null, ['article root does not exists']);
30 | }
31 | $errors = $validator->errors($data['article']);
32 | if (!empty($errors)) {
33 | throw new ValidationException(__('Validation failed'), 0, null, $errors);
34 | }
35 |
36 | return true;
37 | }
38 |
39 | /**
40 | * Execute action.
41 | *
42 | * @return mixed
43 | */
44 | public function execute()
45 | {
46 | $entity = $this->_newEntity();
47 | $data = $this->_articleData();
48 | $data['author_id'] = $this->Auth->user('id');
49 | $entity = $this->_patchEntity($entity, $data);
50 |
51 | $result = $this->_save($entity);
52 | if ($result) {
53 | return ['article' => $this->_getEntity($result['slug'])];
54 | }
55 |
56 | return null;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Service/Action/Article/ArticleDeleteAction.php:
--------------------------------------------------------------------------------
1 | _getEntity($this->_id);
29 | if ($record['author_id'] != $this->Auth->user('id')) {
30 | throw new ForbiddenException();
31 | }
32 |
33 | return true;
34 | }
35 |
36 | /**
37 | * Execute action.
38 | *
39 | * @return mixed
40 | */
41 | public function execute()
42 | {
43 | $record = $this->_getEntity($this->_id);
44 | if ($record) {
45 | $result = $this->getTable()->delete($record);
46 | }
47 |
48 | return !empty($result);
49 | }
50 |
51 | /**
52 | * Returns single entity by id.
53 | *
54 | * @param mixed $primaryKey Primary key.
55 | * @return \Cake\Collection\Collection
56 | */
57 | protected function _getEntity($primaryKey)
58 | {
59 | return $this->getTable()
60 | ->find()
61 | ->where(['Articles.slug' => $primaryKey])
62 | ->firstOrFail();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Service/Action/Article/ArticleEditAction.php:
--------------------------------------------------------------------------------
1 | getData();
31 | if (!array_key_exists('article', $data)) {
32 | throw new ValidationException(__('Validation failed'), 0, null, ['article root does not exists']);
33 | }
34 | $data = Hash::get($data, 'article');
35 | unset($data['author']);
36 | $entity = $this->_patchEntity($this->_getEntityBySlug(), $data);
37 |
38 | if ($entity['author_id'] != $this->Auth->user('id')) {
39 | throw new ForbiddenException();
40 | }
41 |
42 | $errors = $entity->getErrors();
43 | if (!empty($errors)) {
44 | throw new ValidationException(__('Validation failed'), 0, null, $errors);
45 | }
46 |
47 | return true;
48 | }
49 |
50 | /**
51 | * Execute action.
52 | *
53 | * @return mixed
54 | */
55 | public function execute()
56 | {
57 | $data = $this->_articleData();
58 | $entity = $this->_patchEntity($this->_getEntityBySlug(), $data);
59 |
60 | if ($this->_save($entity)) {
61 | return ['article' => $this->_getEntity($this->_id)];
62 | }
63 |
64 | return false;
65 | }
66 |
67 | /**
68 | * Returns single entity by id.
69 | *
70 | * @param mixed $primaryKey Primary key.
71 | * @return \Cake\Collection\Collection
72 | */
73 | protected function _getEntity($primaryKey)
74 | {
75 | return $this->getTable()
76 | ->find('apiFormat')
77 | ->where(['Articles.slug' => $primaryKey])
78 | ->firstOrFail();
79 | }
80 |
81 | /**
82 | * @return mixed
83 | */
84 | protected function _getEntityBySlug()
85 | {
86 | return $this->getTable()->find()->where(['Articles.slug' => $this->_id])->firstOrFail();
87 | }
88 |
89 | /**
90 | * @return array
91 | */
92 | protected function _articleData()
93 | {
94 | $data = Hash::get($this->getData(), 'article');
95 | unset($data['author']);
96 | if (!empty($data['tagList']) && is_array($data['tagList'])) {
97 | $data['tagList'] = join(' ', $data['tagList']);
98 | }
99 |
100 | return $data;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Service/Action/Article/ArticleFavoriteAction.php:
--------------------------------------------------------------------------------
1 | _getEntity($this->_id);
37 | $this->getTable()->favorite($article['id'], $this->Auth->user('id'));
38 |
39 | return $this->_viewArticle();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Service/Action/Article/ArticleFeedAction.php:
--------------------------------------------------------------------------------
1 | Auth->identify();
27 |
28 | return $this->getTable()->find('apiFormat', [
29 | 'feed_by' => $this->Auth->user('id'),
30 | 'currentUser' => $this->Auth->user('id'),
31 | ]);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Service/Action/Article/ArticleIndexAction.php:
--------------------------------------------------------------------------------
1 | Auth->allow($this->getName());
29 | }
30 |
31 | /**
32 | * Returns query object
33 | *
34 | * @return Query
35 | */
36 | protected function _getQuery()
37 | {
38 | $options = $this->getData();
39 | $user = $this->Auth->identify();
40 | if ($user) {
41 | $options['currentUser'] = $user['id'];
42 | }
43 |
44 | return $this->getTable()->find('apiFormat', $options);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Service/Action/Article/ArticleIndexBase.php:
--------------------------------------------------------------------------------
1 | _getEntities();
37 | $pagination = $this->getService()->getResult()->getPayload('pagination');
38 |
39 | return [
40 | 'articles' => $entities,
41 | 'articlesCount' => Hash::get($pagination, 'count'),
42 | ];
43 | }
44 |
45 | /**
46 | * Builds entities list
47 | *
48 | * @return \Cake\Collection\Collection
49 | */
50 | protected function _getEntities()
51 | {
52 | $query = $this->_getQuery();
53 | // debug($query);
54 |
55 | $event = $this->dispatchEvent('Action.Crud.onFindEntities', compact('query'));
56 | // debug($event);
57 | if ($event->result) {
58 | $query = $event->result;
59 | }
60 | $records = $query->all();
61 | $this->dispatchEvent('Action.Crud.afterFindEntities', compact('query', 'records'));
62 |
63 | return $records;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Service/Action/Article/ArticleUnfavoriteAction.php:
--------------------------------------------------------------------------------
1 | _getEntity($this->_id);
37 | $this->getTable()->unfavorite($article['id'], $this->Auth->user('id'));
38 |
39 | return $this->_viewArticle();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Service/Action/Article/ArticleViewAction.php:
--------------------------------------------------------------------------------
1 | isPublic) {
33 | $this->Auth->allow($this->getName());
34 | }
35 | }
36 |
37 | /**
38 | * Execute action.
39 | *
40 | * @return mixed
41 | */
42 | public function execute()
43 | {
44 | return $this->_viewArticle();
45 | }
46 |
47 | /**
48 | * Returns current article.
49 | *
50 | * @return array
51 | */
52 | protected function _viewArticle()
53 | {
54 | $record = $this->getTable()
55 | ->find('apiFormat', [
56 | 'currentUser' => $this->Auth->user('id'),
57 | ])
58 | ->where(['Articles.slug' => $this->_id])
59 | ->firstOrFail();
60 |
61 | return ['article' => $record];
62 | }
63 |
64 | /**
65 | * Build condition for get entity method.
66 | *
67 | * @param string $primaryKey Record id.
68 | * @return array
69 | */
70 | protected function _buildViewCondition($primaryKey)
71 | {
72 | return ['Articles.slug' => $this->_id];
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Service/Action/ChildArticleAction.php:
--------------------------------------------------------------------------------
1 | get('Articles')->find()->where(['slug' => $this->_parentId])->firstOrFail();
32 | if ($article) {
33 | $this->_parentId = $article->id;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Service/Action/Comment/CommentAddAction.php:
--------------------------------------------------------------------------------
1 | getTable()->getValidator();
29 | $data = $this->getData();
30 | if (!array_key_exists('comment', $data)) {
31 | throw new ValidationException(__('Validation failed'), 0, null, ['comment root does not exists']);
32 | }
33 | $errors = $validator->errors($data['comment']);
34 | if (!empty($errors)) {
35 | throw new ValidationException(__('Validation failed'), 0, null, $errors);
36 | }
37 |
38 | return true;
39 | }
40 |
41 | /**
42 | * Execute action.
43 | *
44 | * @return mixed
45 | */
46 | public function execute()
47 | {
48 | $entity = $this->_newEntity();
49 | $data = Hash::get($this->getData(), 'comment');
50 | $data['author_id'] = $this->Auth->user('id');
51 | $data['article_id'] = $this->_parentId;
52 | $entity = $this->_patchEntity($entity, $data);
53 |
54 | $result = $this->_save($entity);
55 | if ($result) {
56 | $comment = $this->getTable()->find('apiFormat')->where(['Comments.id' => $result->id])->first();
57 |
58 | return ['comment' => $comment];
59 | }
60 |
61 | return null;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Service/Action/Comment/CommentDeleteAction.php:
--------------------------------------------------------------------------------
1 | getTable()->find()->where(['id' => $this->_id])->firstOrFail();
28 |
29 | if ($record['author_id'] != $this->Auth->user('id')) {
30 | throw new ForbiddenException();
31 | }
32 |
33 | return true;
34 | }
35 |
36 | /**
37 | * Execute action.
38 | *
39 | * @return mixed
40 | */
41 | public function execute()
42 | {
43 | $record = $this->_getEntity($this->_id);
44 | if ($record) {
45 | $result = $this->getTable()->delete($record);
46 | }
47 |
48 | return !empty($result);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Service/Action/Comment/CommentIndexAction.php:
--------------------------------------------------------------------------------
1 | Auth->allow($this->getName());
34 | }
35 |
36 | /**
37 | * Execute action.
38 | *
39 | * @return mixed
40 | */
41 | public function execute()
42 | {
43 | $entities = $this->_getEntities()->toArray();
44 |
45 | $pagination = $this->getService()->getResult()->getPayload('pagination');
46 |
47 | return [
48 | 'comments' => $entities,
49 | 'commentsCount' => Hash::get($pagination, 'count'),
50 | ];
51 | }
52 |
53 | /**
54 | * Builds entities list
55 | *
56 | * @return \Cake\Collection\Collection
57 | */
58 | protected function _getEntities()
59 | {
60 | $options = $this->getData();
61 | $user = $this->Auth->identify();
62 | if ($user) {
63 | $options['currentUser'] = $user['id'];
64 | }
65 | $query = $this->getTable()->find('apiFormat', $options);
66 |
67 | $event = $this->dispatchEvent('Action.Crud.onFindEntities', compact('query'));
68 | if ($event->result) {
69 | $query = $event->result;
70 | }
71 | $records = $query->all();
72 | $this->dispatchEvent('Action.Crud.afterFindEntities', compact('query', 'records'));
73 |
74 | return $records;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Service/Action/DummyAction.php:
--------------------------------------------------------------------------------
1 | 20,
30 | 'limitField' => 'limit',
31 | 'offsetField' => 'offset',
32 | ];
33 |
34 | /**
35 | * Returns a list of events this object is implementing. When the class is registered
36 | * in an event manager, each individual method will be associated with the respective event.
37 | *
38 | * @return array
39 | */
40 | public function implementedEvents()
41 | {
42 | return [
43 | 'Action.Crud.onFindEntities' => 'findEntities',
44 | 'Action.Crud.afterFindEntities' => 'afterFind',
45 | ];
46 | }
47 |
48 | /**
49 | * Find entities
50 | *
51 | * @param Event $event An Event instance
52 | * @return Entity
53 | */
54 | public function findEntities(Event $event)
55 | {
56 | $action = $event->getSubject();
57 | $query = $event->getData('query');
58 | if ($event->result) {
59 | $query = $event->result;
60 | }
61 | $query->limit($this->_limit($action));
62 | $query->offset($this->_offset($action));
63 |
64 | return $query;
65 | }
66 |
67 | /**
68 | * Returns current offset.
69 | *
70 | * @param Action $action An Action instance
71 | * @return int
72 | */
73 | protected function _offset(Action $action)
74 | {
75 | $data = $action->getData();
76 | $offsetField = $this->getConfig('offsetField');
77 | if (!empty($data[$offsetField]) && is_numeric($data[$offsetField])) {
78 | return (int)$data[$offsetField];
79 | } else {
80 | return 0;
81 | }
82 | }
83 |
84 | /**
85 | * Returns current limit
86 | *
87 | * @param Action $action An Action instance
88 | * @return mixed
89 | */
90 | protected function _limit(Action $action)
91 | {
92 | $data = $action->getData();
93 | $limitField = $this->getConfig('limitField');
94 | $maxLimit = $action->getConfig($limitField);
95 | if (empty($maxLimit)) {
96 | $maxLimit = $this->getConfig('defaultLimit');
97 | }
98 | if (!empty($limitField) && !empty($data[$limitField]) && is_numeric($data[$limitField])) {
99 | $limit = min((int)$data[$limitField], $maxLimit);
100 |
101 | return $limit;
102 | } else {
103 | return $maxLimit;
104 | }
105 | }
106 |
107 | /**
108 | * after find entities
109 | *
110 | * @param Event $event An Event instance
111 | * @return void
112 | */
113 | public function afterFind(Event $event)
114 | {
115 | $action = $event->getSubject();
116 | $query = $event->getData('query');
117 | $result = $action->getService()->getResult();
118 | $count = $query->count();
119 | $limit = $this->_limit($action);
120 | $pagination = [
121 | 'offset' => $this->_offset($action),
122 | 'limit' => $limit,
123 | 'pages' => ceil($count / $limit),
124 | 'count' => $count,
125 | ];
126 | $result->appendPayload('pagination', $pagination);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Service/Action/Profile/ProfileFollowAction.php:
--------------------------------------------------------------------------------
1 | _table = TableRegistry::getTableLocator()->get('Users');
31 | }
32 |
33 | /**
34 | * Apply validation process.
35 | *
36 | * @return bool
37 | */
38 | public function validates()
39 | {
40 | return true;
41 | }
42 |
43 | /**
44 | * Execute action.
45 | *
46 | * @return mixed
47 | */
48 | public function execute()
49 | {
50 | $record = $this->getTable()
51 | ->find()
52 | ->where(['Users.username' => $this->_id])
53 | ->firstOrFail();
54 | TableRegistry::getTableLocator()->get('Follows')->follow($this->Auth->user('id'), $record['id']);
55 |
56 | return $this->_viewProfile();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Service/Action/Profile/ProfileUnfollowAction.php:
--------------------------------------------------------------------------------
1 | _table = TableRegistry::getTableLocator()->get('Users');
31 | }
32 |
33 | /**
34 | * Apply validation process.
35 | *
36 | * @return bool
37 | */
38 | public function validates()
39 | {
40 | return true;
41 | }
42 |
43 | /**
44 | * Execute action.
45 | *
46 | * @return mixed
47 | */
48 | public function execute()
49 | {
50 | $record = $this->getTable()
51 | ->find()
52 | ->where(['Users.username' => $this->_id])
53 | ->firstOrFail();
54 |
55 | if ($record) {
56 | $Follows = TableRegistry::getTableLocator()->get('Follows');
57 | $current = $Follows->find()->where([
58 | 'follower_id' => $this->Auth->user('id'),
59 | 'followable_id' => $record['id'],
60 | ])->first();
61 | if ($current) {
62 | $Follows->delete($current);
63 | }
64 | }
65 | TableRegistry::getTableLocator()->get('Follows')->unfollow($this->Auth->user('id'), $record['id']);
66 |
67 | return $this->_viewProfile();
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Service/Action/Profile/ProfileViewAction.php:
--------------------------------------------------------------------------------
1 | _table = TableRegistry::getTableLocator()->get('Users');
34 | if ($this->isPublic) {
35 | $this->Auth->allow($this->getName());
36 | }
37 | }
38 |
39 | /**
40 | * Execute action.
41 | *
42 | * @return mixed
43 | */
44 | public function execute()
45 | {
46 | return $this->_viewProfile();
47 | }
48 |
49 | /**
50 | * Builds profile view data.
51 | *
52 | * @return array
53 | */
54 | protected function _viewProfile()
55 | {
56 | $user = $this->Auth->identify();
57 | if ($user) {
58 | $options = [
59 | 'currentUser' => $user['id'],
60 | ];
61 | } else {
62 | $options = [];
63 | }
64 | $record = $this->getTable()
65 | ->find('apiFormat', $options)
66 | ->where(['Users.username' => $this->_id])
67 | ->firstOrFail();
68 |
69 | return ['profile' => $record];
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Service/Action/Tag/TagIndexAction.php:
--------------------------------------------------------------------------------
1 | Auth->allow($this->getName());
31 | }
32 |
33 | /**
34 | * Execute action.
35 | *
36 | * @return mixed
37 | */
38 | public function execute()
39 | {
40 | $this->_finder = 'apiFormat';
41 | $entities = $this->_getEntities()->map(function ($item) {
42 | return $item->tag;
43 | })->toArray();
44 |
45 | return [
46 | 'tags' => $entities,
47 | ];
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Service/Action/User/LoginAction.php:
--------------------------------------------------------------------------------
1 | Auth->allow($this->getName());
47 | }
48 |
49 | /**
50 | * Apply validation process.
51 | *
52 | * @return bool
53 | */
54 | public function validates()
55 | {
56 | $validator = TableRegistry::getTableLocator()->get('Users')->getValidator();
57 | $data = $this->getData();
58 | if (!array_key_exists('user', $data)) {
59 | throw new ValidationException(__('Validation failed'), 0, null, ['user root does not exists']);
60 | }
61 | $data = Hash::get($this->getData(), 'user');
62 | $errors = $validator->errors($data);
63 | if (!empty($errors)) {
64 | throw new ValidationException(__('Validation failed'), 0, null, $errors);
65 | }
66 |
67 | return true;
68 | }
69 |
70 | /**
71 | * Execute action.
72 | *
73 | * @return mixed
74 | */
75 | public function execute()
76 | {
77 | $user = $this->Auth->identify();
78 | if (!empty($user)) {
79 | $this->Auth->setUser($user);
80 | $this->dispatchEvent(UsersAuthComponent::EVENT_AFTER_LOGIN, ['user' => $user]);
81 | $user = TableRegistry::getTableLocator()->get('Users')->loginFormat($user['id']);
82 | } else {
83 | throw new UserNotFoundException(__d('CakeDC/Api', 'User not found'));
84 | }
85 |
86 | return ['user' => $user];
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Service/Action/User/RegisterAction.php:
--------------------------------------------------------------------------------
1 | Auth->allow($this->getName());
33 | $this->setTable(TableRegistry::getTableLocator()->get('Users'));
34 | }
35 |
36 | /**
37 | * Apply validation process.
38 | *
39 | * @return bool
40 | */
41 | public function validates()
42 | {
43 | $validator = $this->getTable()->getValidator('register');
44 | $data = $this->getData();
45 | if (!array_key_exists('user', $data)) {
46 | throw new ValidationException(__('Validation failed'), 0, null, ['user root does not exists']);
47 | }
48 | $data = Hash::get($this->getData(), 'user');
49 | $errors = $validator->errors($data);
50 | if (!empty($errors)) {
51 | throw new ValidationException(__('Validation failed'), 0, null, $errors);
52 | }
53 |
54 | return true;
55 | }
56 |
57 | /**
58 | * Execute action.
59 | *
60 | * @return mixed
61 | */
62 | public function execute()
63 | {
64 | $entity = $this->_newEntity();
65 | $data = Hash::get($this->getData(), 'user');
66 | $entity = $this->_patchEntity($entity, $data, [
67 | 'validate' => 'register',
68 | ]);
69 |
70 | $record = $this->_save($entity);
71 | if ($record) {
72 | $user = $this->getTable()->loginFormat($record['id']);
73 |
74 | return ['user' => $user];
75 | }
76 | }
77 |
78 | /**
79 | * Returns action input params
80 | *
81 | * @return mixed
82 | */
83 | public function getData()
84 | {
85 | $data = parent::getData();
86 | $data['user']['active'] = true;
87 | $data['user']['is_superuser'] = false;
88 | $data['user']['role'] = 'user';
89 |
90 | return $data;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Service/Action/User/UserEditAction.php:
--------------------------------------------------------------------------------
1 | setTable(TableRegistry::getTableLocator()->get('Users'));
33 | }
34 |
35 | /**
36 | * Apply validation process.
37 | *
38 | * @return bool
39 | */
40 | public function validates()
41 | {
42 | $data = $this->getData();
43 | if (!array_key_exists('user', $data)) {
44 | throw new ValidationException(__('Validation failed'), 0, null, ['user root does not exists']);
45 | }
46 | $userId = $this->Auth->user('id');
47 | $entity = $this->_getEntity($userId);
48 | $data = Hash::get($this->getData(), 'user');
49 | $entity = $this->_patchEntity($entity, $data, ['validate' => 'register']);
50 | $errors = $entity->getErrors();
51 | if (!empty($errors)) {
52 | throw new ValidationException(__('Validation failed'), 0, null, $errors);
53 | }
54 |
55 | return true;
56 | }
57 |
58 | /**
59 | * Execute action.
60 | *
61 | * @return mixed
62 | */
63 | public function execute()
64 | {
65 | $userId = $this->Auth->user('id');
66 |
67 | $entity = $this->_getEntity($userId);
68 | $data = Hash::get($this->getData(), 'user');
69 | $entity = $this->_patchEntity($entity, $data);
70 |
71 | $record = $this->_save($entity);
72 | if ($record) {
73 | $user = TableRegistry::getTableLocator()->get('Users')->loginFormat($this->Auth->user('id'));
74 |
75 | return ['user' => $user];
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Service/Action/User/UserViewAction.php:
--------------------------------------------------------------------------------
1 | get('Users')->loginFormat($this->Auth->user('id'));
46 | if (empty($user)) {
47 | throw new UserNotFoundException(__d('CakeDC/Api', 'User not found'));
48 | } else {
49 | return ['user' => $user];
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Service/ArticlesService.php:
--------------------------------------------------------------------------------
1 | '\App\Service\Action\Article\ArticleIndexAction',
31 | 'view' => '\App\Service\Action\Article\ArticleViewAction',
32 | 'add' => '\App\Service\Action\Article\ArticleAddAction',
33 | 'edit' => '\App\Service\Action\Article\ArticleEditAction',
34 | 'delete' => '\App\Service\Action\Article\ArticleDeleteAction',
35 | ];
36 |
37 | /**
38 | * Initialize method
39 | *
40 | * @return void
41 | */
42 | public function initialize()
43 | {
44 | parent::initialize();
45 | $this->mapAction('feed', ArticleFeedAction::class, ['method' => ['GET'], 'mapCors' => true]);
46 | $this->mapAction('favorite', ArticleFavoriteAction::class, [
47 | 'method' => ['POST'],
48 | 'mapCors' => true,
49 | 'path' => ':id/favorite',
50 | ]);
51 | $this->mapAction('unfavorite', ArticleUnfavoriteAction::class, [
52 | 'method' => ['DELETE'],
53 | 'mapCors' => true,
54 | 'path' => ':id/favorite',
55 | ]);
56 | $this->_innerServices = [
57 | 'comments',
58 | ];
59 | }
60 |
61 | /**
62 | * Initialize service level routes
63 | *
64 | * @return void
65 | */
66 | public function loadRoutes()
67 | {
68 | $defaultOptions = $this->routerDefaultOptions();
69 | $defaultOptions['id'] = '[a-z0-9_-]+';
70 | ApiRouter::scope('/', $defaultOptions, function (RouteBuilder $routes) use ($defaultOptions) {
71 | $routes->setExtensions($this->_routeExtensions);
72 | $routes->resources($this->getName(), $defaultOptions, function ($routes) {
73 | if (is_array($this->_routeExtensions)) {
74 | $routes->setExtensions($this->_routeExtensions);
75 | $routes->resources('Comments');
76 | }
77 | });
78 | });
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Service/CommentsService.php:
--------------------------------------------------------------------------------
1 | '\App\Service\Action\Comment\CommentIndexAction',
31 | 'add' => '\App\Service\Action\Comment\CommentAddAction',
32 | 'delete' => '\App\Service\Action\Comment\CommentDeleteAction',
33 | ];
34 |
35 | /**
36 | * Initialize service level routes
37 | *
38 | * @return void
39 | */
40 | public function loadRoutes()
41 | {
42 | $defaultOptions = $this->routerDefaultOptions();
43 | ApiRouter::scope('/', $defaultOptions, function (RouteBuilder $routes) use ($defaultOptions) {
44 | $routes->setExtensions($this->_routeExtensions);
45 | $options = $defaultOptions;
46 | $routes->resources($this->getName(), $options);
47 | });
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Service/ProfilesService.php:
--------------------------------------------------------------------------------
1 | '\App\Service\Action\Profile\ProfileViewAction',
30 | ];
31 |
32 | /**
33 | * Initialize method
34 | *
35 | * @return void
36 | */
37 | public function initialize()
38 | {
39 | parent::initialize();
40 | $this->mapAction('follow', ProfileFollowAction::class, ['method' => ['POST'], 'mapCors' => true, 'path' => ':id/follow']);
41 | $this->mapAction('unfollow', ProfileUnfollowAction::class, ['method' => ['DELETE'], 'mapCors' => true, 'path' => ':id/follow']);
42 | }
43 |
44 | /**
45 | * Initialize service level routes
46 | *
47 | * @return void
48 | */
49 | public function loadRoutes()
50 | {
51 | $defaultOptions = $this->routerDefaultOptions();
52 | $defaultOptions['id'] = '[A-Za-z0-9_-]+';
53 | ApiRouter::scope('/', $defaultOptions, function (RouteBuilder $routes) use ($defaultOptions) {
54 | $routes->setExtensions($this->_routeExtensions);
55 | $routes->resources($this->getName(), $defaultOptions);
56 | });
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Service/Renderer/AppJsonRenderer.php:
--------------------------------------------------------------------------------
1 | _service->getResponse();
39 | $data = $result->getData();
40 | $this->_service->setResponse($response->withStringBody($this->_encode($data))->withStatus($result->getCode())
41 | ->withType('application/json'));
42 |
43 | return true;
44 | }
45 |
46 | /**
47 | * Processes an exception thrown while processing the request.
48 | *
49 | * @param Exception $exception The exception object.
50 | * @return void
51 | */
52 | public function error(Exception $exception)
53 | {
54 | $response = $this->_service->getResponse();
55 | $data = [
56 | 'error' => [
57 | 'code' => $exception->getCode(),
58 | 'message' => $this->_buildMessage($exception),
59 | ],
60 | ];
61 | if ($exception instanceof ValidationException) {
62 | $data['errors'] = [];
63 | unset($data['error']);
64 | foreach ($exception->getValidationErrors() as $field => $errors) {
65 | if (is_array($errors)) {
66 | $data['errors'][$field] = array_values($errors);
67 | } else {
68 | $data['errors'][$field] = [$errors];
69 | }
70 | }
71 | }
72 | $this->_service->setResponse($response
73 | ->withStringBody($this->_encode($data))
74 | ->withType('application/json')
75 | ->withStatus($exception->getCode()));
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Service/TagsService.php:
--------------------------------------------------------------------------------
1 | '\App\Service\Action\Tag\TagIndexAction',
31 | ];
32 | }
33 |
--------------------------------------------------------------------------------
/src/Service/UserService.php:
--------------------------------------------------------------------------------
1 | UserViewAction::class,
28 | ];
29 |
30 | /**
31 | * Initialize method
32 | *
33 | * @return void
34 | */
35 | public function initialize()
36 | {
37 | parent::initialize();
38 | $this->mapAction('update', UserEditAction::class, [
39 | 'method' => ['PUT', 'PATCH'],
40 | 'path' => '',
41 | 'mapCors' => true,
42 | ]);
43 | $this->_table = 'users';
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Service/UsersService.php:
--------------------------------------------------------------------------------
1 | RegisterAction::class,
33 | ];
34 |
35 | /**
36 | * Initialize method
37 | *
38 | * @return void
39 | */
40 | public function initialize()
41 | {
42 | parent::initialize();
43 | $this->mapAction('login', LoginAction::class, ['method' => ['POST'], 'mapCors' => true, 'path' => 'login']);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Shell/ConsoleShell.php:
--------------------------------------------------------------------------------
1 | err('Unable to load Psy\Shell.');
37 | $this->err('');
38 | $this->err('Make sure you have installed psysh as a dependency,');
39 | $this->err('and that Psy\Shell is registered in your autoloader.');
40 | $this->err('');
41 | $this->err('If you are using composer run');
42 | $this->err('');
43 | $this->err('$ php composer.phar require --dev psy/psysh');
44 | $this->err('');
45 |
46 | return self::CODE_ERROR;
47 | }
48 |
49 | $this->out("You can exit with `CTRL-C` or `exit`");
50 | $this->out('');
51 |
52 | Log::drop('debug');
53 | Log::drop('error');
54 | $this->_io->setLoggers(false);
55 | restore_error_handler();
56 | restore_exception_handler();
57 |
58 | $psy = new PsyShell();
59 | $psy->run();
60 | }
61 |
62 | /**
63 | * Display help for this console.
64 | *
65 | * @return \Cake\Console\ConsoleOptionParser
66 | */
67 | public function getOptionParser()
68 | {
69 | $parser = new ConsoleOptionParser('console');
70 | $parser->setDescription(
71 | 'This shell provides a REPL that you can use to interact ' .
72 | 'with your application in an interactive fashion. You can use ' .
73 | 'it to run adhoc queries with your models, or experiment ' .
74 | 'and explore the features of CakePHP and your application.' .
75 | "\n\n" .
76 | 'You will need to have psysh installed for this Shell to work.'
77 | );
78 |
79 | return $parser;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Template/Element/Flash/default.ctp:
--------------------------------------------------------------------------------
1 |
21 |
= $message ?>
22 |
--------------------------------------------------------------------------------
/src/Template/Element/Flash/error.ctp:
--------------------------------------------------------------------------------
1 |
17 | = $message ?>
18 |
--------------------------------------------------------------------------------
/src/Template/Element/Flash/success.ctp:
--------------------------------------------------------------------------------
1 |
17 | = $message ?>
18 |
--------------------------------------------------------------------------------
/src/Template/Error/error400.ctp:
--------------------------------------------------------------------------------
1 | layout = 'error';
17 |
18 | if (Configure::read('debug')):
19 | $this->layout = 'dev_error';
20 |
21 | $this->assign('title', $message);
22 | $this->assign('templateName', 'error400.ctp');
23 |
24 | $this->start('file');
25 | ?>
26 | queryString)) : ?>
27 |
28 | SQL Query:
29 | = h($error->queryString) ?>
30 |
31 |
32 | params)) : ?>
33 | SQL Query Params:
34 | params) ?>
35 |
36 | = $this->element('auto_table_warning') ?>
37 | end();
43 | endif;
44 | ?>
45 | = h($message) ?>
46 |
47 | = __d('cake', 'Error') ?>:
48 | = __d('cake', 'The requested address {0} was not found on this server.', "'{$url}'") ?>
49 |
50 |
--------------------------------------------------------------------------------
/src/Template/Error/error500.ctp:
--------------------------------------------------------------------------------
1 | layout = 'error';
17 |
18 | if (Configure::read('debug')):
19 | $this->layout = 'dev_error';
20 |
21 | $this->assign('title', $message);
22 | $this->assign('templateName', 'error500.ctp');
23 |
24 | $this->start('file');
25 | ?>
26 | queryString)) : ?>
27 |
28 | SQL Query:
29 | = h($error->queryString) ?>
30 |
31 |
32 | params)) : ?>
33 | SQL Query Params:
34 | params) ?>
35 |
36 |
37 | Error in:
38 | = sprintf('%s, line %s', str_replace(ROOT, 'ROOT', $error->getFile()), $error->getLine()) ?>
39 |
40 | element('auto_table_warning');
42 |
43 | if (extension_loaded('xdebug')):
44 | xdebug_print_function_stack();
45 | endif;
46 |
47 | $this->end();
48 | endif;
49 | ?>
50 | = __d('cake', 'An Internal Error Has Occurred') ?>
51 |
52 | = __d('cake', 'Error') ?>:
53 | = h($message) ?>
54 |
55 |
--------------------------------------------------------------------------------
/src/Template/Layout/Email/html/default.ctp:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 | = $this->fetch('title') ?>
20 |
21 |
22 | = $this->fetch('content') ?>
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Template/Layout/Email/text/default.ctp:
--------------------------------------------------------------------------------
1 |
16 | = $this->fetch('content') ?>
17 |
--------------------------------------------------------------------------------
/src/Template/Layout/ajax.ctp:
--------------------------------------------------------------------------------
1 |
16 | = $this->fetch('content') ?>
17 |
--------------------------------------------------------------------------------
/src/Template/Layout/default.ctp:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 | = $this->Html->charset() ?>
22 |
23 |
24 | = $cakeDescription ?>:
25 | = $this->fetch('title') ?>
26 |
27 | = $this->Html->meta('icon') ?>
28 |
29 | = $this->Html->css('base.css') ?>
30 | = $this->Html->css('cake.css') ?>
31 |
32 | = $this->fetch('meta') ?>
33 | = $this->fetch('css') ?>
34 | = $this->fetch('script') ?>
35 |
36 |
37 |
50 | = $this->Flash->render() ?>
51 |
52 | = $this->fetch('content') ?>
53 |
54 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/Template/Layout/error.ctp:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 | = $this->Html->charset() ?>
20 |
21 | = $this->fetch('title') ?>
22 |
23 | = $this->Html->meta('icon') ?>
24 |
25 | = $this->Html->css('base.css') ?>
26 | = $this->Html->css('cake.css') ?>
27 |
28 | = $this->fetch('meta') ?>
29 | = $this->fetch('css') ?>
30 | = $this->fetch('script') ?>
31 |
32 |
33 |
34 |
37 |
38 | = $this->Flash->render() ?>
39 |
40 | = $this->fetch('content') ?>
41 |
42 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/Template/Layout/rss/default.ctp:
--------------------------------------------------------------------------------
1 | fetch('title');
18 | endif;
19 |
20 | echo $this->Rss->document(
21 | $this->Rss->channel(
22 | [], $channel, $this->fetch('content')
23 | )
24 | );
25 | ?>
26 |
--------------------------------------------------------------------------------
/src/Utility/Formatter.php:
--------------------------------------------------------------------------------
1 | format('Y-m-d\TH:i:s.000\Z');
29 | unset($row['created']);
30 | }
31 | if (isset($row['modified']) && $row['modified'] instanceof \DateTimeInterface) {
32 | $row['updatedAt'] = $row['modified']->format('Y-m-d\TH:i:s.000\Z');
33 | unset($row['modified']);
34 | }
35 |
36 | return $row;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/View/AjaxView.php:
--------------------------------------------------------------------------------
1 | response->type('ajax');
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/View/AppView.php:
--------------------------------------------------------------------------------
1 | loadHelper('Html');`
34 | *
35 | * @return void
36 | */
37 | public function initialize()
38 | {
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/Fixture/ArticlesFixture.php:
--------------------------------------------------------------------------------
1 | ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
21 | 'title' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
22 | 'slug' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
23 | 'description' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
24 | 'body' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
25 | 'author_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
26 | 'favorites_count' => ['type' => 'integer', 'length' => 11, 'unsigned' => false, 'null' => false, 'default' => '0', 'comment' => '', 'precision' => null, 'autoIncrement' => null],
27 | 'tag_count' => ['type' => 'integer', 'length' => 11, 'unsigned' => false, 'null' => true, 'default' => '0', 'comment' => '', 'precision' => null, 'autoIncrement' => null],
28 | 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
29 | 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
30 | '_constraints' => [
31 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
32 | ],
33 | ];
34 | // @codingStandardsIgnoreEnd
35 |
36 | /**
37 | * Records
38 | *
39 | * @var array
40 | */
41 | public $records = [];
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Fixture/CommentsFixture.php:
--------------------------------------------------------------------------------
1 | ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
32 | 'body' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
33 | 'article_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
34 | 'author_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
35 | 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
36 | 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
37 | '_constraints' => [
38 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
39 | ],
40 | ];
41 | // @codingStandardsIgnoreEnd
42 |
43 | /**
44 | * Records
45 | *
46 | * @var array
47 | */
48 | public $records = [];
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Fixture/FavoritesFixture.php:
--------------------------------------------------------------------------------
1 | ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
32 | 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
33 | 'article_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
34 | 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
35 | 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
36 | '_constraints' => [
37 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
38 | ],
39 | ];
40 | // @codingStandardsIgnoreEnd
41 |
42 | /**
43 | * Records
44 | *
45 | * @var array
46 | */
47 | public $records = [];
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Fixture/FollowsFixture.php:
--------------------------------------------------------------------------------
1 | ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
32 | 'followable_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
33 | 'follower_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
34 | 'blocked' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
35 | 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
36 | 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
37 | '_constraints' => [
38 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
39 | ],
40 | ];
41 | // @codingStandardsIgnoreEnd
42 |
43 | /**
44 | * Records
45 | *
46 | * @var array
47 | */
48 | public $records = [];
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Fixture/SocialAccountsFixture.php:
--------------------------------------------------------------------------------
1 | ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
32 | 'user_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
33 | 'provider' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
34 | 'username' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
35 | 'reference' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
36 | 'avatar' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
37 | 'description' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
38 | 'link' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
39 | 'token' => ['type' => 'string', 'length' => 500, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
40 | 'token_secret' => ['type' => 'string', 'length' => 500, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
41 | 'token_expires' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
42 | 'active' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => '1', 'comment' => '', 'precision' => null],
43 | 'data' => ['type' => 'text', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
44 | 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
45 | 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
46 | '_indexes' => [
47 | 'user_id' => ['type' => 'index', 'columns' => ['user_id'], 'length' => []],
48 | ],
49 | '_constraints' => [
50 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
51 | 'social_accounts_ibfk_1' => ['type' => 'foreign', 'columns' => ['user_id'], 'references' => ['users', 'id'], 'update' => 'cascade', 'delete' => 'cascade', 'length' => []],
52 | ],
53 | ];
54 | // @codingStandardsIgnoreEnd
55 |
56 | /**
57 | * Records
58 | *
59 | * @var array
60 | */
61 | public $records = [];
62 | }
63 |
--------------------------------------------------------------------------------
/tests/Fixture/TaggedFixture.php:
--------------------------------------------------------------------------------
1 | ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
23 | 'tag_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
24 | 'fk_id' => ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
25 | 'fk_table' => ['type' => 'string', 'limit' => 255, 'null' => false],
26 | 'created' => ['type' => 'datetime', 'null' => true],
27 | 'modified' => ['type' => 'datetime', 'null' => true],
28 | '_constraints' => [
29 | 'primary' => ['type' => 'primary', 'columns' => ['id']],
30 | ],
31 | ];
32 |
33 | /**
34 | * Records
35 | *
36 | * @var array
37 | */
38 | public $records = [];
39 | }
40 | // @codingStandardsIgnoreEnd
41 |
--------------------------------------------------------------------------------
/tests/Fixture/TagsFixture.php:
--------------------------------------------------------------------------------
1 | ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
23 | 'namespace' => ['type' => 'string', 'length' => 255, 'null' => true],
24 | 'tag_key' => ['type' => 'string', 'length' => 255],
25 | 'slug' => ['type' => 'string', 'length' => 255],
26 | 'label' => ['type' => 'string', 'length' => 255],
27 | 'counter' => ['type' => 'integer', 'unsigned' => true, 'default' => 0, 'null' => true],
28 | 'created' => ['type' => 'datetime', 'null' => true],
29 | 'modified' => ['type' => 'datetime', 'null' => true],
30 | '_constraints' => [
31 | 'primary' => ['type' => 'primary', 'columns' => ['id']],
32 | ],
33 | ];
34 |
35 | /**
36 | * Records
37 | *
38 | * @var array
39 | */
40 | public $records = [];
41 | }
42 | // @codingStandardsIgnoreEnd
43 |
--------------------------------------------------------------------------------
/tests/Fixture/UsersFixture.php:
--------------------------------------------------------------------------------
1 | ['type' => 'uuid', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
32 | 'username' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
33 | 'email' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
34 | 'password' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
35 | 'first_name' => ['type' => 'string', 'length' => 50, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
36 | 'last_name' => ['type' => 'string', 'length' => 50, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
37 | 'token' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
38 | 'token_expires' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
39 | 'api_token' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
40 | 'activation_date' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
41 | 'tos_date' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
42 | 'active' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => '0', 'comment' => '', 'precision' => null],
43 | 'is_superuser' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => '0', 'comment' => '', 'precision' => null],
44 | 'role' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => 'user', 'comment' => '', 'precision' => null, 'fixed' => null],
45 | 'secret' => ['type' => 'string', 'length' => 32, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
46 | 'secret_verified' => ['type' => 'boolean', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
47 | 'bio' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
48 | 'image' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
49 | 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
50 | 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null],
51 | '_constraints' => [
52 | 'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
53 | ],
54 | ];
55 | // @codingStandardsIgnoreEnd
56 |
57 | /**
58 | * Records
59 | *
60 | * @var array
61 | */
62 | public $records = [];
63 | }
64 |
--------------------------------------------------------------------------------
/tests/FixturesTrait.php:
--------------------------------------------------------------------------------
1 | useHttpServer(true);
25 |
26 | $users = FactoryLoader::seed(2, 'Users');
27 |
28 | $this->loggedInUser = $users[0];
29 |
30 | $this->user = $users[1];
31 |
32 | $this->headers = [
33 | 'Authorization' => "Token {$this->loggedInUser->token}",
34 | 'Content-Type' => 'application/json',
35 | ];
36 | }
37 |
38 | /**
39 | * Send json request.
40 | *
41 | * @param string $url Request api url.
42 | * @param string $method Request method.
43 | * @param array $data Request data.
44 | * @param EntityInterface $authorizeWithUser User to authorize with.
45 | * @return void
46 | */
47 | public function sendAuthJsonRequest($url, $method, $data = [])
48 | {
49 | $this->sendJsonRequest($url, $method, $data, $this->loggedInUser);
50 | }
51 |
52 | /**
53 | * Send json request.
54 | *
55 | * @param string $url Request api url.
56 | * @param string $method Request method.
57 | * @param array $data Request data.
58 | * @param EntityInterface $authWithUser User to authorize with.
59 | * @return void
60 | */
61 | public function sendJsonRequest($url, $method, $data = [], $authWithUser = null)
62 | {
63 | $headers = [];
64 | if ($method != 'GET' && is_array($data)) {
65 | $data = json_encode($data);
66 | }
67 | $headers['Content-Type'] = 'application/json';
68 | if ($authWithUser !== null) {
69 | $headers['Authorization'] = "Token {$authWithUser->token}";
70 | }
71 | $this->configRequest(['headers' => $headers]);
72 | $this->sendRequest($url, $method, $data);
73 | }
74 |
75 | /**
76 | * @inheritdoc
77 | */
78 | public function tearDown()
79 | {
80 | FactoryLoader::flush();
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tests/TestCase/Service/CommentsServiceTest.php:
--------------------------------------------------------------------------------
1 | article = FactoryLoader::create('Articles', ['author_id' => $this->user->id]);
21 | }
22 |
23 | public function testSuccessAddComment()
24 | {
25 | $data = [
26 | 'comment' => [
27 | 'body' => 'This is a comment',
28 | ],
29 | ];
30 |
31 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}/comments", 'POST', $data);
32 | $this->assertResponseSuccess();
33 |
34 | $this->assertArraySubset([
35 | 'comment' => [
36 | 'body' => 'This is a comment',
37 | 'author' => [
38 | 'username' => $this->loggedInUser->username,
39 | ],
40 | ],
41 | ], $this->getJsonResponse());
42 | }
43 |
44 | public function testRemoveExistsComment()
45 | {
46 | $comment = $this->_generateComment($this->loggedInUser->id);
47 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}/comments/{$comment->id}", 'DELETE');
48 | $this->assertResponseSuccess();
49 |
50 | $this->assertEquals(0, TableRegistry::get('Comments')->find()->where(['article_id' => $this->article->id])->count());
51 | }
52 |
53 | public function testReturnAllArticleComments()
54 | {
55 | $comments = FactoryLoader::seed(2, 'Comments', [
56 | 'author_id' => $this->user->id,
57 | 'article_id' => $this->article->id,
58 | ]);
59 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}/comments", 'GET');
60 | $this->assertResponseSuccess();
61 | $response = $this->getJsonResponse();
62 |
63 | $comments = collection($comments)->sortBy(function ($comment) {
64 | return $comment->created->format('Y-m-d H:i:s');
65 | }, SORT_DESC, SORT_STRING)->toList();
66 |
67 | $this->assertArraySubset([
68 | 'comments' => [
69 | [
70 | 'id' => $comments[0]['id'],
71 | 'body' => $comments[0]['body'],
72 | 'author' => [
73 | 'username' => $this->user->username,
74 | ],
75 | ],
76 | [
77 | 'id' => $comments[1]['id'],
78 | 'body' => $comments[1]['body'],
79 | 'author' => [
80 | 'username' => $this->user->username,
81 | ],
82 | ],
83 | ],
84 | ], $response);
85 | }
86 |
87 | public function testUnauthenticatedErrorOnDeleteIfNotLoggedIn()
88 | {
89 | $comment = $this->_generateComment($this->user->id);
90 | $this->sendJsonRequest("/articles/{$this->article->slug}/comments/{$comment->id}", 'DELETE');
91 | $this->assertStatus(401);
92 |
93 | $this->assertEquals(1, TableRegistry::get('Comments')->find()->where(['article_id' => $this->article->id])->count());
94 | }
95 |
96 | public function testDeleteNotExistsComment()
97 | {
98 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}/comments/b66bfa63-460f-4652-add3-c039b0620b4e", 'DELETE');
99 | $this->assertStatus(404);
100 |
101 | $this->sendAuthJsonRequest("/articles/unknown/comments/b66bfa63-460f-4652-add3-c039b0620b4e", 'DELETE');
102 | $this->assertStatus(404);
103 | }
104 |
105 | public function testListCommentsForNonExistsArticle()
106 | {
107 | $this->sendAuthJsonRequest("/articles/unknown/comments", 'GET');
108 | $this->assertStatus(404);
109 | }
110 |
111 | public function testForbiddenErrorIfDeleteOtherUserComment()
112 | {
113 | $comment = $this->_generateComment($this->user->id);
114 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}/comments/{$comment->id}", 'DELETE');
115 | $this->assertStatus(403);
116 | $this->assertEquals(1, TableRegistry::get('Comments')->find()->where(['article_id' => $this->article->id])->count());
117 | }
118 |
119 | /**
120 | * @param $userId
121 | * @return \Cake\Datasource\EntityInterface
122 | */
123 | protected function _generateComment($userId)
124 | {
125 | $comment = FactoryLoader::create('Comments', [
126 | 'author_id' => $userId,
127 | 'article_id' => $this->article->id,
128 | ]);
129 |
130 | return $comment;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/tests/TestCase/Service/FavoritesServiceTest.php:
--------------------------------------------------------------------------------
1 | article = FactoryLoader::create('Articles', ['author_id' => $this->user->id]);
21 | }
22 |
23 | public function testSuccessAddAndRemoveFavorite()
24 | {
25 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}/favorite", 'POST');
26 | $this->assertResponseSuccess();
27 | $this->assertIsFavorited();
28 |
29 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}/favorite", 'DELETE');
30 | $this->assertResponseSuccess();
31 | $this->assertIsNotFavorited();
32 | }
33 |
34 | public function testFavoriteCountersIsCorrect()
35 | {
36 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}", 'GET');
37 | $this->assertResponseSuccess();
38 | $this->assertIsNotFavorited();
39 |
40 | $Articles = TableRegistry::get('Articles');
41 | $Articles->favorite($this->article->id, $this->user->id);
42 |
43 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}/favorite", 'POST');
44 | $this->assertResponseSuccess();
45 | $this->assertIsFavorited(2);
46 |
47 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}/favorite", 'DELETE');
48 | $this->assertResponseSuccess();
49 | $this->assertIsNotFavorited(1);
50 |
51 | $Articles->unfavorite($this->article->id, $this->user->id);
52 |
53 | $this->sendAuthJsonRequest("/articles/{$this->article->slug}", 'GET');
54 | $this->assertResponseSuccess();
55 | $this->assertIsNotFavorited();
56 | }
57 |
58 | public function testUnauthenticatedErrorIfNotLoggedIn()
59 | {
60 | $this->sendJsonRequest("/articles/{$this->article->slug}/favorite", 'POST');
61 | $this->assertStatus(401);
62 |
63 | $this->sendJsonRequest("/articles/{$this->article->slug}/favorite", 'DELETE');
64 | $this->assertStatus(401);
65 | }
66 |
67 | protected function assertIsFavorited($count = 1)
68 | {
69 | $this->assertArraySubset([
70 | 'article' => [
71 | 'favorited' => true,
72 | 'favoritesCount' => $count,
73 | ],
74 | ], $this->getJsonResponse());
75 | }
76 |
77 | protected function assertIsNotFavorited($count = 0)
78 | {
79 | $this->assertArraySubset([
80 | 'article' => [
81 | 'favorited' => false,
82 | 'favoritesCount' => $count,
83 | ],
84 | ], $this->getJsonResponse());
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/TestCase/Service/LoginTest.php:
--------------------------------------------------------------------------------
1 | [
16 | 'email' => $this->user->email,
17 | 'password' => 'passwd',
18 | ],
19 | ];
20 |
21 | $this->sendJsonRequest("/users/login", 'POST', $data);
22 | $result = $this->getJsonResponse();
23 | $this->assertResponseOk();
24 | $this->assertArraySubset([
25 | 'user' => [
26 | 'email' => $this->user->email,
27 | 'username' => $this->user->username,
28 | 'bio' => $this->user->bio,
29 | 'image' => $this->user->image,
30 | ],
31 | ], $result);
32 |
33 | $this->assertArrayHasKey('token', $result['user'], 'Token not found');
34 | }
35 |
36 | public function testNoDataReturnsValidationErrors()
37 | {
38 | $data = [
39 | 'user' => [],
40 | ];
41 |
42 | $this->sendJsonRequest("/users/login", 'POST', $data);
43 | $this->assertStatus(422);
44 | $this->assertEquals([
45 | 'errors' => [
46 | 'email' => ['This field is required'],
47 | 'password' => ['This field is required'],
48 | ],
49 | ], $this->getJsonResponse());
50 | }
51 |
52 | public function testPreciseValidationErrors()
53 | {
54 | $data = [
55 | 'user' => [
56 | 'email' => 'invalid email',
57 | 'password' => 'secret',
58 | ],
59 | ];
60 |
61 | $this->sendJsonRequest("/users/login", 'POST', $data);
62 | $this->assertStatus(422);
63 | $this->assertEquals([
64 | 'errors' => [
65 | 'email' => ['This field must be a valid email address.'],
66 | ],
67 | ], $this->getJsonResponse());
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/TestCase/Service/ProfilesServiceTest.php:
--------------------------------------------------------------------------------
1 | sendAuthJsonRequest("/profiles/{$this->user->username}", 'GET');
16 | $this->assertResponseOk();
17 |
18 | $this->assertEquals([
19 | 'profile' => [
20 | 'username' => $this->user->username,
21 | 'bio' => $this->user->bio,
22 | 'image' => $this->user->image,
23 | 'following' => false,
24 | ],
25 | ], $this->getJsonResponse());
26 | }
27 |
28 | public function testNotFoundInvalidProfile()
29 | {
30 | $this->sendAuthJsonRequest("/profiles/unknown", 'GET');
31 | $this->assertStatus(404);
32 | }
33 |
34 | public function testFollowAndUnfollow()
35 | {
36 | $this->sendAuthJsonRequest("/profiles/{$this->user->username}/follow", 'POST');
37 | $this->assertResponseSuccess();
38 | $this->assertEquals([
39 | 'profile' => [
40 | 'username' => $this->user->username,
41 | 'bio' => $this->user->bio,
42 | 'image' => $this->user->image,
43 | 'following' => true,
44 | ],
45 | ], $this->getJsonResponse());
46 | $Follows = TableRegistry::get('Follows');
47 | $this->assertTrue($Follows->following($this->loggedInUser->id, $this->user->id), 'Failed to follow user');
48 |
49 | $this->sendAuthJsonRequest("/profiles/{$this->user->username}/follow", 'DELETE');
50 | $this->assertResponseSuccess();
51 | $this->assertEquals([
52 | 'profile' => [
53 | 'username' => $this->user->username,
54 | 'bio' => $this->user->bio,
55 | 'image' => $this->user->image,
56 | 'following' => false,
57 | ],
58 | ], $this->getJsonResponse());
59 | $this->assertFalse($Follows->following($this->loggedInUser->id, $this->user->id), 'Failed to unfollow user');
60 | }
61 |
62 | public function testFollowAndUnfollowNotExistsProfiles()
63 | {
64 | $this->sendAuthJsonRequest("/profiles/unknown/follow", 'POST');
65 | $this->assertStatus(404);
66 |
67 | $this->sendAuthJsonRequest("/profiles/unknown/follow", 'DELETE');
68 | $this->assertStatus(404);
69 | }
70 |
71 | public function testFollowAndUnfollowUnauthorized()
72 | {
73 | $this->sendJsonRequest("/profiles/{$this->user->username}/follow", 'POST');
74 | $this->assertStatus(401);
75 |
76 | $this->sendJsonRequest("/profiles/{$this->user->username}/follow", 'DELETE');
77 | $this->assertStatus(401);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/tests/TestCase/Service/RegisterTest.php:
--------------------------------------------------------------------------------
1 | [
16 | 'username' => 'test',
17 | 'email' => 'test@test.com',
18 | 'password' => 'secret',
19 | ],
20 | ];
21 | $this->sendAuthJsonRequest("/users", 'POST', ($data));
22 | $result = $this->getJsonResponse();
23 | $this->assertResponseOk();
24 | $this->assertArraySubset([
25 | 'user' => [
26 | 'email' => 'test@test.com',
27 | 'username' => 'test',
28 | 'bio' => null,
29 | 'image' => null,
30 | ],
31 | ], $result);
32 | $this->assertArrayHasKey('token', $result['user'], 'Token not found');
33 | }
34 |
35 | public function testNoDataReturnsValidationErrors()
36 | {
37 | $this->sendAuthJsonRequest("/users", 'POST', []);
38 | $this->assertStatus(422);
39 | $this->assertEquals([
40 | 'errors' => [
41 | 'username' => ['This field is required'],
42 | 'email' => ['This field is required'],
43 | 'password' => ['This field is required'],
44 | ],
45 | ], $this->getJsonResponse());
46 | }
47 |
48 | public function testPreciseValidationErrors()
49 | {
50 | $data = [
51 | 'user' => [
52 | 'username' => 'invalid username',
53 | 'email' => 'invalid email',
54 | 'password' => '1',
55 | ],
56 | ];
57 | $this->sendAuthJsonRequest("/users", 'POST', $data);
58 | $this->assertStatus(422, "Status invalid");
59 | $this->assertEquals([
60 | 'errors' => [
61 | 'username' => ['Username may only contain letters and numbers.'],
62 | 'email' => ['This field must be a valid email address.'],
63 | 'password' => ['Password must be at least 6 characters.'],
64 | ],
65 | ], $this->getJsonResponse());
66 | }
67 |
68 | public function testDuplicationValidationErrors()
69 | {
70 | $data = [
71 | 'user' => [
72 | 'username' => $this->user->username,
73 | 'email' => $this->user->email,
74 | 'password' => 'secret',
75 | ],
76 | ];
77 | $this->sendAuthJsonRequest("/users", 'POST', $data);
78 | $this->assertStatus(422);
79 | $this->assertEquals([
80 | 'errors' => [
81 | 'username' => ['Username has already been taken.'],
82 | 'email' => ['Email has already been taken.'],
83 | ],
84 | ], $this->getJsonResponse());
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/TestCase/Service/TagsServiceTest.php:
--------------------------------------------------------------------------------
1 | sendJsonRequest("/tags", 'GET');
18 | $this->assertResponseOk();
19 | $response = $this->getJsonResponse();
20 | sort($response['tags']);
21 | $expected = Hash::extract($tags, '{n}.label');
22 | sort($expected);
23 | $this->assertEquals([
24 | 'tags' => $expected,
25 | ], $response);
26 | }
27 |
28 | public function testEmptyTagList()
29 | {
30 | $this->sendJsonRequest("/tags", 'GET');
31 | $this->assertResponseOk();
32 | $this->assertEquals([
33 | 'tags' => [],
34 | ], $this->getJsonResponse());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/TestCase/Service/UserServiceTest.php:
--------------------------------------------------------------------------------
1 | sendAuthJsonRequest("/user", 'GET');
15 | $this->assertResponseSuccess();
16 |
17 | $this->assertArraySubset([
18 | 'user' => [
19 | 'email' => $this->loggedInUser->email,
20 | 'username' => $this->loggedInUser->username,
21 | 'bio' => $this->loggedInUser->bio,
22 | 'image' => $this->loggedInUser->image,
23 | ],
24 | ], $this->getJsonResponse());
25 | }
26 |
27 | public function testUnauthorizedIdNotLoggedIn()
28 | {
29 | $this->sendJsonRequest("/user", 'GET');
30 | $this->assertStatus(401);
31 | }
32 |
33 | public function testUpdateUser()
34 | {
35 | $data = [
36 | 'user' => [
37 | 'username' => 'user123',
38 | 'email' => 'user123@world.com',
39 | 'password' => 'secretpassword',
40 | 'bio' => 'hello',
41 | 'image' => 'http://image.com/user.jpg',
42 | ],
43 | ];
44 | $this->sendAuthJsonRequest("/user", 'PUT', $data);
45 | $this->assertResponseSuccess();
46 |
47 | $this->assertArraySubset([
48 | 'user' => [
49 | 'username' => 'user123',
50 | 'email' => 'user123@world.com',
51 | 'bio' => 'hello',
52 | 'image' => 'http://image.com/user.jpg',
53 | ],
54 | ], $this->getJsonResponse());
55 |
56 | $this->sendAuthJsonRequest("/user", 'GET');
57 | $this->assertResponseSuccess();
58 | $this->assertArraySubset([
59 | 'user' => [
60 | 'username' => 'user123',
61 | 'email' => 'user123@world.com',
62 | 'bio' => 'hello',
63 | 'image' => 'http://image.com/user.jpg',
64 | ],
65 | ], $this->getJsonResponse());
66 | }
67 |
68 | public function testValidationErrorsOnUpdate()
69 | {
70 | $data = [
71 | 'user' => [
72 | 'username' => 'invalid username',
73 | 'email' => 'invalid email',
74 | 'password' => '1',
75 | 'bio' => 'bio data',
76 | 'image' => 'invalid url',
77 | ],
78 | ];
79 |
80 | $this->sendAuthJsonRequest("/user", 'PUT', $data);
81 | $this->assertStatus(422);
82 |
83 | $this->assertEquals([
84 | 'errors' => [
85 | 'email' => ['This field must be a valid email address.'],
86 | 'password' => ['Password must be at least 6 characters.'],
87 | 'image' => ['Invalid url'],
88 | 'username' => ['Username may only contain letters and numbers.'],
89 | ],
90 | ], $this->getJsonResponse());
91 | }
92 |
93 | public function testValidationErrorsOnUpdateToExistsUsernameOrEmail()
94 | {
95 | $data = [
96 | 'user' => [
97 | 'username' => $this->user->username,
98 | 'email' => $this->user->email,
99 | 'password' => 'passwd',
100 | ],
101 | ];
102 |
103 | $this->sendAuthJsonRequest("/user", 'PUT', $data);
104 | $this->assertStatus(422);
105 | $this->assertEquals([
106 | 'errors' => [
107 | 'username' => ['Username has already been taken.'],
108 | 'email' => ['Email has already been taken.'],
109 | ],
110 | ], $this->getJsonResponse());
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
2 | RewriteEngine On
3 | RewriteCond %{REQUEST_FILENAME} !-f
4 | RewriteRule ^ index.php [L]
5 |
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/webroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/cakephp-realworld-example-app/de154c8c5cbff108b07ad2abe4fc623ec56b5ad8/webroot/favicon.ico
--------------------------------------------------------------------------------
/webroot/font/cakedingbats-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/cakephp-realworld-example-app/de154c8c5cbff108b07ad2abe4fc623ec56b5ad8/webroot/font/cakedingbats-webfont.eot
--------------------------------------------------------------------------------
/webroot/font/cakedingbats-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/cakephp-realworld-example-app/de154c8c5cbff108b07ad2abe4fc623ec56b5ad8/webroot/font/cakedingbats-webfont.ttf
--------------------------------------------------------------------------------
/webroot/font/cakedingbats-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/cakephp-realworld-example-app/de154c8c5cbff108b07ad2abe4fc623ec56b5ad8/webroot/font/cakedingbats-webfont.woff
--------------------------------------------------------------------------------
/webroot/font/cakedingbats-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/cakephp-realworld-example-app/de154c8c5cbff108b07ad2abe4fc623ec56b5ad8/webroot/font/cakedingbats-webfont.woff2
--------------------------------------------------------------------------------
/webroot/img/cake-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/cakephp-realworld-example-app/de154c8c5cbff108b07ad2abe4fc623ec56b5ad8/webroot/img/cake-logo.png
--------------------------------------------------------------------------------
/webroot/img/cake.icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/cakephp-realworld-example-app/de154c8c5cbff108b07ad2abe4fc623ec56b5ad8/webroot/img/cake.icon.png
--------------------------------------------------------------------------------
/webroot/img/cake.logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/webroot/img/cake.power.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/cakephp-realworld-example-app/de154c8c5cbff108b07ad2abe4fc623ec56b5ad8/webroot/img/cake.power.gif
--------------------------------------------------------------------------------
/webroot/index.php:
--------------------------------------------------------------------------------
1 | emit($server->run());
41 |
--------------------------------------------------------------------------------
/webroot/js/empty:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gothinkster/cakephp-realworld-example-app/de154c8c5cbff108b07ad2abe4fc623ec56b5ad8/webroot/js/empty
--------------------------------------------------------------------------------