├── .semver ├── CONTRIBUTING.md ├── Docs ├── Documentation │ ├── Configuration.md │ ├── Installation.md │ └── Overview.md └── Home.md ├── LICENSE.txt ├── README.md ├── composer.json ├── config ├── Migrations │ ├── 20170711111306_Initial.php │ ├── 20170712131022_AddPostsLastReplyCreatedColumn.php │ ├── 20170712202134_AddPostsIsVisibleColumn.php │ ├── 20170718130618_CreateModeratorsTable.php │ ├── 20170720115402_AddReportsCount.php │ ├── 20170720122740_CreateLikesTable.php │ └── 20171011135829_AddCategoriesAndPostsLastReplyId.php ├── Seeds │ ├── CategoriesSeed.php │ ├── EverythingSeed.php │ ├── LikesSeed.php │ ├── ModeratorsSeed.php │ ├── PostsSeed.php │ └── ReportsSeed.php ├── bootstrap.php ├── defaults.php └── routes.php ├── src ├── Controller │ ├── Admin │ │ ├── AppController.php │ │ ├── CategoriesController.php │ │ ├── ModeratorsController.php │ │ ├── RepliesController.php │ │ ├── ReportsController.php │ │ └── ThreadsController.php │ ├── AppController.php │ ├── CategoriesController.php │ ├── LikesController.php │ ├── RepliesController.php │ ├── ReportsController.php │ ├── ThreadsController.php │ └── Traits │ │ └── ForumTrait.php ├── Model │ ├── Behavior │ │ └── VisibleOnlyBehavior.php │ ├── Entity │ │ ├── Category.php │ │ ├── Like.php │ │ ├── Moderator.php │ │ ├── Post.php │ │ ├── Reply.php │ │ ├── Report.php │ │ └── Thread.php │ └── Table │ │ ├── CategoriesTable.php │ │ ├── LikesTable.php │ │ ├── ModeratorsTable.php │ │ ├── PostsTable.php │ │ ├── RepliesTable.php │ │ ├── ReportsTable.php │ │ └── ThreadsTable.php └── Template │ ├── Admin │ ├── Categories │ │ ├── add.ctp │ │ ├── edit.ctp │ │ ├── index.ctp │ │ └── view.ctp │ ├── Moderators │ │ └── add.ctp │ ├── Replies │ │ ├── add.ctp │ │ ├── edit.ctp │ │ └── view.ctp │ ├── Reports │ │ └── index.ctp │ └── Threads │ │ ├── add.ctp │ │ ├── edit.ctp │ │ ├── index.ctp │ │ ├── move.ctp │ │ └── view.ctp │ ├── Categories │ └── index.ctp │ ├── Element │ ├── Admin │ │ ├── Categories │ │ │ ├── form.ctp │ │ │ └── rows.ctp │ │ ├── Moderators │ │ │ └── row.ctp │ │ ├── Replies │ │ │ ├── form.ctp │ │ │ └── row.ctp │ │ ├── Reports │ │ │ └── item.ctp │ │ └── Threads │ │ │ ├── filter.ctp │ │ │ ├── form.ctp │ │ │ └── row.ctp │ ├── Categories │ │ └── row.ctp │ ├── Forum │ │ ├── breadcrumbs.ctp │ │ ├── discussion.ctp │ │ ├── likes.ctp │ │ ├── message.ctp │ │ ├── pagination.ctp │ │ └── username.ctp │ ├── Posts │ │ ├── buttons.ctp │ │ └── item.ctp │ ├── Replies │ │ ├── form.ctp │ │ └── quick.ctp │ ├── Reports │ │ ├── form.ctp │ │ ├── item.ctp │ │ └── row.ctp │ └── Threads │ │ ├── form.ctp │ │ └── row.ctp │ ├── Replies │ ├── add.ctp │ └── edit.ctp │ ├── Reports │ ├── add.ctp │ └── index.ctp │ └── Threads │ ├── add.ctp │ ├── category.ctp │ ├── edit.ctp │ ├── index.ctp │ ├── move.ctp │ ├── my.ctp │ └── view.ctp └── webroot └── empty /.semver: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 1 3 | :minor: 0 4 | :patch: 0 5 | :special: '' 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | This repository follows the [CakeDC Plugin Standard](https://www.cakedc.com/plugin-standard). If you'd like to contribute new features, enhancements or bug fixes to the plugin, please read our [Contribution Guidelines](https://www.cakedc.com/contribution-guidelines) for detailed instructions. 5 | -------------------------------------------------------------------------------- /Docs/Documentation/Configuration.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | Configuration options 5 | --------------------- 6 | 7 | The plugin is configured via the Configure class. Check the `vendor/cakedc/cakephp-forum/config/defaults.php` 8 | for a complete list of all the configuration keys and default values. 9 | 10 | ```php 11 | 'Users', 15 | // Name field in user model to display in forum template 16 | 'userNameField' => 'full_name', 17 | // User profile url (false, array or callable). Username will be displayed with no link if FALSE 18 | 'userProfileUrl' => false, 19 | // Posts count field in User model for CounterCache behavior. Not used if FALSE 20 | 'userPostsCountField' => false, 21 | // Function for messages rendering in forum templates 22 | 'messageRenderer' => function($message) { 23 | return nl2br(h($message)); 24 | }, 25 | // Field name or callable function to check if user is has access to admin interface 26 | 'adminCheck' => 'is_superuser', 27 | // Threads limit per page 28 | 'threadsPerPage' => 20, 29 | // Posts limit per page 30 | 'postsPerPage' => 20, 31 | ]; 32 | ``` 33 | 34 | Overriding the default configuration 35 | ------------------------------------ 36 | 37 | You can override default configuration value in your `config/bootstrap.php`, for example: 38 | 39 | ``` 40 | Plugin::load('CakeDC/Forum', ['routes' => true, 'bootstrap' => true]); 41 | Configure::write('Forum.userModel', 'Accounts'); 42 | ``` 43 | 44 | 45 | Plugin Templates 46 | ---------------- 47 | 48 | To overwrite the template follow the [CakePHP conventions](http://book.cakephp.org/3.0/en/plugins.html#overriding-plugin-templates-from-inside-your-application) and place them to `src/Template/Plugin/CakeDC/Forum/` directory. 49 | 50 | You can copy existing template this way: 51 | 52 | ```bash 53 | cp -r vendor/cakedc/cakephp-forum/src/Template/* src/Template/Plugin/CakeDC/Forum/ 54 | ``` 55 | -------------------------------------------------------------------------------- /Docs/Documentation/Installation.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Composer 5 | -------- 6 | 7 | ```bash 8 | composer require cakedc/cakephp-forum 9 | ``` 10 | 11 | Creating Required Tables 12 | ------------------------ 13 | 14 | ```bash 15 | bin/cake migrations migrate --plugin=CakeDC/Forum 16 | ``` 17 | 18 | You can also seed the database with some sample data: 19 | 20 | ```bash 21 | bin/cake migrations seed --seed=EverythingSeed --plugin=CakeDC/Forum 22 | ``` 23 | 24 | Loading the Plugin 25 | ------------------ 26 | 27 | Ensure the Users Plugin is loaded in your config/bootstrap.php file 28 | 29 | ```php 30 | Plugin::load('CakeDC/Forum', ['bootstrap' => true, 'routes' => true]); 31 | ``` 32 | 33 | Now your Forum index pages should be available under `/forum` URL. Admin interface under `/forum/admin`. 34 | 35 | Configuration 36 | ------------- 37 | 38 | Check the [Configuration](Configuration.md) page for more details. 39 | -------------------------------------------------------------------------------- /Docs/Documentation/Overview.md: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | The plugin itself already provides there features: 5 | * up to 2 levels forum categories tree 6 | * creating threads, posting replies 7 | * reporting messages 8 | * assigning category moderators 9 | * forum admin interface 10 | 11 | It does not cover: 12 | * user registration and login 13 | -------------------------------------------------------------------------------- /Docs/Home.md: -------------------------------------------------------------------------------- 1 | Home 2 | ==== 3 | 4 | Forum plugin allows to create forum categories hierarchy, assign moderators, create thread and replies, report messages. 5 | 6 | It DOES NOT offer users implementation. You can use [CakeDC/Users Plugin](https://github.com/CakeDC/users) or have a look on authentication and authorization implementation in [CakePHP Blog Tutorial](https://book.cakephp.org/3.0/en/tutorials-and-examples/blog-auth-example/auth.html). 7 | 8 | Documentation 9 | ------------- 10 | 11 | * [Overview](Documentation/Overview.md) 12 | * [Installation](Documentation/Installation.md) 13 | * [Configuration](Documentation/Configuration.md) 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2009-2017 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://www.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakeDC Forum Plugin 2 | 3 | [![Build Status](https://img.shields.io/travis/CakeDC/cakephp-forum/master.svg?style=flat-square)](https://travis-ci.org/CakeDC/cakephp-forum) 4 | [![Coverage Status](https://img.shields.io/coveralls/CakeDC/cakephp-forum.svg?style=flat-square)](https://coveralls.io/r/CakeDC/cakephp-forum?branch=master) 5 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) 6 | 7 | It covers the following features: 8 | * up to 2 levels forum categories tree 9 | * creating threads, posting replies 10 | * reporting messages 11 | * assigning category moderators 12 | * forum admin interface 13 | 14 | It does not cover: 15 | * users implementation 16 | 17 | Requirements 18 | ------------ 19 | 20 | * CakePHP 3.4.0+ 21 | * PHP 5.6+ 22 | 23 | Documentation 24 | ------------- 25 | 26 | For documentation, as well as tutorials, see the [Docs](Docs/Home.md) directory of this repository. 27 | 28 | 29 | Demo 30 | ---- 31 | 32 | You can check out [CakeDC Forum demo app](http://cakephp-forum.herokuapp.com/) running on Heroku (in read-only mode) and find it's sources [here](https://github.com/CakeDC/cakephp-forum-demo-app). 33 | 34 | Support 35 | ------- 36 | 37 | For bugs and feature requests, please use the [issues](https://github.com/CakeDC/cakephp-forum/issues) section of this repository. 38 | 39 | Commercial support is also available, [contact us](https://www.cakedc.com/contact) for more information. 40 | 41 | Contributing 42 | ------------ 43 | 44 | This repository follows the [CakeDC Plugin Standard](https://www.cakedc.com/plugin-standard). If you'd like to contribute new features, enhancements or bug fixes to the plugin, please read our [Contribution Guidelines](https://www.cakedc.com/contribution-guidelines) for detailed instructions. 45 | 46 | License 47 | ------- 48 | 49 | Copyright 2017 Cake Development Corporation (CakeDC). All rights reserved. 50 | 51 | Licensed under the [MIT](http://www.opensource.org/licenses/mit-license.php) License. Redistributions of the source code included in this repository must retain the copyright notice found in each file. 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakedc/cakephp-forum", 3 | "description": "Forum Plugin for CakePHP", 4 | "type": "cakephp-plugin", 5 | "keywords": ["cakephp", "forum", "cakedc"], 6 | "homepage": "https://github.com/CakeDC/cakephp-forum", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "CakeDC", 11 | "homepage": "http://www.cakedc.com", 12 | "role": "Author" 13 | }, 14 | { 15 | "name": "Others", 16 | "homepage": "https://github.com/CakeDC/cakephp-forum/graphs/contributors" 17 | } 18 | ], 19 | "support": { 20 | "issues": "https://github.com/CakeDC/cakephp-forum/issues", 21 | "source": "https://github.com/CakeDC/cakephp-forum" 22 | }, 23 | "require": { 24 | "cakephp/cakephp": "^3.4", 25 | "muffin/slug": "^1.1", 26 | "muffin/orderly": "^1.0", 27 | "cakephp/migrations": "@stable" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^5.7|^6.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "CakeDC\\Forum\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "CakeDC\\Forum\\Test\\": "tests", 40 | "Cake\\Test\\": "./vendor/cakephp/cakephp/tests" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /config/Migrations/20170712131022_AddPostsLastReplyCreatedColumn.php: -------------------------------------------------------------------------------- 1 | table('forum_posts') 9 | ->addColumn('last_reply_created', 'datetime', [ 10 | 'default' => null, 11 | 'limit' => null, 12 | 'null' => true, 13 | 'after' => 'is_locked' 14 | ]) 15 | ->addIndex(['last_reply_created']) 16 | ->update(); 17 | } 18 | 19 | public function down() 20 | { 21 | $this->table('forum_posts') 22 | ->dropColumn( 23 | 'last_reply_created' 24 | ) 25 | ->update(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /config/Migrations/20170712202134_AddPostsIsVisibleColumn.php: -------------------------------------------------------------------------------- 1 | table('forum_posts') 11 | ->addColumn('is_visible', 'boolean', [ 12 | 'after' => 'is_locked', 13 | 'default' => '1', 14 | 'length' => null, 15 | 'null' => false, 16 | ]) 17 | ->update(); 18 | } 19 | 20 | public function down() 21 | { 22 | 23 | $this->table('forum_posts') 24 | ->removeColumn('is_visible') 25 | ->update(); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /config/Migrations/20170718130618_CreateModeratorsTable.php: -------------------------------------------------------------------------------- 1 | table('forum_moderators') 10 | ->addColumn('category_id', 'integer', [ 11 | 'default' => null, 12 | 'limit' => 11, 13 | 'null' => false, 14 | ]) 15 | ->addColumn('user_id', 'integer', [ 16 | 'default' => null, 17 | 'limit' => 11, 18 | 'null' => false, 19 | ]) 20 | ->addColumn('created', 'datetime', [ 21 | 'default' => null, 22 | 'limit' => null, 23 | 'null' => false, 24 | ]) 25 | ->addColumn('modified', 'datetime', [ 26 | 'default' => null, 27 | 'limit' => null, 28 | 'null' => false, 29 | ]) 30 | ->addIndex( 31 | [ 32 | 'category_id', 33 | ] 34 | ) 35 | ->addIndex( 36 | [ 37 | 'user_id', 38 | ] 39 | ) 40 | ->create(); 41 | 42 | $this->table('forum_moderators') 43 | ->addForeignKey( 44 | 'category_id', 45 | 'forum_categories', 46 | 'id', 47 | [ 48 | 'update' => 'CASCADE', 49 | 'delete' => 'CASCADE' 50 | ] 51 | ) 52 | ->update(); 53 | } 54 | 55 | public function down() 56 | { 57 | $this->table('forum_moderators') 58 | ->dropForeignKey( 59 | 'category_id' 60 | ); 61 | 62 | $this->dropTable('forum_moderators'); 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /config/Migrations/20170720115402_AddReportsCount.php: -------------------------------------------------------------------------------- 1 | table('forum_posts') 11 | ->addColumn('reports_count', 'integer', [ 12 | 'after' => 'replies_count', 13 | 'default' => null, 14 | 'length' => 11, 15 | 'null' => false, 16 | ]) 17 | ->update(); 18 | } 19 | 20 | public function down() 21 | { 22 | 23 | $this->table('forum_posts') 24 | ->removeColumn('reports_count') 25 | ->update(); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /config/Migrations/20170720122740_CreateLikesTable.php: -------------------------------------------------------------------------------- 1 | table('forum_likes') 11 | ->addColumn('post_id', 'integer', [ 12 | 'default' => null, 13 | 'limit' => 11, 14 | 'null' => false, 15 | ]) 16 | ->addColumn('user_id', 'integer', [ 17 | 'default' => null, 18 | 'limit' => 11, 19 | 'null' => false, 20 | ]) 21 | ->addColumn('created', 'datetime', [ 22 | 'default' => null, 23 | 'limit' => null, 24 | 'null' => false, 25 | ]) 26 | ->addColumn('modified', 'datetime', [ 27 | 'default' => null, 28 | 'limit' => null, 29 | 'null' => false, 30 | ]) 31 | ->addIndex( 32 | [ 33 | 'post_id', 34 | ] 35 | ) 36 | ->addIndex( 37 | [ 38 | 'user_id', 39 | ] 40 | ) 41 | ->create(); 42 | 43 | $this->table('forum_likes') 44 | ->addForeignKey( 45 | 'post_id', 46 | 'forum_posts', 47 | 'id', 48 | [ 49 | 'update' => 'CASCADE', 50 | 'delete' => 'CASCADE' 51 | ] 52 | ) 53 | ->update(); 54 | 55 | $this->table('forum_posts') 56 | ->addColumn('likes_count', 'integer', [ 57 | 'after' => 'reports_count', 58 | 'default' => null, 59 | 'length' => 11, 60 | 'null' => false, 61 | ]) 62 | ->update(); 63 | } 64 | 65 | public function down() 66 | { 67 | $this->table('forum_likes') 68 | ->dropForeignKey( 69 | 'post_id' 70 | ); 71 | 72 | $this->table('forum_posts') 73 | ->removeColumn('likes_count') 74 | ->update(); 75 | 76 | $this->dropTable('forum_likes'); 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /config/Migrations/20171011135829_AddCategoriesAndPostsLastReplyId.php: -------------------------------------------------------------------------------- 1 | table('forum_categories') 18 | ->addColumn('last_post_id', 'integer', [ 19 | 'after' => 'parent_id', 20 | 'default' => null, 21 | 'limit' => 11, 22 | 'null' => true, 23 | ]) 24 | ->addIndex('last_post_id') 25 | ->update(); 26 | 27 | $this->table('forum_posts') 28 | ->addColumn('last_reply_id', 'integer', [ 29 | 'after' => 'category_id', 30 | 'default' => null, 31 | 'limit' => 11, 32 | 'null' => true, 33 | ]) 34 | ->addIndex('last_reply_id') 35 | ->update(); 36 | 37 | $this->table('forum_categories') 38 | ->addForeignKey( 39 | 'last_post_id', 40 | 'forum_posts', 41 | 'id', 42 | [ 43 | 'update' => 'CASCADE', 44 | 'delete' => 'SET_NULL' 45 | ] 46 | ) 47 | ->update(); 48 | 49 | $this->table('forum_posts') 50 | ->addForeignKey( 51 | 'last_reply_id', 52 | 'forum_posts', 53 | 'id', 54 | [ 55 | 'update' => 'CASCADE', 56 | 'delete' => 'SET_NULL' 57 | ] 58 | ) 59 | ->update(); 60 | 61 | $Categories = TableRegistry::get('CakeDC/Forum.Categories'); 62 | $Threads = TableRegistry::get('CakeDC/Forum.Threads'); 63 | $Posts = TableRegistry::get('CakeDC/Forum.Posts'); 64 | $Replies = TableRegistry::get('CakeDC/Forum.Replies'); 65 | 66 | $categories = $Categories->find()->order('Categories.id'); 67 | foreach ($categories as $category) { 68 | if (!$post = $Posts->find()->where(['Posts.category_id' => $category->id])->orderDesc('Posts.id')->first()) { 69 | continue; 70 | } 71 | 72 | $category->last_post_id = $post->id; 73 | $Categories->saveOrFail($category); 74 | } 75 | 76 | $threads = $Threads->find()->order('Threads.id'); 77 | foreach ($threads as $thread) { 78 | if (!$reply = $Replies->find()->where(['Replies.parent_id' => $thread->id])->orderDesc('Replies.id')->first()) { 79 | continue; 80 | } 81 | 82 | $thread->last_reply_id = $reply->id; 83 | $Threads->saveOrFail($thread); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /config/Seeds/CategoriesSeed.php: -------------------------------------------------------------------------------- 1 | '1', 24 | 'parent_id' => NULL, 25 | 'lft' => '11', 26 | 'rght' => '18', 27 | 'title' => 'Hardware and Technology', 28 | 'slug' => 'hardware-and-technology', 29 | 'description' => '', 30 | 'threads_count' => '0', 31 | 'replies_count' => '0', 32 | 'is_visible' => '1', 33 | 'created' => '2017-07-10 20:08:21', 34 | 'modified' => '2017-07-13 15:50:06', 35 | ], 36 | [ 37 | 'id' => '2', 38 | 'parent_id' => '1', 39 | 'lft' => '14', 40 | 'rght' => '15', 41 | 'title' => 'CPUs and Overclocking', 42 | 'slug' => 'cpus-andoverclocking', 43 | 'description' => '', 44 | 'threads_count' => '8', 45 | 'replies_count' => '10', 46 | 'is_visible' => '1', 47 | 'created' => '2017-07-10 20:08:41', 48 | 'modified' => '2017-07-18 14:43:21', 49 | ], 50 | [ 51 | 'id' => '3', 52 | 'parent_id' => NULL, 53 | 'lft' => '1', 54 | 'rght' => '10', 55 | 'title' => 'Consumer Electronics', 56 | 'slug' => 'consumer-electronics', 57 | 'description' => '', 58 | 'threads_count' => '0', 59 | 'replies_count' => '0', 60 | 'is_visible' => '1', 61 | 'created' => '2017-07-10 20:08:58', 62 | 'modified' => '2017-07-13 15:50:28', 63 | ], 64 | [ 65 | 'id' => '4', 66 | 'parent_id' => '1', 67 | 'lft' => '12', 68 | 'rght' => '13', 69 | 'title' => 'Motherboards', 70 | 'slug' => 'motherboards', 71 | 'description' => '', 72 | 'threads_count' => '0', 73 | 'replies_count' => '0', 74 | 'is_visible' => '1', 75 | 'created' => '2017-07-10 20:09:15', 76 | 'modified' => '2017-07-13 15:52:41', 77 | ], 78 | [ 79 | 'id' => '5', 80 | 'parent_id' => NULL, 81 | 'lft' => '19', 82 | 'rght' => '26', 83 | 'title' => 'Software', 84 | 'slug' => 'software', 85 | 'description' => '', 86 | 'threads_count' => '0', 87 | 'replies_count' => '0', 88 | 'is_visible' => '1', 89 | 'created' => '2017-07-13 15:50:37', 90 | 'modified' => '2017-07-13 15:50:37', 91 | ], 92 | [ 93 | 'id' => '6', 94 | 'parent_id' => '1', 95 | 'lft' => '16', 96 | 'rght' => '17', 97 | 'title' => 'Memory and Storage', 98 | 'slug' => 'memory-and-storage', 99 | 'description' => '', 100 | 'threads_count' => '0', 101 | 'replies_count' => '0', 102 | 'is_visible' => '1', 103 | 'created' => '2017-07-13 15:51:28', 104 | 'modified' => '2017-07-13 15:51:49', 105 | ], 106 | [ 107 | 'id' => '7', 108 | 'parent_id' => '3', 109 | 'lft' => '2', 110 | 'rght' => '3', 111 | 'title' => 'Digital and Video Cameras', 112 | 'slug' => 'digital-and-video-cameras', 113 | 'description' => '', 114 | 'threads_count' => '0', 115 | 'replies_count' => '0', 116 | 'is_visible' => '1', 117 | 'created' => '2017-07-13 15:52:10', 118 | 'modified' => '2017-07-13 15:52:33', 119 | ], 120 | [ 121 | 'id' => '8', 122 | 'parent_id' => '3', 123 | 'lft' => '4', 124 | 'rght' => '5', 125 | 'title' => 'Mobile Devices and Gadgets', 126 | 'slug' => 'mobile-devices-and-gadgets', 127 | 'description' => '', 128 | 'threads_count' => '0', 129 | 'replies_count' => '0', 130 | 'is_visible' => '1', 131 | 'created' => '2017-07-13 15:53:49', 132 | 'modified' => '2017-07-13 15:53:49', 133 | ], 134 | [ 135 | 'id' => '9', 136 | 'parent_id' => '3', 137 | 'lft' => '6', 138 | 'rght' => '7', 139 | 'title' => 'Home Theater', 140 | 'slug' => 'home-theater', 141 | 'description' => '', 142 | 'threads_count' => '0', 143 | 'replies_count' => '0', 144 | 'is_visible' => '1', 145 | 'created' => '2017-07-13 15:54:27', 146 | 'modified' => '2017-07-13 15:54:27', 147 | ], 148 | [ 149 | 'id' => '10', 150 | 'parent_id' => '5', 151 | 'lft' => '20', 152 | 'rght' => '21', 153 | 'title' => 'Software for Windows', 154 | 'slug' => 'software-for-windows', 155 | 'description' => '', 156 | 'threads_count' => '0', 157 | 'replies_count' => '0', 158 | 'is_visible' => '1', 159 | 'created' => '2017-07-13 15:54:40', 160 | 'modified' => '2017-07-13 15:54:40', 161 | ], 162 | [ 163 | 'id' => '11', 164 | 'parent_id' => '5', 165 | 'lft' => '22', 166 | 'rght' => '23', 167 | 'title' => 'All Things Apple', 168 | 'slug' => 'all-things-apple', 169 | 'description' => '', 170 | 'threads_count' => '0', 171 | 'replies_count' => '0', 172 | 'is_visible' => '1', 173 | 'created' => '2017-07-13 15:54:50', 174 | 'modified' => '2017-07-13 15:54:50', 175 | ], 176 | [ 177 | 'id' => '12', 178 | 'parent_id' => '5', 179 | 'lft' => '24', 180 | 'rght' => '25', 181 | 'title' => 'Operating Systems', 182 | 'slug' => 'operating-systems', 183 | 'description' => '', 184 | 'threads_count' => '0', 185 | 'replies_count' => '0', 186 | 'is_visible' => '1', 187 | 'created' => '2017-07-13 15:55:02', 188 | 'modified' => '2017-07-13 15:55:02', 189 | ], 190 | [ 191 | 'id' => '13', 192 | 'parent_id' => '3', 193 | 'lft' => '8', 194 | 'rght' => '9', 195 | 'title' => 'Invisible category', 196 | 'slug' => 'invisible-category', 197 | 'description' => '', 198 | 'threads_count' => '0', 199 | 'replies_count' => '0', 200 | 'is_visible' => '0', 201 | 'created' => '2017-07-14 11:57:02', 202 | 'modified' => '2017-07-14 11:57:02', 203 | ], 204 | ]; 205 | 206 | $table = $this->table('forum_categories'); 207 | $table->insert($data)->save(); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /config/Seeds/EverythingSeed.php: -------------------------------------------------------------------------------- 1 | call('CakeDC/Forum.CategoriesSeed'); 22 | $this->call('CakeDC/Forum.PostsSeed'); 23 | $this->call('CakeDC/Forum.ModeratorsSeed'); 24 | $this->call('CakeDC/Forum.ReportsSeed'); 25 | $this->call('CakeDC/Forum.LikesSeed'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /config/Seeds/LikesSeed.php: -------------------------------------------------------------------------------- 1 | '1', 24 | 'post_id' => '1', 25 | 'user_id' => '1', 26 | 'created' => '2017-07-20 12:13:01', 27 | 'modified' => '2017-07-20 12:13:01', 28 | ], 29 | [ 30 | 'id' => '2', 31 | 'post_id' => '2', 32 | 'user_id' => '1', 33 | 'created' => '2017-07-20 12:26:22', 34 | 'modified' => '2017-07-20 12:26:22', 35 | ], 36 | ]; 37 | 38 | $table = $this->table('forum_likes'); 39 | $table->insert($data)->save(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/Seeds/ModeratorsSeed.php: -------------------------------------------------------------------------------- 1 | '2', 24 | 'category_id' => '2', 25 | 'user_id' => '1', 26 | 'created' => '2017-07-19 13:53:20', 27 | 'modified' => '2017-07-19 13:53:20', 28 | ], 29 | ]; 30 | 31 | $table = $this->table('forum_moderators'); 32 | $table->insert($data)->save(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config/Seeds/PostsSeed.php: -------------------------------------------------------------------------------- 1 | '1', 24 | 'parent_id' => NULL, 25 | 'category_id' => '2', 26 | 'user_id' => '1', 27 | 'title' => 'Overclocking CPU/GPU/Memory Stability Testing Guidelines', 28 | 'slug' => 'overclocking-cpu-gpu-memory-stability-testing-guidelines', 29 | 'message' => 'Must run with the IBT thread count set equal to the physical core count of the CPU. 30 | HT slows it down and reduces the ability to determine stability. Set to 4 threads on a 2600K. 31 | Set memory to "All". 32 | Stability Criterion: Must pass 5 cycles minimum, passing 20 cycles is preferred (considered gold standard) 33 | aaa', 34 | 'replies_count' => '3', 35 | 'reports_count' => '1', 36 | 'likes_count' => '1', 37 | 'is_sticky' => '1', 38 | 'is_locked' => '0', 39 | 'is_visible' => '1', 40 | 'last_reply_created' => '2017-07-18 11:41:58', 41 | 'created' => '2017-07-14 11:00:55', 42 | 'modified' => '2017-07-18 12:10:00', 43 | ], 44 | [ 45 | 'id' => '2', 46 | 'parent_id' => '1', 47 | 'category_id' => '2', 48 | 'user_id' => '1', 49 | 'title' => '', 50 | 'slug' => '', 51 | 'message' => 'Like others have said, sticky please! 52 | 53 | About the Prime95 Large FFT, I\'ve gotten crashes on my Phenom II on Blend that I haven\'t gotten on Large FFT. This required me to run it 24 hours with crashes happening on the 20th hour a lot of times. These crashes were fixed by increasing increasing CPU or CPU-NB volts or downing the CPU or CPU-NB clock speeds.', 54 | 'replies_count' => '0', 55 | 'reports_count' => '1', 56 | 'likes_count' => '1', 57 | 'is_sticky' => '0', 58 | 'is_locked' => '0', 59 | 'is_visible' => '1', 60 | 'last_reply_created' => NULL, 61 | 'created' => '2017-07-14 11:01:45', 62 | 'modified' => '2017-07-14 11:01:45', 63 | ], 64 | [ 65 | 'id' => '3', 66 | 'parent_id' => NULL, 67 | 'category_id' => '2', 68 | 'user_id' => '1', 69 | 'title' => '7700K vs 7800X Techspot review', 70 | 'slug' => '7700k-vs-7800x-techspot-review', 71 | 'message' => 'Obviously it is 1080p and will not matter much in 4K, but that is some really bad numbers for the 7800X.', 72 | 'replies_count' => '1', 73 | 'reports_count' => '0', 74 | 'likes_count' => '0', 75 | 'is_sticky' => '0', 76 | 'is_locked' => '1', 77 | 'is_visible' => '1', 78 | 'last_reply_created' => '2017-07-14 11:28:31', 79 | 'created' => '2017-07-14 11:27:10', 80 | 'modified' => '2017-07-14 11:28:42', 81 | ], 82 | [ 83 | 'id' => '4', 84 | 'parent_id' => NULL, 85 | 'category_id' => '2', 86 | 'user_id' => '1', 87 | 'title' => 'Anandtech:Intel\'s Skylake-SP Xeon VS AMD\'s EPYC 7000', 88 | 'slug' => 'anandtech-intels-skylake-sp-xeon-vs-amds-epyc-7000', 89 | 'message' => 'With the exception of database software and vectorizable HPC code, AMD\'s EPYC 7601 ($4200) offers slightly less or slightly better performance than Intel\'s Xeon 8176 ($8000+).', 90 | 'replies_count' => '2', 91 | 'reports_count' => '0', 92 | 'likes_count' => '0', 93 | 'is_sticky' => '0', 94 | 'is_locked' => '0', 95 | 'is_visible' => '1', 96 | 'last_reply_created' => '2017-07-14 11:29:28', 97 | 'created' => '2017-07-14 11:27:39', 98 | 'modified' => '2017-07-14 11:27:39', 99 | ], 100 | [ 101 | 'id' => '5', 102 | 'parent_id' => NULL, 103 | 'category_id' => '2', 104 | 'user_id' => '1', 105 | 'title' => '16C/16T Intel Atom C3955 (Goldmont core) – Denverton platform benchmark leaked', 106 | 'slug' => '16c-16t-intel-atom-c3955-goldmont-core-denverton-platform-benchmark-leaked', 107 | 'message' => 'SiSoftware Sandra 22.20 - Processor Multi-Media - Windows x64 10.0.6 108 | 109 | • Intel Atom(TM) CPU C3955 @ 2.10GHz (16C 2.1GHz, 8x 2MB L2 cache, Goldmont - Denverton 31W TDP): 229.47Mpix/s 110 | source: ranker.sisoftware.net 111 | 112 | for comparation: 113 | 114 | • AMD FX(tm)-8350 Eight-Core Processor (4M 8T 4.84GHz, 2.63GHz IMC, 4x 2MB L2 cache, 8MB L3 cache, Steamroller 125W TDP): 223.16Mpix/s 115 | source: ranker.sisoftware.net', 116 | 'replies_count' => '0', 117 | 'reports_count' => '0', 118 | 'likes_count' => '0', 119 | 'is_sticky' => '0', 120 | 'is_locked' => '0', 121 | 'is_visible' => '1', 122 | 'last_reply_created' => '2017-07-14 11:28:11', 123 | 'created' => '2017-07-14 11:28:11', 124 | 'modified' => '2017-07-14 11:28:11', 125 | ], 126 | [ 127 | 'id' => '6', 128 | 'parent_id' => '3', 129 | 'category_id' => '2', 130 | 'user_id' => '1', 131 | 'title' => '', 132 | 'slug' => '', 133 | 'message' => 'Is this targeted at data center use because it doesn\'t make much sense for anything else. Maybe a hedge against ARM?', 134 | 'replies_count' => '0', 135 | 'reports_count' => '0', 136 | 'likes_count' => '0', 137 | 'is_sticky' => '0', 138 | 'is_locked' => '0', 139 | 'is_visible' => '1', 140 | 'last_reply_created' => NULL, 141 | 'created' => '2017-07-14 11:28:31', 142 | 'modified' => '2017-07-14 11:28:31', 143 | ], 144 | [ 145 | 'id' => '7', 146 | 'parent_id' => '4', 147 | 'category_id' => '2', 148 | 'user_id' => '1', 149 | 'title' => '', 150 | 'slug' => '', 151 | 'message' => 'Those prices though! :( 152 | 153 | Planned on buying a Gold 6000 series, until today. Now I may have to settle for a Silver 4110.', 154 | 'replies_count' => '0', 155 | 'reports_count' => '0', 156 | 'likes_count' => '0', 157 | 'is_sticky' => '0', 158 | 'is_locked' => '0', 159 | 'is_visible' => '1', 160 | 'last_reply_created' => NULL, 161 | 'created' => '2017-07-14 11:29:15', 162 | 'modified' => '2017-07-14 11:29:15', 163 | ], 164 | [ 165 | 'id' => '8', 166 | 'parent_id' => '4', 167 | 'category_id' => '2', 168 | 'user_id' => '1', 169 | 'title' => '', 170 | 'slug' => '', 171 | 'message' => 'Good article good read, glad AMD is finally back in the game.', 172 | 'replies_count' => '0', 173 | 'reports_count' => '0', 174 | 'likes_count' => '0', 175 | 'is_sticky' => '0', 176 | 'is_locked' => '0', 177 | 'is_visible' => '1', 178 | 'last_reply_created' => NULL, 179 | 'created' => '2017-07-14 11:29:28', 180 | 'modified' => '2017-07-14 11:29:28', 181 | ], 182 | ]; 183 | 184 | $table = $this->table('forum_posts'); 185 | $table->insert($data)->save(); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /config/Seeds/ReportsSeed.php: -------------------------------------------------------------------------------- 1 | '2', 24 | 'post_id' => '1', 25 | 'user_id' => '1', 26 | 'message' => 'This thread is very offensive', 27 | 'created' => '2017-07-18 14:42:56', 28 | 'modified' => '2017-07-18 14:42:56', 29 | ], 30 | [ 31 | 'id' => '3', 32 | 'post_id' => '2', 33 | 'user_id' => '1', 34 | 'message' => 'Please remove this post', 35 | 'created' => '2017-07-18 15:03:57', 36 | 'modified' => '2017-07-18 15:03:57', 37 | ], 38 | ]; 39 | 40 | $table = $this->table('forum_reports'); 41 | $table->insert($data)->save(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'Users', 5 | // Name field in user model to display in forum template 6 | 'userNameField' => 'full_name', 7 | // User profile url (false, array or callable). Username will be displayed with no link if FALSE 8 | 'userProfileUrl' => false, 9 | // Posts count field in User model for CounterCache behavior. Not used if FALSE 10 | 'userPostsCountField' => false, 11 | // Function for messages rendering in forum templates 12 | 'messageRenderer' => function($message) { 13 | return nl2br(h($message)); 14 | }, 15 | // Field name or callable function to check if user is has access to admin interface 16 | 'adminCheck' => 'is_superuser', 17 | // Threads limit per page 18 | 'threadsPerPage' => 20, 19 | // Posts limit per page 20 | 'postsPerPage' => 20, 21 | ]; 22 | -------------------------------------------------------------------------------- /config/routes.php: -------------------------------------------------------------------------------- 1 | '/forum'], 9 | function (RouteBuilder $routes) { 10 | $routes->prefix('admin', function (RouteBuilder $routes) { 11 | $routes->fallbacks(DashedRoute::class); 12 | }); 13 | 14 | $routes->connect('/', ['controller' => 'Categories', 'action' => 'index']); 15 | $routes->connect('/admin', ['controller' => 'Categories', 'action' => 'index', 'prefix' => 'admin']); 16 | $routes->connect('/reports', ['controller' => 'Reports', 'action' => 'index'], ['routeClass' => 'InflectedRoute']); 17 | $routes->connect('/reports/:action/*', ['controller' => 'Reports'], ['routeClass' => 'InflectedRoute']); 18 | $routes->connect('/my-conversations', ['controller' => 'Threads', 'action' => 'my']); 19 | $routes->connect('/:category/new-thread', ['controller' => 'Threads', 'action' => 'add'], ['pass' => ['category']]); 20 | $routes->connect('/:category/:thread/edit', ['controller' => 'Threads', 'action' => 'edit'], ['pass' => ['category', 'thread']]); 21 | $routes->connect('/:category/:thread/delete', ['controller' => 'Threads', 'action' => 'delete'], ['pass' => ['category', 'thread']]); 22 | $routes->connect('/:category/:thread/move', ['controller' => 'Threads', 'action' => 'move'], ['pass' => ['category', 'thread']]); 23 | $routes->connect('/:category/:thread/reply', ['controller' => 'Replies', 'action' => 'add'], ['pass' => ['category', 'thread']]); 24 | $routes->connect('/:category/:thread/:reply', ['controller' => 'Replies', 'action' => 'edit'], ['pass' => ['category', 'thread', 'reply'], 'reply' => '\d+']); 25 | $routes->connect('/:category/:thread/:reply/edit', ['controller' => 'Replies', 'action' => 'edit'], ['pass' => ['category', 'thread', 'reply'], 'reply' => '\d+']); 26 | $routes->connect('/:category/:thread/:reply/delete', ['controller' => 'Replies', 'action' => 'delete'], ['pass' => ['category', 'thread', 'reply'], 'reply' => '\d+']); 27 | $routes->connect('/:category/:thread/:post/report', ['controller' => 'Reports', 'action' => 'add'], ['pass' => ['category', 'thread', 'post'], 'post' => '\d+']); 28 | $routes->connect('/:category/:thread/:post/like', ['controller' => 'Likes', 'action' => 'add'], ['pass' => ['category', 'thread', 'post'], 'post' => '\d+']); 29 | $routes->connect('/:category/:thread', ['controller' => 'Threads', 'action' => 'view'], ['pass' => ['category', 'thread']]); 30 | $routes->connect('/:category', ['controller' => 'Threads', 'action' => 'index'], ['pass' => ['category']]); 31 | 32 | $routes->fallbacks(DashedRoute::class); 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /src/Controller/Admin/AppController.php: -------------------------------------------------------------------------------- 1 | Auth->deny(); 38 | 39 | if ($this->Auth->user() && Configure::read('Forum.adminCheck') && !$this->_forumUserIsAdmin()) { 40 | throw new UnauthorizedException(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Controller/Admin/CategoriesController.php: -------------------------------------------------------------------------------- 1 | Categories->find('threaded')->contain(['Moderators.Users']); 32 | 33 | $this->set(compact('categories')); 34 | $this->set('_serialize', ['categories']); 35 | } 36 | 37 | /** 38 | * View method 39 | * 40 | * @param string|null $id Category id. 41 | * @return \Cake\Http\Response|void 42 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 43 | */ 44 | public function view($id = null) 45 | { 46 | $category = $this->Categories->get($id, [ 47 | 'contain' => ['ParentCategories', 'SubCategories', 'Moderators.Users'] 48 | ]); 49 | 50 | $this->set('category', $category); 51 | $this->set('_serialize', ['category']); 52 | } 53 | 54 | /** 55 | * Add method 56 | * 57 | * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise. 58 | */ 59 | public function add() 60 | { 61 | $category = $this->Categories->newEntity(); 62 | if ($this->request->is('post')) { 63 | $category = $this->Categories->patchEntity($category, $this->request->getData()); 64 | if ($this->Categories->save($category)) { 65 | $this->Flash->success(__('The category has been saved.')); 66 | 67 | return $this->redirect(['action' => 'index']); 68 | } 69 | $this->Flash->error(__('The category could not be saved. Please, try again.')); 70 | } 71 | 72 | $categories = $this->Categories->find('list')->where(['parent_id IS' => null])->toArray(); 73 | 74 | $this->set(compact('category', 'categories')); 75 | $this->set('_serialize', ['category', 'categories']); 76 | } 77 | 78 | /** 79 | * Edit method 80 | * 81 | * @param string|null $id Category id. 82 | * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. 83 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 84 | */ 85 | public function edit($id = null) 86 | { 87 | $category = $this->Categories->get($id, [ 88 | 'contain' => [] 89 | ]); 90 | if ($this->request->is(['patch', 'post', 'put'])) { 91 | $category = $this->Categories->patchEntity($category, $this->request->getData()); 92 | if ($this->Categories->save($category)) { 93 | $this->Flash->success(__('The category has been saved.')); 94 | 95 | return $this->redirect(['action' => 'index']); 96 | } 97 | $this->Flash->error(__('The category could not be saved. Please, try again.')); 98 | } 99 | 100 | $categories = $this->Categories->find('list')->where(['parent_id IS' => null])->toArray(); 101 | 102 | $this->set(compact('category', 'categories')); 103 | $this->set('_serialize', ['category', 'categories']); 104 | } 105 | 106 | /** 107 | * Delete method 108 | * 109 | * @param string|null $id Category id. 110 | * @return \Cake\Http\Response|null Redirects to index. 111 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 112 | */ 113 | public function delete($id = null) 114 | { 115 | $this->request->allowMethod(['post', 'delete']); 116 | $category = $this->Categories->get($id); 117 | if ($this->Categories->delete($category)) { 118 | $this->Flash->success(__('The category has been deleted.')); 119 | } else { 120 | $this->Flash->error(__('The category could not be deleted. Please, try again.')); 121 | } 122 | 123 | return $this->redirect(['action' => 'index']); 124 | } 125 | 126 | /** 127 | * Move category up 128 | * 129 | * @param int $id Category id 130 | * @return \Cake\Http\Response|null 131 | */ 132 | public function moveUp($id) 133 | { 134 | $this->request->allowMethod('post'); 135 | 136 | $category = $this->Categories->get($id); 137 | 138 | if ($this->Categories->moveUp($category)) { 139 | $this->Flash->success(__('The category has been moved.')); 140 | } else { 141 | $this->Flash->error(__('The category could not be moved. Please, try again.')); 142 | } 143 | 144 | return $this->redirect(['action' => 'index']); 145 | } 146 | 147 | /** 148 | * Move category down 149 | * 150 | * @param int $id Category id 151 | * @return \Cake\Http\Response|null 152 | */ 153 | public function moveDown($id) 154 | { 155 | $this->request->allowMethod('post'); 156 | 157 | $category = $this->Categories->get($id); 158 | 159 | if ($this->Categories->moveDown($category)) { 160 | $this->Flash->success(__('The category has been moved.')); 161 | } else { 162 | $this->Flash->error(__('The category could not be moved. Please, try again.')); 163 | } 164 | 165 | return $this->redirect(['action' => 'index']); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Controller/Admin/ModeratorsController.php: -------------------------------------------------------------------------------- 1 | Moderators->newEntity(); 32 | if ($this->request->is('post')) { 33 | $moderator = $this->Moderators->patchEntity($moderator, $this->request->getData()); 34 | if ($this->Moderators->save($moderator)) { 35 | $this->Flash->success(__('The moderator has been added.')); 36 | 37 | return $this->redirect(['controller' => 'Categories', 'action' => 'view', $moderator->category_id]); 38 | } 39 | $this->Flash->error(__('The moderator could not be saved. Please, try again.')); 40 | } 41 | 42 | $categories = $this->Moderators->Categories->getOptionsList(true); 43 | $users = $this->Moderators->Users->find('list')->toArray(); 44 | 45 | $this->set(compact('moderator', 'categories', 'users')); 46 | $this->set('_serialize', ['moderator', 'categories', 'users']); 47 | } 48 | 49 | /** 50 | * Delete method 51 | * 52 | * @param string|null $id Post id. 53 | * @return \Cake\Http\Response|null Redirects to index. 54 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 55 | */ 56 | public function delete($id = null) 57 | { 58 | $this->request->allowMethod(['post', 'delete']); 59 | $moderator = $this->Moderators->get($id); 60 | if ($this->Moderators->delete($moderator)) { 61 | $this->Flash->success(__('The moderator has been deleted.')); 62 | } else { 63 | $this->Flash->error(__('The moderator could not be deleted. Please, try again.')); 64 | } 65 | 66 | return $this->redirect(['controller' => 'Categories', 'action' => 'view', $moderator->category_id]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Controller/Admin/RepliesController.php: -------------------------------------------------------------------------------- 1 | Replies->get($id, [ 36 | 'contain' => ['Threads', 'Categories', 'Users'] 37 | ]); 38 | 39 | $this->set(compact('reply')); 40 | $this->set('_serialize', ['reply']); 41 | } 42 | 43 | /** 44 | * Add method 45 | * 46 | * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise. 47 | */ 48 | public function add() 49 | { 50 | $thread = null; 51 | if (!$parentId = $this->request->getQuery('parent_id')) { 52 | throw new BadRequestException(); 53 | } 54 | 55 | $thread = $this->Replies->Threads->get($parentId); 56 | $reply = $this->Replies->newEntity(); 57 | $reply->user_id = $this->Auth->user('id'); 58 | $reply->parent_id = $thread->id; 59 | $reply->category_id = $thread->category_id; 60 | if ($this->request->is('post')) { 61 | $reply = $this->Replies->patchEntity($reply, $this->request->getData()); 62 | if ($this->Replies->save($reply)) { 63 | $this->Flash->success(__('The reply has been saved.')); 64 | 65 | return $this->redirect(['controller' => 'Threads', 'action' => 'view', $reply->parent_id]); 66 | } 67 | $this->Flash->error(__('The reply could not be saved. Please, try again.')); 68 | } 69 | 70 | $this->set(compact('reply', 'thread')); 71 | $this->set('_serialize', ['reply', 'thread']); 72 | } 73 | 74 | /** 75 | * Edit method 76 | * 77 | * @param string|null $id Post id. 78 | * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. 79 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 80 | */ 81 | public function edit($id = null) 82 | { 83 | $reply = $this->Replies->get($id, [ 84 | 'contain' => ['Threads'] 85 | ]); 86 | 87 | $thread = $reply->thread; 88 | 89 | if ($this->request->is(['patch', 'post', 'put'])) { 90 | $reply = $this->Replies->patchEntity($reply, $this->request->getData()); 91 | if ($this->Replies->save($reply)) { 92 | $this->Flash->success(__('The reply has been saved.')); 93 | 94 | return $this->redirect(['controller' => 'Threads', 'action' => 'view', $reply->parent_id]); 95 | } 96 | $this->Flash->error(__('The reply could not be saved. Please, try again.')); 97 | } 98 | 99 | $this->set(compact('reply', 'thread')); 100 | $this->set('_serialize', ['reply', 'thread']); 101 | } 102 | 103 | /** 104 | * Delete method 105 | * 106 | * @param string|null $id Post id. 107 | * @return \Cake\Http\Response|null Redirects to index. 108 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 109 | */ 110 | public function delete($id = null) 111 | { 112 | $this->request->allowMethod(['post', 'delete']); 113 | $reply = $this->Replies->get($id); 114 | if ($this->Replies->delete($reply)) { 115 | $this->Flash->success(__('The reply has been deleted.')); 116 | } else { 117 | $this->Flash->error(__('The reply could not be deleted. Please, try again.')); 118 | } 119 | 120 | return $this->redirect(['controller' => 'Threads', 'action' => 'view', $reply->parent_id]); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Controller/Admin/ReportsController.php: -------------------------------------------------------------------------------- 1 | request->getQueryParams(), array_flip(['post_id', 'thread_id'])); 31 | 32 | $reports = $this->paginate($this->Reports, ['finder' => 'filtered'] + $filter); 33 | 34 | $this->set(compact('reports')); 35 | $this->set('_serialize', ['reports']); 36 | } 37 | 38 | /** 39 | * Delete method 40 | * 41 | * @param int $id Report id 42 | * @return \Cake\Http\Response|null Redirects to index. 43 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 44 | */ 45 | public function delete($id) 46 | { 47 | $this->request->allowMethod(['post', 'delete']); 48 | 49 | $report = $this->Reports->get($id); 50 | 51 | if ($this->Reports->delete($report)) { 52 | $this->Flash->success(__('The report has been deleted.')); 53 | } else { 54 | $this->Flash->error(__('The report could not be deleted. Please, try again.')); 55 | } 56 | 57 | return $this->redirect($this->request->referer()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Controller/Admin/ThreadsController.php: -------------------------------------------------------------------------------- 1 | request->getQuery('category_id')) { 40 | $conditions[$this->Threads->aliasField('category_id')] = $categoryId; 41 | } 42 | if (($isSticky = $this->request->getQuery('is_sticky', '')) !== '') { 43 | $conditions[$this->Threads->aliasField('is_sticky')] = ((int)$isSticky == 1); 44 | } 45 | if (($isLocked = $this->request->getQuery('is_locked', '')) !== '') { 46 | $conditions[$this->Threads->aliasField('is_locked')] = ((int)$isLocked == 1); 47 | } 48 | if (($isVisible = $this->request->getQuery('is_visible', '')) !== '') { 49 | $conditions[$this->Threads->aliasField('is_visible')] = ((int)$isVisible == 1); 50 | } 51 | 52 | $threads = $this->paginate($this->Threads, compact('contain', 'conditions', 'limit', 'group')); 53 | 54 | $categories = $this->Threads->Categories->getOptionsList(true); 55 | 56 | $this->set(compact('threads', 'categories')); 57 | $this->set('_serialize', ['threads', 'categories']); 58 | } 59 | 60 | /** 61 | * View method 62 | * 63 | * @param string|null $id Thread id. 64 | * @return \Cake\Http\Response|void 65 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 66 | */ 67 | public function view($id = null) 68 | { 69 | $thread = $this->Threads->get($id, [ 70 | 'contain' => ['Categories', 'Users', 'Likes.Users'] 71 | ]); 72 | 73 | $replies = []; 74 | if (!$thread->parent_id) { 75 | $replies = $this->paginate( 76 | $this->loadModel('CakeDC/Forum.Replies'), 77 | [ 78 | 'conditions' => [ 79 | $this->Threads->Replies->aliasField('parent_id') => $thread->id 80 | ], 81 | 'contain' => ['Users', 'Likes.Users'], 82 | 'limit' => Configure::read('Forum.postsPerPage'), 83 | ] 84 | ); 85 | } 86 | 87 | $this->set(compact('thread', 'replies')); 88 | $this->set('_serialize', ['thread', 'replies']); 89 | } 90 | 91 | /** 92 | * Add method 93 | * 94 | * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise. 95 | */ 96 | public function add() 97 | { 98 | $thread = $this->Threads->newEntity(); 99 | $thread->user_id = $this->Auth->user('id'); 100 | if ($this->request->is('post')) { 101 | $thread = $this->Threads->patchEntity($thread, $this->request->getData()); 102 | if ($this->Threads->save($thread)) { 103 | $this->Flash->success(__('The thread has been saved.')); 104 | 105 | return $this->redirect(['action' => 'index', '?' => ['category_id' => $thread->category_id]]); 106 | } 107 | $this->Flash->error(__('The thread could not be saved. Please, try again.')); 108 | } 109 | 110 | $categories = $this->Threads->Categories->getOptionsList(true); 111 | 112 | $this->set(compact('thread', 'categories')); 113 | $this->set('_serialize', ['thread', 'categories']); 114 | } 115 | 116 | /** 117 | * Edit method 118 | * 119 | * @param string|null $id Post id. 120 | * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. 121 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 122 | */ 123 | public function edit($id = null) 124 | { 125 | $thread = $this->Threads->get($id); 126 | 127 | if ($this->request->is(['patch', 'post', 'put'])) { 128 | $thread = $this->Threads->patchEntity($thread, $this->request->getData()); 129 | if ($this->Threads->save($thread)) { 130 | $this->Flash->success(__('The thread has been saved.')); 131 | 132 | return $this->redirect(['action' => 'index', '?' => ['category_id' => $thread->category_id]]); 133 | } 134 | $this->Flash->error(__('The thread could not be saved. Please, try again.')); 135 | } 136 | 137 | $categories = $this->Threads->Categories->getOptionsList(true); 138 | 139 | $this->set(compact('thread', 'categories')); 140 | $this->set('_serialize', ['thread', 'categories']); 141 | } 142 | 143 | /** 144 | * Move method 145 | * 146 | * @param string|null $id Post id. 147 | * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. 148 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 149 | */ 150 | public function move($id = null) 151 | { 152 | $thread = $this->Threads->get($id); 153 | 154 | if ($this->request->is(['patch', 'post', 'put'])) { 155 | $thread = $this->Threads->patchEntity($thread, $this->request->getData(), ['validate' => 'moveThread']); 156 | if ($this->Threads->save($thread)) { 157 | $this->Flash->success(__('The thread has been moved.')); 158 | 159 | return $this->redirect(['action' => 'index', '?' => ['category_id' => $thread->category_id]]); 160 | } 161 | $this->Flash->error(__('The thread could not be moved. Please, try again.')); 162 | } 163 | 164 | $categories = $this->Threads->Categories->getOptionsList(true); 165 | 166 | $this->set(compact('thread', 'categories')); 167 | $this->set('_serialize', ['thread', 'categories']); 168 | } 169 | 170 | /** 171 | * Delete method 172 | * 173 | * @param string|null $id Post id. 174 | * @return \Cake\Http\Response|null Redirects to index. 175 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 176 | */ 177 | public function delete($id = null) 178 | { 179 | $this->request->allowMethod(['post', 'delete']); 180 | $thread = $this->Threads->get($id); 181 | if ($this->Threads->delete($thread)) { 182 | $this->Flash->success(__('The thread has been deleted.')); 183 | } else { 184 | $this->Flash->error(__('The thread could not be deleted. Please, try again.')); 185 | } 186 | 187 | return $this->redirect(['action' => 'index', '?' => ['category_id' => $thread->category_id]]); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Controller/AppController.php: -------------------------------------------------------------------------------- 1 | loadModel('CakeDC/Forum.Categories'); 46 | $this->loadModel('CakeDC/Forum.Threads'); 47 | $this->loadModel('CakeDC/Forum.Replies'); 48 | $this->loadModel('CakeDC/Forum.Posts'); 49 | 50 | if (!$this->request->getParam('prefix')) { 51 | $this->Categories->addBehavior('CakeDC/Forum.VisibleOnly'); 52 | $this->Threads->addBehavior('CakeDC/Forum.VisibleOnly'); 53 | $this->Replies->addBehavior('CakeDC/Forum.VisibleOnly'); 54 | $this->Posts->addBehavior('CakeDC/Forum.VisibleOnly'); 55 | } 56 | 57 | /* 58 | * Enable the following components for recommended CakePHP security settings. 59 | * see https://book.cakephp.org/3.0/en/controllers/components/security.html 60 | */ 61 | $this->loadComponent('Security'); 62 | $this->loadComponent('Csrf'); 63 | } 64 | 65 | /** 66 | * beforeFilter callback 67 | * 68 | * @param Event $event Event 69 | * @return \Cake\Http\Response|null|void 70 | */ 71 | public function beforeFilter(Event $event) 72 | { 73 | parent::beforeFilter($event); 74 | 75 | if ($this->request->getParam('prefix') != 'admin') { 76 | $this->set('userInfo', $this->Auth->user()); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Controller/CategoriesController.php: -------------------------------------------------------------------------------- 1 | Auth->deny(); 37 | $this->Auth->allow(['index']); 38 | } 39 | 40 | /** 41 | * Index method 42 | * 43 | * @return \Cake\Http\Response|void 44 | */ 45 | public function index() 46 | { 47 | $categories = $this->Categories 48 | ->find('threaded') 49 | ->where(['OR' => [ 50 | 'Categories.parent_id IS' => null, 51 | 'ParentCategories.is_visible' => true, 52 | ]]) 53 | ->contain(['LastPosts.Users', 'LastPosts.Threads', 'ParentCategories']); 54 | 55 | $forumUserIsModerator = $this->_forumUserIsModerator(); 56 | 57 | $this->set(compact('categories', 'forumUserIsModerator')); 58 | $this->set('_serialize', ['categories', 'forumUserIsModerator']); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Controller/LikesController.php: -------------------------------------------------------------------------------- 1 | Auth->deny(); 39 | } 40 | 41 | /** 42 | * Add method 43 | * 44 | * @param string $categorySlug Category slug 45 | * @param string $threadSlug Thread slug 46 | * @param int $postId Post id 47 | * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise. 48 | */ 49 | public function add($categorySlug, $threadSlug, $postId) 50 | { 51 | $post = $this->_getPost($categorySlug, $threadSlug, $postId); 52 | 53 | if ($this->Likes->find()->where(['user_id' => $this->Auth->user('id'), 'post_id' => $post->id])->first()) { 54 | throw new BadRequestException(); 55 | } 56 | 57 | $like = $this->Likes->newEntity(); 58 | $like->user_id = $this->Auth->user('id'); 59 | $like->post_id = $post->id; 60 | 61 | if ($this->request->is(['post'])) { 62 | $like = $this->Likes->patchEntity($like, $this->request->getData()); 63 | if ($this->Likes->save($like)) { 64 | $this->Flash->success(__('The like has been saved.')); 65 | } else { 66 | $this->Flash->error(__('The like could not be saved. Please, try again.')); 67 | } 68 | } 69 | 70 | return $this->redirect(['controller' => 'Threads', 'action' => 'view', 'category' => $categorySlug, 'thread' => $threadSlug]); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Controller/RepliesController.php: -------------------------------------------------------------------------------- 1 | Auth->deny(); 39 | } 40 | 41 | /** 42 | * Add method 43 | * 44 | * @param string $categorySlug Category slug 45 | * @param string $threadSlug Thread slug 46 | * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise. 47 | */ 48 | public function add($categorySlug, $threadSlug) 49 | { 50 | $thread = $this->_getThread($categorySlug, $threadSlug); 51 | if ($thread->is_locked) { 52 | throw new BadRequestException(); 53 | } 54 | 55 | $reply = $this->Replies->newEntity(); 56 | $reply->user_id = $this->Auth->user('id'); 57 | $reply->set('category', $thread->category); 58 | $reply->set('thread', $thread); 59 | 60 | $this->set(compact('reply')); 61 | 62 | if ($this->request->is(['post'])) { 63 | return $this->_save($reply); 64 | } 65 | } 66 | 67 | /** 68 | * Edit method 69 | * 70 | * @param string $categorySlug Category slug. 71 | * @param string $threadSlug Thread slug. 72 | * @param int $id Reply id. 73 | * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. 74 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 75 | */ 76 | public function edit($categorySlug, $threadSlug, $id) 77 | { 78 | $reply = $this->_getReply($categorySlug, $threadSlug, $id); 79 | 80 | if ($reply->user_id != $this->Auth->user('id')) { 81 | throw new UnauthorizedException(); 82 | } 83 | 84 | if ($this->request->is(['post', 'put', 'patch'])) { 85 | return $this->_save($reply); 86 | } 87 | } 88 | 89 | /** 90 | * Delete method 91 | * 92 | * @param string $categorySlug Category slug. 93 | * @param string $threadSlug Thread slug. 94 | * @param int $id Reply id 95 | * @return \Cake\Http\Response|null Redirects to index. 96 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 97 | */ 98 | public function delete($categorySlug, $threadSlug, $id) 99 | { 100 | $this->request->allowMethod(['post', 'delete']); 101 | 102 | $reply = $this->_getReply($categorySlug, $threadSlug, $id); 103 | 104 | if ($reply->user_id !== $this->Auth->user('id') && !$this->_forumUserIsModerator($reply->category_id)) { 105 | throw new UnauthorizedException(); 106 | } 107 | 108 | if ($this->Replies->delete($reply)) { 109 | $this->Flash->success(__('The reply has been deleted.')); 110 | } else { 111 | $this->Flash->error(__('The reply could not be deleted. Please, try again.')); 112 | } 113 | 114 | return $this->redirect(['controller' => 'Threads', 'action' => 'view', 'category' => $categorySlug, 'thread' => $threadSlug]); 115 | } 116 | 117 | /** 118 | * Save thread 119 | * 120 | * @param \CakeDC\Forum\Model\Entity\Reply $reply Reply 121 | * @return \Cake\Http\Response|null 122 | */ 123 | protected function _save($reply) 124 | { 125 | $reply = $this->Replies->patchEntity($reply, $this->request->getData(), ['fieldList' => ['message']]); 126 | if (!$this->Replies->save($reply)) { 127 | $this->Flash->error(__('The reply could not be saved. Please, try again.')); 128 | 129 | return null; 130 | } 131 | 132 | $this->Flash->success(__('The reply has been saved.')); 133 | 134 | if (!$reply->category || !$reply->thread) { 135 | $this->Replies->loadInto($reply, ['Categories', 'Threads']); 136 | } 137 | 138 | return $this->redirect(['controller' => 'Threads', 'action' => 'view', 'category' => $reply->category->slug, 'thread' => $reply->thread->slug]); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Controller/ReportsController.php: -------------------------------------------------------------------------------- 1 | Auth->deny(); 40 | } 41 | 42 | /** 43 | * Index method 44 | * 45 | * @return \Cake\Http\Response|void 46 | */ 47 | public function index() 48 | { 49 | $filter = array_intersect_key($this->request->getQueryParams(), array_flip(['post_id', 'thread_id'])); 50 | 51 | $this->loadModel('CakeDC/Forum.Moderators'); 52 | if (!$categoryIds = $this->Moderators->getUserCategories($this->Auth->user('id'))) { 53 | throw new UnauthorizedException(); 54 | } 55 | $filter['category_id'] = $categoryIds; 56 | 57 | $reports = $this->paginate($this->Reports, ['finder' => 'filtered'] + $filter); 58 | 59 | $this->set(compact('reports')); 60 | $this->set('_serialize', ['reports']); 61 | } 62 | 63 | /** 64 | * Add method 65 | * 66 | * @param string $categorySlug Category slug 67 | * @param string $threadSlug Thread slug 68 | * @param int $postId Post id 69 | * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise. 70 | */ 71 | public function add($categorySlug, $threadSlug, $postId) 72 | { 73 | $post = $this->_getPost($categorySlug, $threadSlug, $postId); 74 | 75 | if ($this->Reports->find()->where(['user_id' => $this->Auth->user('id'), 'post_id' => $post->id])->first()) { 76 | throw new BadRequestException(); 77 | } 78 | 79 | $report = $this->Reports->newEntity(); 80 | $report->user_id = $this->Auth->user('id'); 81 | $report->post_id = $post->id; 82 | 83 | if ($this->request->is(['post'])) { 84 | $report = $this->Reports->patchEntity($report, $this->request->getData(), ['fieldList' => ['message']]); 85 | if ($this->Reports->save($report)) { 86 | $this->Flash->success(__('The report has been saved.')); 87 | 88 | return $this->redirect(['controller' => 'Threads', 'action' => 'view', 'category' => $categorySlug, 'thread' => $threadSlug]); 89 | } 90 | $this->Flash->error(__('The report could not be saved. Please, try again.')); 91 | } 92 | 93 | $this->set(compact('report')); 94 | } 95 | 96 | /** 97 | * Delete method 98 | * 99 | * @param int $id Report id 100 | * @return \Cake\Http\Response|null Redirects to index. 101 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 102 | */ 103 | public function delete($id) 104 | { 105 | $this->request->allowMethod(['post', 'delete']); 106 | 107 | $report = $this->Reports->get($id, ['contain' => ['Posts']]); 108 | 109 | if (!$this->_forumUserIsModerator($report->post->category_id)) { 110 | throw new UnauthorizedException(); 111 | } 112 | 113 | if ($this->Reports->delete($report)) { 114 | $this->Flash->success(__('The report has been deleted.')); 115 | } else { 116 | $this->Flash->error(__('The report could not be deleted. Please, try again.')); 117 | } 118 | 119 | return $this->redirect($this->request->referer()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Controller/ThreadsController.php: -------------------------------------------------------------------------------- 1 | paginate['Threads'] = [ 40 | 'limit' => Configure::read('Forum.threadsPerPage'), 41 | ]; 42 | 43 | $this->paginate['Posts'] = [ 44 | 'limit' => Configure::read('Forum.postsPerPage'), 45 | ]; 46 | 47 | $this->Auth->allow(); 48 | $this->Auth->deny(['my', 'add', 'edit', 'move', 'delete']); 49 | } 50 | 51 | /** 52 | * List threads in category method 53 | * 54 | * @return \Cake\Http\Response|void 55 | */ 56 | public function index() 57 | { 58 | if (!$categorySlug = func_get_arg(0)) { 59 | throw new BadRequestException(); 60 | } 61 | 62 | $category = $this->_getCategory($categorySlug); 63 | 64 | if ($subCategories = $category->children) { 65 | $this->set('categories', $subCategories); 66 | $this->render('category'); 67 | 68 | return; 69 | } 70 | 71 | $threads = $this->paginate($this->Threads->find('byCategory', ['category_id' => $category->id])); 72 | 73 | $this->set(compact('threads')); 74 | } 75 | 76 | /** 77 | * List threads user has created or participated in 78 | * 79 | * @return \Cake\Http\Response|void 80 | */ 81 | public function my() 82 | { 83 | $threads = $this->paginate($this->Threads->find('byUser', ['user_id' => $this->Auth->user('id')])); 84 | 85 | $this->set(compact('threads')); 86 | } 87 | 88 | /** 89 | * View method 90 | * 91 | * @param string $categorySlug Category slug 92 | * @param string $slug Thread slug 93 | * @return \Cake\Http\Response|void 94 | */ 95 | public function view($categorySlug, $slug) 96 | { 97 | $thread = $this->_getThread($categorySlug, $slug); 98 | 99 | $query = $this->Posts->find('byThread', ['thread_id' => $thread->id]); 100 | if ($userId = $this->Auth->user('id')) { 101 | $query = $query 102 | ->find('withUserReport', ['user_id' => $userId]) 103 | ->find('withUserLike', ['user_id' => $userId]); 104 | } 105 | $posts = $this->paginate($query); 106 | 107 | $page = $this->request->getQuery('page'); 108 | $pageCount = $this->request->getParam('paging.Posts.pageCount'); 109 | if ($page === 'last' && $pageCount !== 1) { 110 | $this->request = $this->request->withQueryParams(['page' => $pageCount]); 111 | $posts = $this->paginate($query); 112 | } 113 | 114 | $reply = $this->Replies->newEntity(); 115 | 116 | $this->set(compact('posts', 'reply')); 117 | } 118 | 119 | /** 120 | * Add method 121 | * 122 | * @param string $categorySlug Category slug 123 | * @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise. 124 | */ 125 | public function add($categorySlug) 126 | { 127 | $category = $this->_getCategory($categorySlug); 128 | if ($category->sub_categories) { 129 | throw new BadRequestException(); 130 | } 131 | 132 | $thread = $this->Threads->newEntity(); 133 | $thread->user_id = $this->Auth->user('id'); 134 | $thread->set('category', $category); 135 | 136 | $this->set(compact('thread')); 137 | 138 | if ($this->request->is(['post'])) { 139 | return $this->_save($thread); 140 | } 141 | } 142 | 143 | /** 144 | * Edit method 145 | * 146 | * @param string $categorySlug Category slug. 147 | * @param string $threadSlug Thread slug. 148 | * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. 149 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 150 | */ 151 | public function edit($categorySlug, $threadSlug) 152 | { 153 | $thread = $this->_getThread($categorySlug, $threadSlug); 154 | if ($thread->user_id != $this->Auth->user('id')) { 155 | throw new UnauthorizedException(); 156 | } 157 | 158 | if ($this->request->is(['post', 'put', 'patch'])) { 159 | return $this->_save($thread); 160 | } 161 | } 162 | 163 | /** 164 | * Move method 165 | * 166 | * @param string $categorySlug Category slug. 167 | * @param string $threadSlug Thread slug. 168 | * @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise. 169 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 170 | */ 171 | public function move($categorySlug, $threadSlug) 172 | { 173 | $thread = $this->_getThread($categorySlug, $threadSlug); 174 | if (!$this->_forumUserIsModerator($thread->category_id)) { 175 | throw new UnauthorizedException(); 176 | } 177 | 178 | $categories = $this->Categories->getOptionsList(true); 179 | 180 | if ($this->request->is(['post', 'put', 'patch'])) { 181 | return $this->_save($thread, ['category_id']); 182 | } 183 | 184 | $this->set(compact('categories')); 185 | } 186 | 187 | /** 188 | * Delete method 189 | * 190 | * @param string $categorySlug Category slug. 191 | * @param string $threadSlug Thread slug. 192 | * @return \Cake\Http\Response|null Redirects to index. 193 | * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. 194 | */ 195 | public function delete($categorySlug, $threadSlug) 196 | { 197 | $this->request->allowMethod(['post', 'delete']); 198 | 199 | $thread = $this->_getThread($categorySlug, $threadSlug); 200 | if ($thread->user_id !== $this->Auth->user('id') && !$this->_forumUserIsModerator($thread->category_id)) { 201 | throw new UnauthorizedException(); 202 | } 203 | 204 | if ($this->Threads->delete($thread)) { 205 | $this->Flash->success(__('The thread has been deleted.')); 206 | } else { 207 | $this->Flash->error(__('The thread could not be deleted. Please, try again.')); 208 | } 209 | 210 | return $this->redirect(['controller' => 'Threads', 'action' => 'index', 'category' => $categorySlug]); 211 | } 212 | 213 | /** 214 | * Save thread 215 | * 216 | * @param \CakeDC\Forum\Model\Entity\Thread $thread Thread 217 | * @param array $fieldList Fields list 218 | * @return \Cake\Http\Response|null 219 | */ 220 | protected function _save($thread, $fieldList = ['title', 'message']) 221 | { 222 | if ($this->_forumUserIsModerator($thread->category_id)) { 223 | $fieldList = array_merge($fieldList, ['is_sticky', 'is_locked']); 224 | } 225 | 226 | $thread = $this->Threads->patchEntity($thread, $this->request->getData(), compact('fieldList')); 227 | $reloadCategory = $thread->isDirty('category_id'); 228 | 229 | if (!$this->Threads->save($thread)) { 230 | $this->Flash->error(__('The thread could not be saved. Please, try again.')); 231 | 232 | return null; 233 | } 234 | 235 | $this->Flash->success(__('The thread has been saved.')); 236 | 237 | if ($reloadCategory) { 238 | $this->Threads->loadInto($thread, ['Categories']); 239 | } 240 | 241 | return $this->redirect(['controller' => 'Threads', 'action' => 'view', 'category' => $thread->category->slug, 'thread' => $thread->slug]); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/Controller/Traits/ForumTrait.php: -------------------------------------------------------------------------------- 1 | Auth->user()) { 30 | return false; 31 | } 32 | 33 | if (!$adminCheck = Configure::read('Forum.adminCheck')) { 34 | return false; 35 | } 36 | 37 | if (is_string($adminCheck) && !Hash::get($user, $adminCheck)) { 38 | return false; 39 | } elseif (is_callable($adminCheck) && !$adminCheck($user)) { 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | /** 47 | * Check if current user is moderator 48 | * 49 | * @param null $categoryId Category id 50 | * @return bool 51 | */ 52 | protected function _forumUserIsModerator($categoryId = null) 53 | { 54 | if ($this->_forumUserIsAdmin()) { 55 | return true; 56 | } 57 | 58 | $this->loadModel('CakeDC/Forum.Moderators'); 59 | 60 | $userCategories = $this->Moderators->getUserCategories($this->Auth->user('id')); 61 | 62 | if (!$categoryId) { 63 | return !empty($userCategories); 64 | } 65 | 66 | return in_array($categoryId, $userCategories); 67 | } 68 | 69 | /** 70 | * Get category by slug 71 | * 72 | * @param string $slug Category slug 73 | * @return \CakeDC\Forum\Model\Entity\Category 74 | */ 75 | protected function _getCategory($slug) 76 | { 77 | /** @var \CakeDC\Forum\Model\Entity\Category $category */ 78 | $category = $this->Categories->find('slugged', compact('slug'))->firstOrFail(); 79 | $category->set('children', $this->Categories->find('children', compact('category'))->toArray()); 80 | 81 | $this->set(compact('category')); 82 | 83 | $this->_getBreadcrumbs($category->id); 84 | 85 | return $category; 86 | } 87 | 88 | /** 89 | * Get breadcrumbs 90 | * 91 | * @param int $categoryId Category id 92 | * @return \CakeDC\Forum\Model\Entity\Category[] 93 | */ 94 | protected function _getBreadcrumbs($categoryId) 95 | { 96 | $breadcrumbs = $this->Categories->find('path', ['for' => $categoryId])->toArray(); 97 | $forumUserIsModerator = $this->_forumUserIsModerator($categoryId); 98 | 99 | $this->set(compact('breadcrumbs', 'forumUserIsModerator')); 100 | 101 | return $breadcrumbs; 102 | } 103 | 104 | /** 105 | * Get thread by category slug and thread slug 106 | * 107 | * @param string $categorySlug Category slug 108 | * @param string $slug Slug 109 | * @return \CakeDC\Forum\Model\Entity\Thread 110 | */ 111 | protected function _getThread($categorySlug, $slug) 112 | { 113 | /** @var \CakeDC\Forum\Model\Entity\Thread $thread */ 114 | $thread = $this->Threads 115 | ->find() 116 | ->contain([ 117 | 'Users', 118 | 'Categories' => function (Query $query) use ($categorySlug) { 119 | return $query->find('slugged', ['slug' => $categorySlug]); 120 | }, 121 | 'Categories.SubCategories' 122 | ]) 123 | ->find('slugged', compact('slug')) 124 | ->firstOrFail(); 125 | 126 | $category = $thread->category; 127 | 128 | $this->_getBreadcrumbs($thread->category_id); 129 | 130 | $this->set(compact('thread', 'category')); 131 | 132 | return $thread; 133 | } 134 | 135 | /** 136 | * Get reply by category slug, thread slug and reply id 137 | * 138 | * @param string $categorySlug Category slug 139 | * @param string $threadSlug Thread slug 140 | * @param int $id Reply id 141 | * @return \CakeDC\Forum\Model\Entity\Reply 142 | */ 143 | protected function _getReply($categorySlug, $threadSlug, $id) 144 | { 145 | /** @var \CakeDC\Forum\Model\Entity\Reply $reply */ 146 | $reply = $this->Replies->get($id, ['finder' => 'byThreadAndCategory'] + compact('categorySlug', 'threadSlug')); 147 | 148 | $category = $reply->category; 149 | $thread = $reply->thread; 150 | 151 | $this->_getBreadcrumbs($reply->category_id); 152 | 153 | $this->set(compact('thread', 'category', 'reply')); 154 | 155 | return $reply; 156 | } 157 | 158 | /** 159 | * Get post by category slug, thread slug and post id 160 | * 161 | * @param string $categorySlug Category slug 162 | * @param string $threadSlug Thread slug 163 | * @param int $id Post id 164 | * @return \CakeDC\Forum\Model\Entity\Reply 165 | */ 166 | protected function _getPost($categorySlug, $threadSlug, $id) 167 | { 168 | $thread = $this->_getThread($categorySlug, $threadSlug); 169 | $post = $this->Posts->get($id, ['finder' => 'byThread', 'thread_id' => $thread->id]); 170 | 171 | $this->set(compact('post')); 172 | 173 | return $post; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Model/Behavior/VisibleOnlyBehavior.php: -------------------------------------------------------------------------------- 1 | 'is_visible', 31 | ]; 32 | 33 | /** 34 | * beforeFind callback 35 | * 36 | * @param \Cake\Event\Event $event Event 37 | * @param \Cake\ORM\Query $query Query 38 | * @param ArrayObject $options Options 39 | * @param bool $primary Primary 40 | */ 41 | public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary) 42 | { 43 | $query->where([$this->getTable()->aliasField($this->getConfig('field')) => true]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Model/Entity/Category.php: -------------------------------------------------------------------------------- 1 | true, 54 | 'id' => false 55 | ]; 56 | } 57 | -------------------------------------------------------------------------------- /src/Model/Entity/Like.php: -------------------------------------------------------------------------------- 1 | true, 42 | 'id' => false 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /src/Model/Entity/Moderator.php: -------------------------------------------------------------------------------- 1 | true, 42 | 'id' => false 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /src/Model/Entity/Post.php: -------------------------------------------------------------------------------- 1 | false, 61 | ]; 62 | 63 | /** 64 | * Get title 65 | */ 66 | protected function _getTitle() 67 | { 68 | if ($this->parent_id) { 69 | return Hash::get($this->_properties, 'thread.title'); 70 | } 71 | 72 | return Hash::get($this->_properties, 'title'); 73 | } 74 | 75 | /** 76 | * Get slug 77 | */ 78 | protected function _getSlug() 79 | { 80 | if ($this->parent_id) { 81 | return Hash::get($this->_properties, 'thread.slug'); 82 | } 83 | 84 | return Hash::get($this->_properties, 'slug'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Model/Entity/Reply.php: -------------------------------------------------------------------------------- 1 | false, 53 | 'message' => true, 54 | ]; 55 | } 56 | -------------------------------------------------------------------------------- /src/Model/Entity/Report.php: -------------------------------------------------------------------------------- 1 | true, 43 | 'id' => false 44 | ]; 45 | } 46 | -------------------------------------------------------------------------------- /src/Model/Entity/Thread.php: -------------------------------------------------------------------------------- 1 | false, 59 | 'category_id' => true, 60 | 'title' => true, 61 | 'slug' => true, 62 | 'message' => true, 63 | 'is_sticky' => true, 64 | 'is_locked' => true, 65 | 'is_visible' => true, 66 | ]; 67 | 68 | /** 69 | * is_reported getter 70 | * 71 | * @return bool 72 | */ 73 | protected function _getIsReported() 74 | { 75 | return $this->reported_reply || $this->reports_count; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Model/Table/CategoriesTable.php: -------------------------------------------------------------------------------- 1 | setTable('forum_categories'); 53 | $this->setDisplayField('title'); 54 | $this->setPrimaryKey('id'); 55 | 56 | $this->addBehavior('Timestamp'); 57 | $this->addBehavior('Tree'); 58 | $this->addBehavior('Muffin/Slug.Slug'); 59 | $this->addBehavior('Muffin/Orderly.Orderly', ['order' => $this->aliasField('lft')]); 60 | 61 | $this->hasMany('Threads', [ 62 | 'className' => 'CakeDC/Forum.Threads', 63 | 'conditions' => [ 64 | 'Threads.parent_id IS' => null, 65 | ], 66 | ]); 67 | $this->belongsTo('LastPosts', [ 68 | 'className' => 'CakeDC/Forum.Posts', 69 | 'foreignKey' => 'last_post_id', 70 | ]); 71 | $this->belongsTo('ParentCategories', [ 72 | 'className' => 'CakeDC/Forum.Categories', 73 | 'foreignKey' => 'parent_id', 74 | ]); 75 | $this->hasMany('SubCategories', [ 76 | 'className' => 'CakeDC/Forum.Categories', 77 | 'foreignKey' => 'parent_id', 78 | ]); 79 | $this->hasMany('Moderators', [ 80 | 'className' => 'CakeDC/Forum.Moderators', 81 | 'foreignKey' => 'category_id', 82 | ]); 83 | } 84 | 85 | /** 86 | * Default validation rules. 87 | * 88 | * @param \Cake\Validation\Validator $validator Validator instance. 89 | * @return \Cake\Validation\Validator 90 | */ 91 | public function validationDefault(Validator $validator) 92 | { 93 | $validator 94 | ->integer('id') 95 | ->allowEmpty('id', 'create'); 96 | 97 | $validator 98 | ->requirePresence('title', 'create') 99 | ->notEmpty('title'); 100 | 101 | $validator 102 | ->allowEmpty('slug'); 103 | 104 | $validator 105 | ->allowEmpty('description'); 106 | 107 | $validator 108 | ->integer('threads_count'); 109 | 110 | $validator 111 | ->integer('replies_count'); 112 | 113 | $validator 114 | ->boolean('is_visible') 115 | ->requirePresence('is_visible', 'create') 116 | ->notEmpty('is_visible'); 117 | 118 | return $validator; 119 | } 120 | 121 | /** 122 | * Returns a rules checker object that will be used for validating 123 | * application integrity. 124 | * 125 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 126 | * @return \Cake\ORM\RulesChecker 127 | */ 128 | public function buildRules(RulesChecker $rules) 129 | { 130 | $rules->add($rules->existsIn(['parent_id'], 'ParentCategories', ['allowNullableNulls' => true])); 131 | 132 | return $rules; 133 | } 134 | 135 | /** 136 | * Get options list for dropdown 137 | * 138 | * @param bool $grouped Grouped 139 | * @return array 140 | */ 141 | public function getOptionsList($grouped = false) 142 | { 143 | $categories = $this->find()->all()->nest('id', 'parent_id'); 144 | 145 | if ($grouped) { 146 | $result = []; 147 | foreach ($categories->toArray() as $category) { 148 | if ($category->children) { 149 | $result[$category->title] = collection($category->children)->indexBy('id')->extract('title')->toArray(); 150 | } else { 151 | $result[$category->id] = $category->title; 152 | } 153 | } 154 | 155 | return $result; 156 | } else { 157 | return $categories 158 | ->listNested() 159 | ->printer('title', 'id', '     ') 160 | ->toArray(); 161 | } 162 | } 163 | 164 | /** 165 | * Find category children 166 | * 167 | * @param \Cake\ORM\Query $query The query builder. 168 | * @param array $options Options. 169 | * @return \Cake\ORM\Query 170 | */ 171 | public function findChildren(Query $query, $options = []) 172 | { 173 | if (!$category = Hash::get($options, 'category')) { 174 | throw new \InvalidArgumentException('Category is required'); 175 | } 176 | 177 | return $query 178 | ->where([ 179 | $query->newExpr()->gt('lft', $category->get('lft')), 180 | $query->newExpr()->lt('rght', $category->get('rght')), 181 | ]) 182 | ->contain(['LastPosts.Users', 'LastPosts.Threads']); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Model/Table/LikesTable.php: -------------------------------------------------------------------------------- 1 | setTable('forum_likes'); 49 | $this->setDisplayField('id'); 50 | $this->setPrimaryKey('id'); 51 | 52 | $this->addBehavior('Timestamp'); 53 | $this->addBehavior('CounterCache', [ 54 | 'Posts' => [ 55 | 'likes_count' 56 | ], 57 | ]); 58 | 59 | $this->belongsTo('Posts', [ 60 | 'className' => 'CakeDC/Forum.Posts', 61 | 'joinType' => 'INNER' 62 | ]); 63 | $this->belongsTo('Users', [ 64 | 'className' => Configure::read('Forum.userModel'), 65 | 'joinType' => 'INNER' 66 | ]); 67 | } 68 | 69 | /** 70 | * Default validation rules. 71 | * 72 | * @param \Cake\Validation\Validator $validator Validator instance. 73 | * @return \Cake\Validation\Validator 74 | */ 75 | public function validationDefault(Validator $validator) 76 | { 77 | $validator 78 | ->integer('id') 79 | ->allowEmpty('id', 'create'); 80 | 81 | return $validator; 82 | } 83 | 84 | /** 85 | * Returns a rules checker object that will be used for validating 86 | * application integrity. 87 | * 88 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 89 | * @return \Cake\ORM\RulesChecker 90 | */ 91 | public function buildRules(RulesChecker $rules) 92 | { 93 | $rules->add($rules->existsIn(['post_id'], 'Posts')); 94 | $rules->add($rules->existsIn(['user_id'], 'Users')); 95 | 96 | return $rules; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Model/Table/ModeratorsTable.php: -------------------------------------------------------------------------------- 1 | setTable('forum_moderators'); 49 | $this->setDisplayField('id'); 50 | $this->setPrimaryKey('id'); 51 | 52 | $this->addBehavior('Timestamp'); 53 | 54 | $this->belongsTo('Categories', [ 55 | 'className' => 'CakeDC/Forum.Categories', 56 | 'joinType' => 'INNER' 57 | ]); 58 | $this->belongsTo('Users', [ 59 | 'className' => Configure::read('Forum.userModel'), 60 | 'joinType' => 'INNER' 61 | ]); 62 | } 63 | 64 | /** 65 | * Default validation rules. 66 | * 67 | * @param \Cake\Validation\Validator $validator Validator instance. 68 | * @return \Cake\Validation\Validator 69 | */ 70 | public function validationDefault(Validator $validator) 71 | { 72 | $validator 73 | ->integer('id') 74 | ->allowEmpty('id', 'create'); 75 | 76 | $validator 77 | ->integer('user_id') 78 | ->notEmpty('user_id'); 79 | 80 | return $validator; 81 | } 82 | 83 | /** 84 | * Returns a rules checker object that will be used for validating 85 | * application integrity. 86 | * 87 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 88 | * @return \Cake\ORM\RulesChecker 89 | */ 90 | public function buildRules(RulesChecker $rules) 91 | { 92 | $rules->add($rules->existsIn(['category_id'], 'Categories')); 93 | $rules->add($rules->existsIn(['user_id'], 'Users')); 94 | 95 | return $rules; 96 | } 97 | 98 | /** 99 | * Get user categories 100 | * 101 | * @param int $userId User id 102 | * @return array 103 | */ 104 | public function getUserCategories($userId) 105 | { 106 | return $this->find() 107 | ->where(['user_id' => $userId]) 108 | ->extract('category_id') 109 | ->toArray(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Model/Table/PostsTable.php: -------------------------------------------------------------------------------- 1 | setTable('forum_posts'); 51 | $this->setDisplayField('title'); 52 | $this->setPrimaryKey('id'); 53 | 54 | $this->addBehavior('Timestamp'); 55 | $this->addBehavior('Muffin/Orderly.Orderly', ['order' => $this->aliasField('id')]); 56 | 57 | $this->belongsTo('Threads', [ 58 | 'className' => 'CakeDC/Forum.Threads', 59 | 'foreignKey' => 'parent_id' 60 | ]); 61 | $this->belongsTo('Categories', [ 62 | 'className' => 'CakeDC/Forum.Categories', 63 | 'joinType' => 'INNER' 64 | ]); 65 | $this->belongsTo('Users', [ 66 | 'className' => Configure::read('Forum.userModel'), 67 | ]); 68 | $this->hasOne('UserLikes', [ 69 | 'className' => 'CakeDC/Forum.Likes', 70 | 'foreignKey' => 'post_id' 71 | ]); 72 | $this->hasOne('UserReports', [ 73 | 'className' => 'CakeDC/Forum.Reports', 74 | 'foreignKey' => 'post_id' 75 | ]); 76 | $this->hasMany('Likes', [ 77 | 'className' => 'CakeDC/Forum.Likes', 78 | 'foreignKey' => 'post_id' 79 | ]); 80 | } 81 | 82 | /** 83 | * Find posts by thread 84 | * 85 | * @param \Cake\ORM\Query $query The query builder. 86 | * @param array $options Options. 87 | * @return \Cake\ORM\Query 88 | */ 89 | public function findByThread(Query $query, $options = []) 90 | { 91 | if (!$parentId = Hash::get($options, 'thread_id')) { 92 | throw new \InvalidArgumentException('thread_id is required'); 93 | } 94 | 95 | return $query 96 | ->where([ 97 | 'OR' => [ 98 | $this->aliasField('id') => $parentId, 99 | $this->aliasField('parent_id') => $parentId, 100 | ], 101 | ]) 102 | ->contain(['Users', 'Likes.Users']); 103 | } 104 | 105 | /** 106 | * Find posts with user report 107 | * 108 | * @param \Cake\ORM\Query $query The query builder. 109 | * @param array $options Options. 110 | * @return \Cake\ORM\Query 111 | */ 112 | public function findWithUserReport(Query $query, $options = []) 113 | { 114 | if (!$userId = Hash::get($options, 'user_id')) { 115 | throw new \InvalidArgumentException('user_id is required'); 116 | } 117 | 118 | return $query->contain(['UserReports' => function (Query $q) use ($userId) { 119 | return $q->where(['UserReports.user_id' => $userId]); 120 | }]); 121 | } 122 | 123 | /** 124 | * Find posts with user like 125 | * 126 | * @param \Cake\ORM\Query $query The query builder. 127 | * @param array $options Options. 128 | * @return \Cake\ORM\Query 129 | */ 130 | public function findWithUserLike(Query $query, $options = []) 131 | { 132 | if (!$userId = Hash::get($options, 'user_id')) { 133 | throw new \InvalidArgumentException('user_id is required'); 134 | } 135 | 136 | return $query->contain(['UserLikes' => function (Query $q) use ($userId) { 137 | return $q->where(['UserLikes.user_id' => $userId]); 138 | }]); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Model/Table/RepliesTable.php: -------------------------------------------------------------------------------- 1 | setTable('forum_posts'); 57 | $this->setDisplayField('message'); 58 | $this->setPrimaryKey('id'); 59 | 60 | $this->belongsTo('Threads', [ 61 | 'className' => 'CakeDC/Forum.Threads', 62 | 'foreignKey' => 'parent_id' 63 | ]); 64 | $this->belongsTo('Categories', [ 65 | 'className' => 'CakeDC/Forum.Categories', 66 | 'joinType' => 'INNER' 67 | ]); 68 | $this->hasMany('Reports', [ 69 | 'className' => 'CakeDC/Forum.Reports', 70 | 'foreignKey' => 'post_id' 71 | ]); 72 | $this->hasMany('Likes', [ 73 | 'className' => 'CakeDC/Forum.Likes', 74 | 'foreignKey' => 'post_id' 75 | ]); 76 | $this->belongsTo('Users', [ 77 | 'className' => Configure::read('Forum.userModel'), 78 | 'joinType' => 'INNER' 79 | ]); 80 | 81 | $this->addBehavior('Timestamp'); 82 | $this->addBehavior('Muffin/Orderly.Orderly', ['order' => $this->aliasField('id')]); 83 | 84 | $options = [ 85 | 'Categories' => [ 86 | 'replies_count', 87 | 'last_post_id' => function ($event, Reply $entity, RepliesTable $table) { 88 | $Posts = TableRegistry::get('CakeDC/Forum.Posts'); 89 | if (!$lastPost = $Posts->find()->where(['category_id' => $entity->category_id])->orderDesc('id')->first()) { 90 | return null; 91 | } 92 | 93 | return $lastPost->id; 94 | }, 95 | ], 96 | 'Threads' => [ 97 | 'last_reply_created' => function ($event, Reply $entity, RepliesTable $table) { 98 | if (!$lastReply = $table->find()->where(['parent_id' => $entity->parent_id])->orderDesc('id')->first()) { 99 | return $this->Threads->get($entity->parent_id)->created; 100 | } 101 | 102 | return $lastReply->get('created'); 103 | }, 104 | 'last_reply_id' => function ($event, Reply $entity, RepliesTable $table) { 105 | if (!$lastReply = $table->find()->where(['parent_id' => $entity->parent_id])->orderDesc('id')->first()) { 106 | return null; 107 | } 108 | 109 | return $lastReply->id; 110 | }, 111 | 'replies_count', 112 | ], 113 | ]; 114 | if ($userPostsCountField = Configure::read('Forum.userPostsCountField')) { 115 | $options['Users'] = [$userPostsCountField => ['all' => true]]; 116 | } 117 | $this->addBehavior('CounterCache', $options); 118 | } 119 | 120 | /** 121 | * Default validation rules. 122 | * 123 | * @param \Cake\Validation\Validator $validator Validator instance. 124 | * @return \Cake\Validation\Validator 125 | */ 126 | public function validationDefault(Validator $validator) 127 | { 128 | $validator 129 | ->integer('id') 130 | ->allowEmpty('id', 'create'); 131 | 132 | $validator 133 | ->requirePresence('message', 'create') 134 | ->notEmpty('message'); 135 | 136 | return $validator; 137 | } 138 | 139 | /** 140 | * Returns a rules checker object that will be used for validating 141 | * application integrity. 142 | * 143 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 144 | * @return \Cake\ORM\RulesChecker 145 | */ 146 | public function buildRules(RulesChecker $rules) 147 | { 148 | $rules->add($rules->existsIn(['parent_id'], 'Threads')); 149 | $rules->add($rules->existsIn(['category_id'], 'Categories')); 150 | $rules->add($rules->existsIn(['user_id'], 'Users')); 151 | 152 | return $rules; 153 | } 154 | 155 | /** 156 | * Last reply finder 157 | * 158 | * @param \Cake\ORM\Query $query The query builder. 159 | * @param array $options Options. 160 | * @return \Cake\ORM\Query 161 | */ 162 | public function findLastReply(Query $query, $options = []) 163 | { 164 | return $query->orderDesc($this->aliasField('id')); 165 | } 166 | 167 | /** 168 | * Find by ID and parent_id 169 | * 170 | * @param \Cake\ORM\Query $query The query builder. 171 | * @param array $options Options. 172 | * @return \Cake\ORM\Query 173 | */ 174 | public function findByThreadAndCategory(Query $query, $options = []) 175 | { 176 | if (!($categorySlug = Hash::get($options, 'categorySlug')) || !($threadSlug = Hash::get($options, 'threadSlug'))) { 177 | throw new \InvalidArgumentException('categorySlug and threadSlug are required'); 178 | } 179 | 180 | return $query->contain([ 181 | 'Users', 182 | 'Categories' => function (Query $query) use ($categorySlug) { 183 | return $query->find('slugged', ['slug' => $categorySlug]); 184 | }, 185 | 'Threads' => function (Query $query) use ($threadSlug) { 186 | return $query->find('slugged', ['slug' => $threadSlug]); 187 | }, 188 | 'Threads.Users' 189 | ]); 190 | } 191 | 192 | /** 193 | * beforeFind callback 194 | * 195 | * @param Event $event Event 196 | * @param Query $query Query 197 | * @param ArrayObject $options Options 198 | * @param bool $primary Primary 199 | */ 200 | public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary) 201 | { 202 | if (!Hash::get($options, 'all')) { 203 | $query->where([$query->newExpr()->isNotNull($this->aliasField('parent_id'))]); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Model/Table/ReportsTable.php: -------------------------------------------------------------------------------- 1 | setTable('forum_reports'); 51 | $this->setDisplayField('id'); 52 | $this->setPrimaryKey('id'); 53 | 54 | $this->addBehavior('Timestamp'); 55 | $this->addBehavior('CounterCache', [ 56 | 'Posts' => [ 57 | 'reports_count' 58 | ], 59 | ]); 60 | 61 | $this->belongsTo('Posts', [ 62 | 'className' => 'CakeDC/Forum.Posts', 63 | 'joinType' => 'INNER' 64 | ]); 65 | $this->belongsTo('Users', [ 66 | 'className' => Configure::read('Forum.userModel'), 67 | 'joinType' => 'INNER' 68 | ]); 69 | } 70 | 71 | /** 72 | * Default validation rules. 73 | * 74 | * @param \Cake\Validation\Validator $validator Validator instance. 75 | * @return \Cake\Validation\Validator 76 | */ 77 | public function validationDefault(Validator $validator) 78 | { 79 | $validator 80 | ->integer('id') 81 | ->allowEmpty('id', 'create'); 82 | 83 | $validator 84 | ->requirePresence('message', 'create') 85 | ->notEmpty('message'); 86 | 87 | return $validator; 88 | } 89 | 90 | /** 91 | * Returns a rules checker object that will be used for validating 92 | * application integrity. 93 | * 94 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 95 | * @return \Cake\ORM\RulesChecker 96 | */ 97 | public function buildRules(RulesChecker $rules) 98 | { 99 | $rules->add($rules->existsIn(['post_id'], 'Posts')); 100 | $rules->add($rules->existsIn(['user_id'], 'Users')); 101 | 102 | return $rules; 103 | } 104 | 105 | /** 106 | * Find filtered 107 | * 108 | * @param \Cake\ORM\Query $query The query builder. 109 | * @param array $options Options. 110 | * @return \Cake\ORM\Query 111 | */ 112 | public function findFiltered(Query $query, $options = []) 113 | { 114 | $where = []; 115 | $contain = [ 116 | 'Users', 117 | 'Posts' => ['Categories', 'Users', 'Threads.Users'] 118 | ]; 119 | 120 | if ($postId = Hash::get($options, 'post_id')) { 121 | $where['Reports.post_id'] = $postId; 122 | } elseif ($threadId = Hash::get($options, 'thread_id')) { 123 | $where['OR'] = [ 124 | 'Posts.id' => $threadId, 125 | 'Posts.parent_id' => $threadId, 126 | ]; 127 | } 128 | 129 | if (!is_null($categoryId = Hash::get($options, 'category_id'))) { 130 | $where['Posts.category_id IN'] = (array)$categoryId; 131 | } 132 | 133 | return $query 134 | ->contain($contain) 135 | ->where($where); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Template/Admin/Categories/add.ctp: -------------------------------------------------------------------------------- 1 | 15 |

16 | 19 | element('Admin/Categories/form') ?> 20 | 21 | -------------------------------------------------------------------------------- /src/Template/Admin/Categories/edit.ctp: -------------------------------------------------------------------------------- 1 | 15 |

16 | 25 | element('Admin/Categories/form') ?> 26 | 27 | -------------------------------------------------------------------------------- /src/Template/Admin/Categories/index.ctp: -------------------------------------------------------------------------------- 1 | 15 |

16 | 20 | toArray())): ?> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | element('Admin/Categories/rows', ['categories' => $categories->toArray()]) ?> 32 | 33 |
34 | 35 |

36 | 37 | -------------------------------------------------------------------------------- /src/Template/Admin/Categories/view.ctp: -------------------------------------------------------------------------------- 1 | 15 |

title) ?>

16 | 22 | 23 | 24 | 25 | 26 | 27 | parent_category): ?> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
Number->format($category->id) ?>
Html->link($category->parent_category->title, ['action' => 'view', $category->parent_category->id]) ?>
title) ?>
slug) ?>
Text->autoParagraph(h($category->description)); ?>
Number->format($category->threads_count) ?>
Number->format($category->replies_count) ?>
is_visible ? __('Yes') : __('No'); ?>
created) ?>
modified) ?>
66 | 67 | sub_categories): ?> 68 |

69 | 72 | moderators): ?> 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | moderators as $moderator): ?> 82 | element('Admin/Moderators/row', compact('moderator')) ?> 83 | 84 | 85 |
86 | 87 |

88 | 89 | 90 | -------------------------------------------------------------------------------- /src/Template/Admin/Moderators/add.ctp: -------------------------------------------------------------------------------- 1 | 16 |

17 | 20 | Form->create($moderator) ?> 21 | Form->control('category_id', ['default' => $this->request->getQuery('category_id')]) ?> 22 | Form->control('user_id') ?> 23 | Form->button(__('Submit')) ?> 24 | Form->end() ?> 25 | -------------------------------------------------------------------------------- /src/Template/Admin/Replies/add.ctp: -------------------------------------------------------------------------------- 1 | 16 |

17 | 20 |
Html->link($thread->title, ['controller' => 'Threads', 'action' => 'view', $thread->id]) ?>
21 | element('Admin/Replies/form') ?> 22 | 23 | -------------------------------------------------------------------------------- /src/Template/Admin/Replies/edit.ctp: -------------------------------------------------------------------------------- 1 | 16 |

17 | 26 |
Html->link($thread->title, ['controller' => 'Threads', 'action' => 'view', $thread->id]) ?>
27 | element('Admin/Replies/form') ?> 28 | 29 | -------------------------------------------------------------------------------- /src/Template/Admin/Replies/view.ctp: -------------------------------------------------------------------------------- 1 | 15 |

16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
Number->format($reply->id) ?>
element('Forum/username', ['user' => $reply->user]) ?>
Html->link($reply->thread->title, ['action' => 'view', $reply->thread->id]) ?>
element('Forum/message', ['message' => $reply->message]) ?>
created) ?>
modified) ?>
48 | -------------------------------------------------------------------------------- /src/Template/Admin/Reports/index.ctp: -------------------------------------------------------------------------------- 1 | toArray())->groupBy('post_id')->toArray(); 18 | ?> 19 |

20 | 23 |
24 | $postReports): ?> 25 | post; 28 | ?> 29 |

Html->link($post->category->title, ['controller' => 'Threads', 'action' => 'index', '?' => ['category_id' => $post->category_id]]), $this->element('Forum/username', ['user' => Hash::get($post, 'thread.user', $post->user)]), $this->Html->link(Hash::get($post, 'thread.created', $post->created)->timeAgoInWords(), ['controller' => 'Threads', 'action' => 'view', Hash::get($post, 'thread.id', $post->id)])) ?>

30 |
31 |
32 | element('Forum/username', ['user' => $post->user]) ?>, created->timeAgoInWords() ?> 33 |
34 |
35 | element('Forum/message', ['message' => $post->message]) ?> 36 |
37 | 42 |
43 | 44 |
45 | 46 |

47 | 48 | element('Forum/pagination') ?> 49 | -------------------------------------------------------------------------------- /src/Template/Admin/Threads/add.ctp: -------------------------------------------------------------------------------- 1 | 14 |

15 | 18 | element('Admin/Threads/form') ?> 19 | -------------------------------------------------------------------------------- /src/Template/Admin/Threads/edit.ctp: -------------------------------------------------------------------------------- 1 | 15 |

16 | 25 | element('Admin/Threads/form') ?> 26 | -------------------------------------------------------------------------------- /src/Template/Admin/Threads/index.ctp: -------------------------------------------------------------------------------- 1 | 16 |

17 | 20 | element('Admin/Threads/filter') ?> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | element('Admin/Threads/row', compact('thread')) ?> 33 | 34 | 35 |
Paginator->sort('title') ?>Paginator->sort('replies_count', __('Replies')) ?>Paginator->sort('last_reply_created', __('Last Message')) ?>
36 |
37 | 42 |

Paginator->counter() ?>

43 |
44 | -------------------------------------------------------------------------------- /src/Template/Admin/Threads/move.ctp: -------------------------------------------------------------------------------- 1 | 16 |

title)) ?>

17 | 26 | Form->create($thread) ?> 27 | Form->control('category_id', ['options' => $categories, 'escape' => false, 'empty' => false]) ?> 28 | Form->button(__('Submit')) ?> 29 | Form->end() ?> 30 | -------------------------------------------------------------------------------- /src/Template/Admin/Threads/view.ctp: -------------------------------------------------------------------------------- 1 | 16 |

17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
Number->format($thread->id) ?>
element('Forum/username', ['user' => $thread->user]) ?>
Html->link($thread->category->title, ['controller' => 'Categories', 'action' => 'view', $thread->category->id]) ?>
39 | reports_count): ?>Html->link(__('Reported'), ['controller' => 'Reports', 'action' => 'index', '?' => ['post_id' => $thread->id]], ['class' => 'label label-danger']) ?> 40 | title) ?> 41 |
slug) ?>
50 | element('Forum/message', ['message' => $thread->message]) ?> 51 | element('Forum/likes', ['likes' => $thread->likes]) ?> 52 |
Number->format($thread->replies_count) ?>
is_sticky ? __('Yes') : __('No'); ?>
is_locked ? __('Yes') : __('No'); ?>
created) ?>
modified) ?>
75 | 76 |

77 | 80 | count()): ?> 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | element('Admin/Replies/row', compact('reply')) ?> 91 | 92 | 93 |
94 |
95 | 100 |

Paginator->counter() ?>

101 |
102 | 103 |

104 | 105 | -------------------------------------------------------------------------------- /src/Template/Categories/index.ctp: -------------------------------------------------------------------------------- 1 | assign('title', isset($category) ? h($category->title) : __('Forum')); 16 | ?> 17 | element('Forum/breadcrumbs') ?> 18 |

fetch('title') ?>

19 | 25 | 26 | 27 | element('Categories/row', compact('category')) ?> 28 | 29 |
30 | -------------------------------------------------------------------------------- /src/Template/Element/Admin/Categories/form.ctp: -------------------------------------------------------------------------------- 1 | 16 | Form->create($category) ?> 17 | Form->control('parent_id', ['options' => $categories, 'escape' => false, 'empty' => true, 'default' => $this->request->getQuery('parent_id')]) ?> 18 | Form->control('title') ?> 19 | isNew()): ?> 20 | Form->control('slug') ?> 21 | 22 | Form->control('description') ?> 23 | Form->control('is_visible', ['default' => true]) ?> 24 | Form->button(__('Submit')) ?> 25 | Form->end() ?> 26 | -------------------------------------------------------------------------------- /src/Template/Element/Admin/Categories/rows.ctp: -------------------------------------------------------------------------------- 1 | 19 | $category): ?> 20 | 21 | children): ?> colspan="3"> 22 | Html->link($category->title, ['controller' => 'Threads', 'action' => 'index', '?' => ['category_id' => $category->id]]) ?> 23 | is_visible): ?> 24 | moderators): ?> 25 |
moderators)->map(function($item) { return $this->element('Forum/username', ['user' => $item->user]); })->toArray()) ?> 26 | 27 | 28 | children): ?> 29 | Number->format($category->threads_count) ?> 30 | Number->format($category->replies_count) ?> 31 | 32 | 33 | Form->postLink('', ['action' => 'moveUp', $category->id], ['title' => __('Move up'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-arrow-up', 'disabled' => ($k == 0)]) ?> 34 | Form->postLink('', ['action' => 'moveDown', $category->id], ['title' => __('Move down'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-arrow-down', 'disabled' => ($k + 1 == count($categories))]) ?> 35 | 36 | 37 | parent_id): ?> 38 | Html->link('', ['action' => 'add', '?' => ['parent_id' => $category->id]], ['title' => __('Add sub category'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-plus']) ?> 39 | 40 | Html->link('', ['action' => 'view', $category->id], ['title' => __('View'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-eye-open']) ?> 41 | Html->link('', ['action' => 'edit', $category->id], ['title' => __('Edit'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-pencil']) ?> 42 | Form->postLink('', ['action' => 'delete', $category->id], ['confirm' => __('Are you sure you want to delete "{0}"?', $category->title), 'title' => __('Delete'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-trash']) ?> 43 | 44 | 45 | children): ?> 46 | element('Admin/Categories/rows', ['categories' => $category->children, 'level' => $level + 1]) ?> 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/Template/Element/Admin/Moderators/row.ctp: -------------------------------------------------------------------------------- 1 | 15 | 16 | element('Forum/username', ['user' => $moderator->user]) ?> 17 | Form->postLink('', ['controller' => 'Moderators', 'action' => 'delete', $moderator->id], ['confirm' => __('Are you sure you want to delete "{0}"?', $this->element('Forum/username', ['user' => $moderator->user, 'link' => false])), 'title' => __('Delete'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-trash']) ?> 18 | 19 | -------------------------------------------------------------------------------- /src/Template/Element/Admin/Replies/form.ctp: -------------------------------------------------------------------------------- 1 | 15 | Form->create($reply) ?> 16 | Form->control('message', ['class' => 'form-control forum-message-input']) ?> 17 | Form->button(__('Submit')) ?> 18 | Form->end() ?> 19 | -------------------------------------------------------------------------------- /src/Template/Element/Admin/Replies/row.ctp: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | reports_count): ?>Html->link(__('Reported'), ['controller' => 'Reports', 'action' => 'index', '?' => ['post_id' => $reply->id]], ['class' => 'label label-danger']) ?> 18 | element('Forum/username', ['user' => $reply->user]) ?>, created->timeAgoInWords() ?>
19 | element('Forum/message', ['message' => $reply->message]) ?> 20 | element('Forum/likes', ['likes' => $reply->likes]) ?> 21 | 22 | 23 | Html->link('', ['controller' => 'Replies', 'action' => 'view', $reply->id], ['title' => __('View'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-eye-open']) ?> 24 | Html->link('', ['controller' => 'Replies', 'action' => 'edit', $reply->id], ['title' => __('Edit'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-pencil']) ?> 25 | Form->postLink('', ['controller' => 'Replies', 'action' => 'delete', $reply->id], ['confirm' => __('Are you sure you want to delete # {0}?', $reply->id), 'title' => __('Delete'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-trash']) ?> 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Template/Element/Admin/Reports/item.ctp: -------------------------------------------------------------------------------- 1 | 16 |

17 | Form->postLink('', ['action' => 'delete', $report->id], ['confirm' => __('Are you sure you want to delete # {0}?', $report->id), 'title' => __('Delete'), 'class' => 'btn btn-xs btn-default pull-right glyphicon glyphicon-trash']) ?> 18 | element('Forum/username', ['user' => $post->user]) ?>, created->timeAgoInWords() ?>
19 | message)) ?> 20 |

21 | -------------------------------------------------------------------------------- /src/Template/Element/Admin/Threads/filter.ctp: -------------------------------------------------------------------------------- 1 | 15 | Form->create(null, ['type' => 'get', 'class' => 'form-inline', 'style' => 'margin-bottom:15px']) ?> 16 | Form->control('category_id', ['label' => false, 'empty' => __('[Category]'), 'options' => $categories, 'default' => $this->request->getQuery('category_id')]) ?> 17 | Form->control('is_sticky', ['label' => false, 'empty' => __('[Is Sticky]'), 'options' => [1 => __('Yes'), 2 => __('No')], 'default' => $this->request->getQuery('is_sticky')]) ?> 18 | Form->control('is_locked', ['label' => false, 'empty' => __('[Is Locked]'), 'options' => [1 => __('Yes'), 2 => __('No')], 'default' => $this->request->getQuery('is_locked')]) ?> 19 | Form->control('is_visible', ['label' => false, 'empty' => __('[Is Visible]'), 'options' => [1 => __('Yes'), 2 => __('No')], 'default' => $this->request->getQuery('is_visible')]) ?> 20 | Form->button(__('Filter')) ?> 21 | Form->end() ?> 22 | -------------------------------------------------------------------------------- /src/Template/Element/Admin/Threads/form.ctp: -------------------------------------------------------------------------------- 1 | 16 | Form->create($thread) ?> 17 | Form->control('category_id', ['options' => $categories, 'escape' => false, 'empty' => false, 'default' => $this->request->getQuery('category_id')]) ?> 18 | Form->control('title') ?> 19 | isNew()): ?> 20 | Form->control('slug') ?> 21 | 22 | Form->control('message', ['class' => 'form-control forum-message-input']) ?> 23 | Form->control('is_sticky') ?> 24 | Form->control('is_locked') ?> 25 | Form->control('is_visible', ['default' => true]) ?> 26 | Form->button(__('Submit')) ?> 27 | Form->end() ?> 28 | -------------------------------------------------------------------------------- /src/Template/Element/Admin/Threads/row.ctp: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | Html->link($thread->title, ['action' => 'view', $thread->id]) ?> 18 | is_reported): ?>Html->link(__('Reported'), ['controller' => 'Reports', 'action' => 'index', '?' => ['thread_id' => $thread->id]], ['class' => 'label label-danger']) ?> 19 | is_sticky): ?> 20 | is_locked): ?> 21 | is_visible): ?> 22 |
23 | element('Forum/username', ['user' => $thread->user]) ?>, created->timeAgoInWords() ?> 24 | 25 | Number->format($thread->replies_count) ?> 26 | 27 | last_reply): ?> 28 | element('Forum/username', ['user' => $thread->last_reply->user]) ?>, Html->link($thread->last_reply->created->timeAgoInWords(), ['controller' => 'Replies', 'action' => 'view', $thread->last_reply->id]) ?> 29 | 30 | 31 | 32 | Html->link('', ['action' => 'view', $thread->id], ['title' => __('View'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-eye-open']) ?> 33 | Html->link('', ['action' => 'move', $thread->id], ['title' => __('Move'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-arrow-right']) ?> 34 | Html->link('', ['action' => 'edit', $thread->id], ['title' => __('Edit'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-pencil']) ?> 35 | Form->postLink('', ['action' => 'delete', $thread->id], ['confirm' => __('Are you sure you want to delete "{0}"?', $thread->title), 'title' => __('Delete'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-trash']) ?> 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Template/Element/Categories/row.ctp: -------------------------------------------------------------------------------- 1 | 15 | children): ?> 16 | 17 | 18 | title) ?> 19 | 20 | 21 | 22 | children as $subCategory): ?> 23 | element('Categories/row', ['category' => $subCategory]) ?> 24 | 25 | 26 | 27 | 28 | 29 | Html->link($category->title, ['controller' => 'Threads', 'action' => 'index', 'category' => $category->slug]) ?>
30 | Number->format($category->threads_count) ?> 31 | Number->format($category->replies_count) ?> 32 | 33 | 34 | last_post): ?> 35 | Html->link($this->Text->truncate($category->last_post->title, 40), ['controller' => 'Threads', 'action' => 'view', 'category' => $category->slug, 'thread' => $category->last_post->slug, '#' => 'post' . $category->last_post->id])) ?>
36 | element('Forum/username', ['user' => $category->last_post->user]) ?>, last_post->created->timeAgoInWords() ?> 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Template/Element/Forum/breadcrumbs.ctp: -------------------------------------------------------------------------------- 1 | 18 | 25 | -------------------------------------------------------------------------------- /src/Template/Element/Forum/discussion.ctp: -------------------------------------------------------------------------------- 1 | 16 |

Html->link($category->title, ['controller' => 'Threads', 'action' => 'index', 'category' => $category->slug]), $this->element('Forum/username', ['user' => $thread->user]), $this->Html->link($thread->created->timeAgoInWords(), ['controller' => 'Threads', 'action' => 'view', 'category' => $category->slug, 'thread' => $thread->slug])) ?>

17 | -------------------------------------------------------------------------------- /src/Template/Element/Forum/likes.ctp: -------------------------------------------------------------------------------- 1 | 15 | 16 |
map(function($like) { return $this->element('Forum/username', ['user' => $like->user]); })->toArray())) ?> 17 | 18 | -------------------------------------------------------------------------------- /src/Template/Element/Forum/message.ctp: -------------------------------------------------------------------------------- 1 | 14 | Paginator->total() > 1): ?> 15 |
16 | 21 |
22 | 23 | -------------------------------------------------------------------------------- /src/Template/Element/Forum/username.ctp: -------------------------------------------------------------------------------- 1 | id]; 21 | } elseif (is_callable($link)) { 22 | $link = $link($user); 23 | } 24 | } 25 | 26 | $username = Hash::get($user, Configure::read('Forum.userNameField')); 27 | 28 | if ($link === false) { 29 | echo h($username); 30 | } else { 31 | echo $this->Html->link($username, $link); 32 | } 33 | -------------------------------------------------------------------------------- /src/Template/Element/Posts/buttons.ctp: -------------------------------------------------------------------------------- 1 | user_id == $userInfo['id']); 20 | $isThread = !$post->parent_id; 21 | $updateUrl = $isThread 22 | ? ['controller' => 'Threads', 'category' => $category->slug, 'thread' => $thread->slug] 23 | : ['controller' => 'Replies', 'category' => $category->slug, 'thread' => $thread->slug, 'reply' => $post->id] 24 | ?> 25 | 26 | 27 |
28 | user_like): ?> 29 | Form->postLink('', ['controller' => 'Likes', 'action' => 'add', 'category' => $category->slug, 'thread' => $thread->slug, 'post' => $post->id], ['title' => __('Like'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-thumbs-up']) ?> 30 | 31 | is_locked): ?> 32 | Html->link('', '#reply', ['title' => __('Reply'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-share-alt']) ?> 33 | 34 | user_report): ?> 35 | Html->link('', ['controller' => 'Reports', 'action' => 'add', 'category' => $category->slug, 'thread' => $thread->slug, 'post' => $post->id], ['title' => __('Report'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-exclamation-sign']) ?> 36 | 37 | 38 | Html->link('', $updateUrl + ['action' => 'edit'], ['title' => __('Edit'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-pencil']) ?> 39 | 40 | 41 | Html->link('', $updateUrl + ['action' => 'move'], ['title' => __('Move'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-arrow-right']) ?> 42 | 43 | 44 | Form->postLink('', $updateUrl + ['action' => 'delete'], ['title' => __('Delete'), 'class' => 'btn btn-xs btn-default glyphicon glyphicon-trash', 'confirm' => __('Do you really want to delete this post?')]) ?> 45 | 46 |
47 | 48 | -------------------------------------------------------------------------------- /src/Template/Element/Posts/item.ctp: -------------------------------------------------------------------------------- 1 | 20 |
21 |
22 | reports_count): ?> 23 | Html->link('', ['controller' => 'Reports', 'action' => 'index', '?' => ['post_id' => $post->id]], ['title' => __('Reported'), 'class' => 'glyphicon glyphicon-exclamation-sign']) ?> 24 | 25 | element('Forum/username', ['user' => $post->user]) ?>, created->timeAgoInWords() ?> 26 | element('Posts/buttons', compact('post')) ?> 27 |
28 |
29 | element('Forum/message', ['message' => $post->message]) ?> 30 | element('Forum/likes', ['likes' => $post->likes]) ?> 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/Template/Element/Replies/form.ctp: -------------------------------------------------------------------------------- 1 | 15 | Form->create($reply) ?> 16 | Form->control('message', ['class' => 'form-control forum-message-input']) ?> 17 | Form->button(__('Submit'), ['class' => 'btn btn-primary']) ?> 18 | Form->end() ?> 19 | -------------------------------------------------------------------------------- /src/Template/Element/Replies/quick.ctp: -------------------------------------------------------------------------------- 1 | 17 |
18 | 19 | Form->create($reply, ['url' => ['controller' => 'Replies', 'action' => 'add', 'category' => $category->slug, 'thread' => $thread->slug]]) ?> 20 | Form->control('message', ['class' => 'form-control forum-message-input']) ?> 21 | Form->button(__('Reply'), ['class' => 'btn btn-primary']) ?> 22 | Form->end() ?> 23 |
24 | -------------------------------------------------------------------------------- /src/Template/Element/Reports/form.ctp: -------------------------------------------------------------------------------- 1 | 15 | Form->create($report) ?> 16 | Form->control('message', ['label' => __('Report')]) ?> 17 | Form->button(__('Submit')) ?> 18 | Form->end() ?> 19 | -------------------------------------------------------------------------------- /src/Template/Element/Reports/item.ctp: -------------------------------------------------------------------------------- 1 | 16 |
17 |
18 | element('Forum/username', ['user' => $post->user]) ?>, created->timeAgoInWords() ?> 19 |
20 |
21 | element('Forum/message', ['message' => $post->message]) ?> 22 |
23 | 28 |
29 | -------------------------------------------------------------------------------- /src/Template/Element/Reports/row.ctp: -------------------------------------------------------------------------------- 1 | 16 |

17 | Form->postLink('', ['action' => 'delete', $report->id], ['confirm' => __('Are you sure you want to delete # {0}?', $report->id), 'title' => __('Delete'), 'class' => 'btn btn-xs btn-default pull-right glyphicon glyphicon-trash']) ?> 18 | element('Forum/username', ['user' => $post->user]) ?>, created->timeAgoInWords() ?>
19 | message)) ?> 20 |

21 | -------------------------------------------------------------------------------- /src/Template/Element/Threads/form.ctp: -------------------------------------------------------------------------------- 1 | 16 | Form->create($thread) ?> 17 | Form->control('title') ?> 18 | Form->control('message', ['class' => 'form-control forum-message-input']) ?> 19 | 20 | Form->control('is_sticky') ?> 21 | Form->control('is_locked') ?> 22 | 23 | Form->button(__('Submit'), ['class' => 'btn btn-primary']) ?> 24 | Form->end() ?> 25 | -------------------------------------------------------------------------------- /src/Template/Element/Threads/row.ctp: -------------------------------------------------------------------------------- 1 | 'Threads', 'action' => 'view', 'category' => $category->slug, 'thread' => $thread->slug]; 22 | ?> 23 | 24 | 25 | is_reported && $forumUserIsModerator): ?>Html->link('', ['controller' => 'Reports', 'action' => 'index', '?' => ['thread_id' => $thread->id]], ['title' => __('Reported'), 'class' => 'glyphicon glyphicon-exclamation-sign']) ?> 26 | is_sticky): ?> 27 | is_locked): ?> 28 | Html->link($thread->title, $url) ?>
29 | element('Forum/username', ['user' => $thread->user]) ?>, created->timeAgoInWords() ?> 30 | 31 | 32 | Number->format($thread->replies_count) ?> 33 | 34 | 35 | last_reply): ?> 36 | element('Forum/username', ['user' => $thread->last_reply->user, 'link' => $url + ['#' => 'post' . $thread->last_reply->id, '?' => ['page' => 'last']]]) ?>
37 | last_reply->created->timeAgoInWords() ?> 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Template/Replies/add.ctp: -------------------------------------------------------------------------------- 1 | assign('title', h($category->title)); 17 | ?> 18 | element('Forum/breadcrumbs') ?> 19 |

fetch('title') ?>

20 | element('Forum/discussion') ?> 21 | element('Replies/form'); ?> 22 | -------------------------------------------------------------------------------- /src/Template/Replies/edit.ctp: -------------------------------------------------------------------------------- 1 | assign('title', h($category->title)); 17 | ?> 18 | element('Forum/breadcrumbs') ?> 19 |

fetch('title') ?>

20 | element('Forum/discussion') ?> 21 | element('Replies/form'); ?> 22 | -------------------------------------------------------------------------------- /src/Template/Reports/add.ctp: -------------------------------------------------------------------------------- 1 | assign('title', __('Report post in "{0}"', h($category->title))); 19 | ?> 20 | element('Forum/breadcrumbs') ?> 21 |

fetch('title') ?>

22 | element('Forum/discussion') ?> 23 |
24 | element('Posts/item', compact('post') + ['noButtons' => true]) ?> 25 |
26 | element('Reports/form') ?> 27 | -------------------------------------------------------------------------------- /src/Template/Reports/index.ctp: -------------------------------------------------------------------------------- 1 | assign('title', __('Reports')); 16 | 17 | $reports = collection($reports->toArray())->groupBy('post_id')->toArray(); 18 | ?> 19 | element('Forum/breadcrumbs') ?> 20 |

fetch('title') ?>

21 |
22 | $postReports): ?> 23 | post; 26 | ?> 27 | element('Forum/discussion', ['category' => $post->category, 'thread' => $post->thread ? $post->thread : $post]) ?> 28 | element('Reports/item', ['post' => $post, 'reports' => $postReports]) ?> 29 | 30 |
31 | 32 |

33 | 34 | element('Forum/pagination') ?> 35 | -------------------------------------------------------------------------------- /src/Template/Threads/add.ctp: -------------------------------------------------------------------------------- 1 | assign('title', h($category->title)); 16 | ?> 17 | element('Forum/breadcrumbs') ?> 18 |

fetch('title') ?>

19 | element('Threads/form'); ?> 20 | -------------------------------------------------------------------------------- /src/Template/Threads/category.ctp: -------------------------------------------------------------------------------- 1 | extend('Categories/index'); 14 | -------------------------------------------------------------------------------- /src/Template/Threads/edit.ctp: -------------------------------------------------------------------------------- 1 | assign('title', h($category->title)); 16 | ?> 17 | element('Forum/breadcrumbs') ?> 18 |

fetch('title') ?>

19 | element('Threads/form'); ?> 20 | -------------------------------------------------------------------------------- /src/Template/Threads/index.ctp: -------------------------------------------------------------------------------- 1 | assign('title', h($category->title)); 16 | ?> 17 | element('Forum/breadcrumbs') ?> 18 |

fetch('title') ?>

19 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | element('Threads/row', compact('thread', 'category')) ?> 36 | 37 | 38 |
26 | Paginator->sort('title') ?> 27 | Paginator->sort('id', __('Start Date')) ?> 28 | Paginator->sort('replies_count', __('Replies')) ?>Paginator->sort('last_reply_created', __('Last Message')) ?>
39 | element('Forum/pagination') ?> 40 | -------------------------------------------------------------------------------- /src/Template/Threads/move.ctp: -------------------------------------------------------------------------------- 1 | assign('title', __('Move Thread: {0}', h($thread->title))); 16 | ?> 17 | element('Forum/breadcrumbs') ?> 18 |

fetch('title') ?>

19 | Form->create($thread) ?> 20 | Form->control('category_id', ['label' => __('New Category')]) ?> 21 | Form->button(__('Move'), ['class' => 'btn btn-primary']) ?> 22 | Form->end() ?> 23 | -------------------------------------------------------------------------------- /src/Template/Threads/my.ctp: -------------------------------------------------------------------------------- 1 | assign('title', __('My Conversations')); 15 | ?> 16 | element('Forum/breadcrumbs') ?> 17 |

fetch('title') ?>

18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | element('Threads/row', ['thread' => $thread, 'category' => $thread->category]) ?> 32 | 33 | 34 |
22 | Paginator->sort('title') ?> 23 | Paginator->sort('id', __('Start Date')) ?> 24 | Paginator->sort('replies_count', __('Replies')) ?>Paginator->sort('last_reply_created', __('Last Message')) ?>
35 | element('Forum/pagination') ?> 36 | -------------------------------------------------------------------------------- /src/Template/Threads/view.ctp: -------------------------------------------------------------------------------- 1 | assign('title', h($thread->title)); 17 | ?> 18 | element('Forum/breadcrumbs') ?> 19 |

fetch('title') ?>

20 | element('Forum/discussion') ?> 21 | is_locked): ?> 22 | 25 | 26 | element('Forum/pagination') ?> 27 |
28 | 29 | element('Posts/item', compact('post')) ?> 30 | 31 |
32 | element('Forum/pagination') ?> 33 | is_locked): ?> 34 | element('Replies/quick') ?> 35 | 36 | -------------------------------------------------------------------------------- /webroot/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CakeDC/cakephp-forum/1cccf2b128ca1c13933e18552fb31a8cf1483e5d/webroot/empty --------------------------------------------------------------------------------