├── .coveralls.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── php.yml ├── .gitignore ├── .php_cs ├── .styleci.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── composer.json ├── migrations ├── 2018_05_28_224023_create_blog_etc_posts_table.php ├── 2018_09_16_224023_add_author_and_url_blog_etc_posts_table.php ├── 2018_09_26_085711_add_short_desc_textrea_to_blog_etc.php └── 2018_09_27_122627_create_blog_etc_uploaded_photos_table.php ├── phpunit.xml ├── src ├── BlogEtcServiceProvider.php ├── Captcha │ ├── Basic.php │ └── CaptchaAbstract.php ├── Config │ └── blogetc.php ├── Controllers │ ├── Admin │ │ ├── ManageCategoriesController.php │ │ ├── ManageCommentsController.php │ │ ├── ManagePostsController.php │ │ └── ManageUploadsController.php │ ├── BlogEtcAdminController.php │ ├── BlogEtcCategoryAdminController.php │ ├── BlogEtcCommentWriterController.php │ ├── BlogEtcCommentsAdminController.php │ ├── BlogEtcImageUploadController.php │ ├── BlogEtcReaderController.php │ ├── BlogEtcRssFeedController.php │ ├── CommentsController.php │ ├── FeedController.php │ └── PostsController.php ├── Events │ ├── BlogPostAdded.php │ ├── BlogPostEdited.php │ ├── BlogPostWillBeDeleted.php │ ├── CategoryAdded.php │ ├── CategoryEdited.php │ ├── CategoryWillBeDeleted.php │ ├── CommentAdded.php │ ├── CommentApproved.php │ ├── CommentWillBeDeleted.php │ └── UploadedImage.php ├── Exceptions │ ├── BlogEtcAuthGateNotImplementedException.php │ ├── CategoryNotFoundException.php │ ├── CommentNotFoundException.php │ ├── PostNotFoundException.php │ └── UploadedPhotoNotFoundException.php ├── Factories │ ├── CategoryFactory.php │ ├── CommentFactory.php │ └── PostFactory.php ├── Gates │ ├── DefaultAddCommentsGate.php │ ├── DefaultAdminGate.php │ └── GateTypes.php ├── Helpers.php ├── Interfaces │ ├── BaseRequestInterface.php │ ├── CaptchaInterface.php │ ├── LegacyGetImageFileInterface.php │ └── SearchResultInterface.php ├── Middleware │ └── UserCanManageBlogPosts.php ├── Models │ ├── BlogEtcCategory.php │ ├── BlogEtcComment.php │ ├── BlogEtcPost.php │ ├── BlogEtcUploadedPhoto.php │ ├── Category.php │ ├── Comment.php │ ├── Post.php │ └── UploadedPhoto.php ├── Repositories │ ├── CategoriesRepository.php │ ├── CommentsRepository.php │ ├── PostsRepository.php │ └── UploadedPhotosRepository.php ├── Requests │ ├── AddNewCommentRequest.php │ ├── BaseAdminRequest.php │ ├── BaseBlogEtcPostRequest.php │ ├── BaseRequest.php │ ├── CategoryRequest.php │ ├── CreateBlogEtcPostRequest.php │ ├── DeleteBlogEtcPostRequest.php │ ├── FeedRequest.php │ ├── SearchRequest.php │ ├── Traits │ │ ├── HasCategoriesTrait.php │ │ └── HasImageUploadTrait.php │ ├── UpdateBlogEtcPostRequest.php │ └── UploadImageRequest.php ├── Scopes │ ├── BlogCommentApprovedAndDefaultOrderScope.php │ └── BlogEtcPublishedScope.php ├── Services │ ├── CaptchaService.php │ ├── CategoriesService.php │ ├── CommentsService.php │ ├── FeedService.php │ ├── PostsService.php │ └── UploadsService.php ├── Views │ ├── blogetc │ │ ├── captcha │ │ │ └── basic.blade.php │ │ ├── index.blade.php │ │ ├── partials │ │ │ ├── add_comment_form.blade.php │ │ │ ├── author.blade.php │ │ │ ├── built_in_comments.blade.php │ │ │ ├── categories.blade.php │ │ │ ├── custom_comments.blade.php │ │ │ ├── disqus_comments.blade.php │ │ │ ├── full_post_details.blade.php │ │ │ ├── index_loop.blade.php │ │ │ ├── search_form.blade.php │ │ │ ├── show_comments.blade.php │ │ │ ├── show_errors.blade.php │ │ │ └── use_view_file.blade.php │ │ ├── saved_comment.blade.php │ │ ├── search.blade.php │ │ ├── single_post.blade.php │ │ └── sitewide │ │ │ ├── random_posts.blade.php │ │ │ ├── recent_posts.blade.php │ │ │ ├── search_form.blade.php │ │ │ └── show_all_categories.blade.php │ └── blogetc_admin │ │ ├── categories │ │ ├── add_category.blade.php │ │ ├── deleted_category.blade.php │ │ ├── edit_category.blade.php │ │ ├── form.blade.php │ │ └── index.blade.php │ │ ├── comments │ │ └── index.blade.php │ │ ├── imageupload │ │ ├── create.blade.php │ │ ├── delete-post-image.blade.php │ │ ├── deleted-post-image.blade.php │ │ ├── index.blade.php │ │ └── uploaded.blade.php │ │ ├── index.blade.php │ │ ├── layouts │ │ ├── admin_layout.blade.php │ │ └── sidebar.blade.php │ │ └── posts │ │ ├── add_post.blade.php │ │ ├── deleted_post.blade.php │ │ ├── edit_post.blade.php │ │ └── form.blade.php ├── css │ └── blogetc_admin_css.css └── routes.php └── tests ├── Feature ├── Controllers │ ├── Admin │ │ ├── ManageCategoriesControllerTest.php │ │ ├── ManageCommentsControllerTest.php │ │ ├── ManagePostsControllerTest.php │ │ └── ManageUploadsControllerTest.php │ ├── CommentsControllerTest.php │ ├── FeedControllerTest.php │ └── PostsControllerTest.php ├── Repositories │ ├── CategoriesRepositoryTest.php │ ├── CommentsRepositoryTest.php │ └── PostsRepositoryTest.php └── Services │ ├── CaptchaServiceTest.php │ ├── CategoriesServiceTest.php │ ├── CommentsServiceTest.php │ ├── FeedServiceTest.php │ └── PostsServiceTest.php ├── TestCase.php ├── Unit └── Services │ └── PostsServiceTest.php └── views └── layouts └── app.blade.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | src_dir: src 2 | coverage_clover: build/logs/clover.xml 3 | json_path: build/logs/coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer and tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | operating-system: [ubuntu-latest, windows-latest, macos-latest] 10 | php-versions: ['7.3', '7.4'] 11 | dependency-version: [prefer-lowest, prefer-stable] 12 | laravel: ['7.6.0', '7.28.0', '8.0.1', '8.2.0', '8.1.0'] 13 | runs-on: ${{ matrix.operating-system }} 14 | name: PHP ${{ matrix.php-versions }} Laravel L${{ matrix.laravel }} Test on ${{ matrix.operating-system }} 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php-versions }} 23 | extensions: mbstring, intl, fileinfo, pdo_sqlite, mysql 24 | ini-values: post_max_size=256M, short_open_tag=On 25 | coverage: xdebug 26 | 27 | - name: Validate composer.json and composer.lock 28 | run: composer validate 29 | 30 | - name: Install dependencies 31 | run: | 32 | composer self-update 33 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 34 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 35 | # composer update --prefer-source --no-interaction --no-suggest 36 | 37 | - name: Run phpunit tests 38 | run: ./vendor/bin/phpunit 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | vendor/ 4 | node_modules/ 5 | npm-debug.log 6 | composer.lock 7 | .php_cs.cache 8 | bootstrap/compiled.php 9 | app/storage/ 10 | public/storage 11 | public/hot 12 | storage/*.key 13 | .env.*.php 14 | .env.php 15 | .env 16 | Homestead.yaml 17 | Homestead.json 18 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | exclude($excluded_folders) 12 | ->in(__DIR__); 13 | ; 14 | 15 | return PhpCsFixer\Config::create() 16 | ->setRules(array( 17 | '@Symfony' => true, 18 | 'binary_operator_spaces' => ['align_double_arrow' => true], 19 | 'array_syntax' => ['syntax' => 'short'], 20 | 'linebreak_after_opening_tag' => true, 21 | 'not_operator_with_successor_space' => true, 22 | 'ordered_imports' => true, 23 | 'phpdoc_order' => true, 24 | )) 25 | ->setFinder($finder) 26 | ; 27 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | # Styleci for WebDevEtc. 2 | 3 | preset: laravel 4 | 5 | disabled: 6 | - not_operator_with_successor_space 7 | 8 | #finder: 9 | # exclude: 10 | # - "tests" 11 | # - "node_modules" 12 | # - "storage" 13 | # - "vendor" 14 | # not-name: 15 | # - "AcceptanceTester.php" 16 | # - "FunctionalTester.php" 17 | # - "UnitTester.php" 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | - 7.4 6 | 7 | env: 8 | - LARAVEL_VERSION=8.2.0 9 | - LARAVEL_VERSION=8.1.0 10 | - LARAVEL_VERSION=8.0.1 11 | - LARAVEL_VERSION=7.28.0 12 | - LARAVEL_VERSION=7.6.0 13 | - LARAVEL_VERSION=7.0.0 14 | - LARAVEL_VERSION=6.18.8 15 | - LARAVEL_VERSION=6.8.0 16 | - LARAVEL_VERSION=6.5.2 17 | - LARAVEL_VERSION=5.8.35 18 | - LARAVEL_VERSION=dev-master 19 | - LARAVEL_VERSION= 20 | matrix: 21 | fast_finish: true 22 | 23 | before_script: 24 | - travis_retry composer self-update 25 | - travis_retry composer install --prefer-source --no-interaction 26 | - if [ "$LARAVEL_VERSION" != "" ]; then composer require --dev "laravel/laravel:${LARAVEL_VERSION}" --no-update; fi; 27 | - composer update 28 | 29 | script: 30 | - vendor/bin/phpunit 31 | 32 | after_script: 33 | # - php vendor/bin/php-coveralls -v 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ghcodeofconduct@webdevetc.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Feel free to suggest PRs. 2 | 3 | Email me (github@webdevet.com) or message me on https://twitter.com/web_dev_etc with any questions. 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 WebDevEtc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Contact me at github@webdevetc.com or on twitter https://twitter.com/web_dev_etc 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdevetc/blogetc", 3 | "keywords": [ 4 | "laravel", 5 | "blog", 6 | "package", 7 | "admin", 8 | "panel", 9 | "rss", 10 | "feed", 11 | "write", 12 | "posts", 13 | "news", 14 | "update", 15 | "webdevetc", 16 | "webdev" 17 | ], 18 | "description": "Simple blog (with admin panel) for Laravel from https://webdevetc.com/", 19 | "license": "MIT", 20 | "support": { 21 | "docs": "https://webdevetc.com/laravel/packages/blogetc-blog-system-for-your-laravel-app/help-documentation/laravel-blog-package-blogetc" 22 | }, 23 | "authors": [ 24 | { 25 | "name": "WebDevEtc", 26 | "homepage": "https://webdevetc.com/", 27 | "role": "developer", 28 | "email": "packages@webdevetc.com" 29 | } 30 | ], 31 | "require": { 32 | "intervention/image": "2.*", 33 | "cviebrock/eloquent-sluggable": "~8.0|~7.0|~6.0|~4.8|~4.7|~4.6|~4.5", 34 | "laravel/framework": "~5.8|~6.0|~7.0|~8.0", 35 | "laravel/helpers": "^1.2", 36 | "laravelium/feed": "~8.0|~7.0|~6.0|~3.1" 37 | }, 38 | "require-dev": { 39 | "phpunit/phpunit": "~9.1|~8.4|~8.1", 40 | "orchestra/testbench": "~5.2|~4.8|~4.0|~3.8|~3.7", 41 | "friendsofphp/php-cs-fixer": "~2.16" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "WebDevEtc\\BlogEtc\\": "src" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "psr-4": { 50 | "WebDevEtc\\BlogEtc\\Tests\\": "tests" 51 | } 52 | }, 53 | "extra": { 54 | "laravel": { 55 | "providers": [ 56 | "WebDevEtc\\BlogEtc\\BlogEtcServiceProvider" 57 | ], 58 | "aliases": { 59 | } 60 | } 61 | }, 62 | "abandoned": true 63 | } 64 | -------------------------------------------------------------------------------- /migrations/2018_05_28_224023_create_blog_etc_posts_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | 20 | $table->string('slug')->unique(); 21 | $table->unsignedInteger('user_id')->index()->nullable(); 22 | $table->string('title')->nullable()->default('New blog post'); 23 | $table->string('subtitle')->nullable()->default(''); 24 | $table->text('meta_desc')->nullable(); 25 | $table->mediumText('post_body')->nullable(); 26 | $table->string('use_view_file') 27 | ->nullable() 28 | ->comment('If not null, this should refer to a blade file in /views/'); 29 | $table->dateTime('posted_at')->index()->nullable() 30 | ->comment('Public posted at time, if this is in future then it wont appear yet'); 31 | $table->boolean('is_published')->default(true); 32 | $table->string('image_large')->nullable(); 33 | $table->string('image_medium')->nullable(); 34 | $table->string('image_thumbnail')->nullable(); 35 | 36 | $table->timestamps(); 37 | }); 38 | 39 | Schema::create('blog_etc_categories', static function (Blueprint $table) { 40 | $table->increments('id'); 41 | 42 | $table->string('category_name')->nullable(); 43 | $table->string('slug')->unique(); 44 | $table->mediumText('category_description')->nullable(); 45 | $table->unsignedInteger('created_by')->nullable()->index()->comment('user id'); 46 | 47 | $table->timestamps(); 48 | }); 49 | 50 | // linking table: 51 | Schema::create('blog_etc_post_categories', static function (Blueprint $table) { 52 | $table->increments('id'); 53 | 54 | $table->unsignedInteger('blog_etc_post_id')->index(); 55 | $table->foreign('blog_etc_post_id')->references('id')->on('blog_etc_posts')->onDelete('cascade'); 56 | $table->unsignedInteger('blog_etc_category_id')->index(); 57 | 58 | $table->foreign('blog_etc_category_id')->references('id')->on('blog_etc_categories')->onDelete('cascade'); 59 | }); 60 | 61 | Schema::create('blog_etc_comments', static function (Blueprint $table) { 62 | $table->increments('id'); 63 | 64 | $table->unsignedInteger('blog_etc_post_id')->index(); 65 | $table->foreign('blog_etc_post_id')->references('id')->on('blog_etc_posts')->onDelete('cascade'); 66 | $table->unsignedInteger('user_id')->nullable()->index()->comment('if user was logged in'); 67 | $table->string('ip')->nullable()->comment('if enabled in the config file'); 68 | $table->string('author_name')->nullable()->comment('if not logged in'); 69 | $table->text('comment')->comment('the comment body'); 70 | $table->boolean('approved')->default(true); 71 | 72 | $table->timestamps(); 73 | }); 74 | } 75 | 76 | /** 77 | * Reverse the migrations. 78 | */ 79 | public function down(): void 80 | { 81 | Schema::dropIfExists('blog_etc_comments'); 82 | Schema::dropIfExists('blog_etc_post_categories'); 83 | Schema::dropIfExists('blog_etc_categories'); 84 | Schema::dropIfExists('blog_etc_posts'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /migrations/2018_09_16_224023_add_author_and_url_blog_etc_posts_table.php: -------------------------------------------------------------------------------- 1 | string('author_email')->nullable(); 19 | $table->string('author_website')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::table('blog_etc_comments', static function (Blueprint $table) { 29 | $table->dropColumn('author_email'); 30 | $table->dropColumn('author_website'); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrations/2018_09_26_085711_add_short_desc_textrea_to_blog_etc.php: -------------------------------------------------------------------------------- 1 | text('short_description')->nullable(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::table('blog_etc_posts', static function (Blueprint $table) { 30 | $table->dropColumn('short_description'); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrations/2018_09_27_122627_create_blog_etc_uploaded_photos_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | $table->text('uploaded_images')->nullable(); 20 | $table->string('image_title')->nullable(); 21 | $table->string('source')->default('unknown'); 22 | $table->unsignedInteger('uploader_id')->nullable()->index(); 23 | 24 | $table->timestamps(); 25 | }); 26 | 27 | Schema::table('blog_etc_posts', static function (Blueprint $table) { 28 | $table->string('seo_title')->nullable(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | */ 35 | public function down(): void 36 | { 37 | Schema::dropIfExists('blog_etc_uploaded_photos'); 38 | 39 | Schema::table('blog_etc_posts', static function (Blueprint $table) { 40 | $table->dropColumn('seo_title'); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests/ 17 | ./tests/views/layouts/app.blade.php 18 | 19 | 20 | 21 | 22 | src/ 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/BlogEtcServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 29 | __DIR__.'/../migrations/'.$file => database_path('migrations/'.$file), 30 | ]); 31 | } 32 | 33 | // Set up default gates to allow/disallow access to features. 34 | $this->setupDefaultGates(); 35 | 36 | $this->publishes([ 37 | __DIR__.'/Views/blogetc' => base_path('resources/views/vendor/blogetc'), 38 | __DIR__.'/Config/blogetc.php' => config_path('blogetc.php'), 39 | __DIR__.'/css/blogetc_admin_css.css' => public_path('blogetc_admin_css.css'), 40 | ]); 41 | } 42 | 43 | /** 44 | * Set up default gates. 45 | */ 46 | protected function setupDefaultGates(): void 47 | { 48 | if (!Gate::has(GateTypes::MANAGE_BLOG_ADMIN)) { 49 | Gate::define(GateTypes::MANAGE_BLOG_ADMIN, include(__DIR__.'/Gates/DefaultAdminGate.php')); 50 | } 51 | 52 | /* 53 | * For people to add comments to your blog posts. By default it will allow anyone - you can add your 54 | * own logic here if needed. 55 | */ 56 | if (!Gate::has(GateTypes::ADD_COMMENT)) { 57 | Gate::define(GateTypes::ADD_COMMENT, include(__DIR__.'/Gates/DefaultAddCommentsGate.php')); 58 | } 59 | } 60 | 61 | /** 62 | * Register services. 63 | * 64 | * @return void 65 | */ 66 | public function register() 67 | { 68 | $this->loadViewsFrom(__DIR__.'/Views/blogetc_admin', 'blogetc_admin'); 69 | 70 | // if you do the vendor:publish, these will be copied to /resources/views/vendor/blogetc anyway 71 | $this->loadViewsFrom(__DIR__.'/Views/blogetc', 'blogetc'); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Captcha/Basic.php: -------------------------------------------------------------------------------- 1 | captchaFieldName(); 64 | } 65 | 66 | /** 67 | * What should the field name be (in the ). 68 | */ 69 | public function captchaFieldName(): string 70 | { 71 | return 'captcha'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Captcha/CaptchaAbstract.php: -------------------------------------------------------------------------------- 1 | middleware(UserCanManageBlogPosts::class); 28 | $this->service = $service; 29 | } 30 | 31 | /** 32 | * Show list of categories. 33 | * 34 | * @return mixed 35 | */ 36 | public function index(): View 37 | { 38 | $categories = $this->service->indexPaginated(); 39 | 40 | return view( 41 | 'blogetc_admin::categories.index', 42 | [ 43 | 'categories' => $categories, 44 | ] 45 | ); 46 | } 47 | 48 | /** 49 | * @deprecated - use store() 50 | */ 51 | public function store_category(CategoryRequest $request) 52 | { 53 | return $this->store($request); 54 | } 55 | 56 | /** 57 | * Store a new category. 58 | */ 59 | public function store(CategoryRequest $request) 60 | { 61 | $this->service->create($request->validated()); 62 | 63 | Helpers::flashMessage('Saved new category'); 64 | 65 | return redirect(route('blogetc.admin.categories.index')); 66 | } 67 | 68 | /** 69 | * @deprecated - use edit() 70 | */ 71 | public function edit_category($categoryId) 72 | { 73 | return $this->edit($categoryId); 74 | } 75 | 76 | /** 77 | * Show the edit form for category. 78 | */ 79 | public function edit(int $categoryID): View 80 | { 81 | $category = $this->service->find($categoryID); 82 | 83 | return view( 84 | 'blogetc_admin::categories.edit_category', 85 | [ 86 | 'category' => $category, 87 | ] 88 | ); 89 | } 90 | 91 | /** 92 | * @deprecated - use create() 93 | */ 94 | public function create_category() 95 | { 96 | return $this->create(); 97 | } 98 | 99 | /** 100 | * Show the form for creating new category. 101 | */ 102 | public function create(): View 103 | { 104 | return view('blogetc_admin::categories.add_category'); 105 | } 106 | 107 | /** 108 | * @deprecated - use update() 109 | */ 110 | public function update_category(CategoryRequest $request, $categoryId) 111 | { 112 | return $this->update($request, $categoryId); 113 | } 114 | 115 | /** 116 | * Save submitted changes. 117 | * 118 | * @return RedirectResponse|Redirector 119 | */ 120 | public function update(CategoryRequest $request, $categoryID) 121 | { 122 | $category = $this->service->update($categoryID, $request->validated()); 123 | 124 | Helpers::flashMessage('Updated category'); 125 | 126 | return redirect($category->editUrl()); 127 | } 128 | 129 | /** 130 | * @deprecated - use destroy() 131 | */ 132 | public function destroy_category(CategoryRequest $request, $categoryId) 133 | { 134 | return $this->destroy($request, $categoryId); 135 | } 136 | 137 | /** 138 | * Delete the category. 139 | */ 140 | public function destroy(/** @scrutinizer ignore-unused */ CategoryRequest $request, $categoryID) 141 | { 142 | $this->service->delete($categoryID); 143 | 144 | return view('blogetc_admin::categories.deleted_category'); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Controllers/Admin/ManageCommentsController.php: -------------------------------------------------------------------------------- 1 | middleware(UserCanManageBlogPosts::class); 28 | $this->service = $service; 29 | } 30 | 31 | /** 32 | * Show all comments (and show buttons with approve/delete). 33 | * 34 | * @return mixed 35 | */ 36 | public function index(Request $request) 37 | { 38 | //TODO - use service 39 | $comments = Comment::withoutGlobalScopes() 40 | ->orderBy('created_at', 'desc') 41 | ->with('post'); 42 | 43 | if ($request->get('waiting_for_approval')) { 44 | $comments->where('approved', false); 45 | } 46 | 47 | $comments = $comments->paginate(100); 48 | 49 | return view('blogetc_admin::comments.index', ['comments' => $comments]); 50 | } 51 | 52 | /** 53 | * Approve a comment. 54 | * 55 | * @param $blogCommentID 56 | */ 57 | public function approve(int $blogCommentID): RedirectResponse 58 | { 59 | $this->service->approve($blogCommentID); 60 | 61 | Helpers::flashMessage('Approved comment!'); 62 | 63 | return back(); 64 | } 65 | 66 | /** 67 | * Delete a submitted comment. 68 | * 69 | * @param $blogCommentID 70 | * 71 | * @throws Exception 72 | */ 73 | public function destroy(int $blogCommentID): RedirectResponse 74 | { 75 | $this->service->delete($blogCommentID); 76 | 77 | Helpers::flashMessage('Deleted comment!'); 78 | 79 | return back(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Controllers/Admin/ManagePostsController.php: -------------------------------------------------------------------------------- 1 | middleware(UserCanManageBlogPosts::class); 35 | 36 | $this->uploadsService = $uploadsService; 37 | 38 | if (!is_array(config('blogetc'))) { 39 | throw new RuntimeException('The config/blogetc.php does not exist. Publish the vendor files for the BlogEtc package by running the php artisan publish:vendor command'); 40 | } 41 | } 42 | 43 | /** 44 | * View all posts. 45 | * 46 | * @return mixed 47 | */ 48 | public function index() 49 | { 50 | $posts = Post::orderBy('posted_at', 'desc')->paginate(10); 51 | 52 | return view('blogetc_admin::index', ['posts' => $posts]); 53 | } 54 | 55 | /** 56 | * Show form for creating new post. 57 | * 58 | * @return Factory|View 59 | */ 60 | public function create() 61 | { 62 | return view('blogetc_admin::posts.add_post'); 63 | } 64 | 65 | /** 66 | * @deprecated - use create() instead 67 | */ 68 | public function create_post() 69 | { 70 | return $this->create(); 71 | } 72 | 73 | /** 74 | * Save a new post. 75 | * 76 | * @throws Exception 77 | * 78 | * @return RedirectResponse|Redirector 79 | */ 80 | public function store(CreateBlogEtcPostRequest $request) 81 | { 82 | $editUrl = $this->uploadsService->legacyStorePost($request); 83 | 84 | return redirect($editUrl); 85 | } 86 | 87 | /** 88 | * @deprecated use store() instead 89 | */ 90 | public function store_post(CreateBlogEtcPostRequest $request) 91 | { 92 | return $this->store($request); 93 | } 94 | 95 | /** 96 | * Show form to edit post. 97 | * 98 | * @param $blogPostId 99 | * 100 | * @return mixed 101 | */ 102 | public function edit($blogPostId) 103 | { 104 | $post = Post::findOrFail($blogPostId); 105 | 106 | return view('blogetc_admin::posts.edit_post', ['post' => $post]); 107 | } 108 | 109 | /** 110 | * @deprecated - use edit() instead 111 | */ 112 | public function edit_post($blogPostId) 113 | { 114 | return $this->edit($blogPostId); 115 | } 116 | 117 | /** 118 | * Save changes to a post. 119 | * 120 | * This uses some legacy code. This will get refactored soon into something nicer. 121 | * 122 | * @param $blogPostId 123 | * 124 | * @throws Exception 125 | * 126 | * @return RedirectResponse|Redirector 127 | */ 128 | public function update(UpdateBlogEtcPostRequest $request, $blogPostId) 129 | { 130 | $editUrl = $this->uploadsService->legacyUpdatePost($request, $blogPostId); 131 | 132 | return redirect($editUrl); 133 | } 134 | 135 | /** 136 | * @deprecated use update() instead 137 | */ 138 | public function update_post(UpdateBlogEtcPostRequest $request, $blogPostId) 139 | { 140 | return $this->update($request, $blogPostId); 141 | } 142 | 143 | /** 144 | * Delete a post. 145 | * 146 | * @param $blogPostId 147 | * 148 | * @return mixed 149 | */ 150 | public function destroy(DeleteBlogEtcPostRequest $request, $blogPostId) 151 | { 152 | $deletedPost = $this->uploadsService->legacyDestroyPost($request, $blogPostId); 153 | 154 | return view('blogetc_admin::posts.deleted_post') 155 | ->withDeletedPost($deletedPost); 156 | } 157 | 158 | /** 159 | * @deprecated - use destroy() instead 160 | */ 161 | public function destroy_post(DeleteBlogEtcPostRequest $request, $blogPostId) 162 | { 163 | return $this->destroy($request, $blogPostId); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Controllers/Admin/ManageUploadsController.php: -------------------------------------------------------------------------------- 1 | middleware(UserCanManageBlogPosts::class); 36 | $this->uploadsService = $uploadsService; 37 | $this->postsService = $postsService; 38 | 39 | if (!config('blogetc.image_upload_enabled')) { 40 | throw new RuntimeException('The blogetc.php config option is missing or has not enabled image uploading'); 41 | } 42 | } 43 | 44 | /** 45 | * Show the main listing of uploaded images. 46 | * 47 | * @return mixed 48 | */ 49 | public function index() 50 | { 51 | return view('blogetc_admin::imageupload.index', [ 52 | 'uploaded_photos' => UploadedPhoto::orderBy('id', 'desc') 53 | ->paginate(10), 54 | ]); 55 | } 56 | 57 | /** 58 | * show the form for uploading a new image. 59 | * 60 | * @return Factory|View 61 | */ 62 | public function create() 63 | { 64 | return view('blogetc_admin::imageupload.create', []); 65 | } 66 | 67 | /** 68 | * Save a new uploaded image. 69 | * 70 | * @throws Exception 71 | */ 72 | public function store(UploadImageRequest $request) 73 | { 74 | // Uses some legacy code - this will be refactored and fixed soon! 75 | $processed_images = $this->uploadsService->legacyProcessUploadedImagesSingle($request); 76 | 77 | return view('blogetc_admin::imageupload.uploaded', ['images' => $processed_images]); 78 | } 79 | 80 | public function deletePostImage(int $postId) 81 | { 82 | return view('blogetc_admin::imageupload.delete-post-image', ['postId' => $postId]); 83 | } 84 | 85 | public function deletePostImageConfirmed(int $postId) 86 | { 87 | $post = $this->postsService->findById($postId); 88 | 89 | $deletedSizes = $this->uploadsService->deletePostImage($post); 90 | $this->postsService->clearImageSizes($post, $deletedSizes); 91 | 92 | return view('blogetc_admin::imageupload.deleted-post-image'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Controllers/BlogEtcAdminController.php: -------------------------------------------------------------------------------- 1 | postsService = $postsService; 42 | $this->commentsService = $commentsService; 43 | $this->captchaService = $captchaService; 44 | } 45 | 46 | /** 47 | * @deprecated - use store instead 48 | */ 49 | public function addNewComment(AddNewCommentRequest $request, $slug) 50 | { 51 | return $this->store($request, $slug); 52 | } 53 | 54 | /** 55 | * Let a guest (or logged in user) submit a new comment for a blog post. 56 | */ 57 | public function store(AddNewCommentRequest $request, $slug) 58 | { 59 | if (CommentsService::COMMENT_TYPE_BUILT_IN !== config('blogetc.comments.type_of_comments_to_show')) { 60 | throw new RuntimeException('Built in comments are disabled'); 61 | } 62 | 63 | if (Gate::denies(GateTypes::ADD_COMMENT)) { 64 | abort(Response::HTTP_FORBIDDEN, 'Unable to add comments'); 65 | } 66 | 67 | $blogPost = $this->postsService->repository()->findBySlug($slug); 68 | 69 | $captcha = $this->captchaService->getCaptchaObject(); 70 | 71 | if ($captcha && method_exists($captcha, 'runCaptchaBeforeAddingComment')) { 72 | $captcha->runCaptchaBeforeAddingComment($request, $blogPost); 73 | } 74 | 75 | $comment = $this->commentsService->create( 76 | $blogPost, 77 | $request->validated(), 78 | $request->ip(), 79 | (int) Auth::id() 80 | ); 81 | 82 | return response()->view('blogetc::saved_comment', [ 83 | 'captcha' => $captcha, 84 | 'blog_post' => $blogPost, 85 | 'new_comment' => $comment, 86 | ])->setStatusCode(Response::HTTP_CREATED); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Controllers/FeedController.php: -------------------------------------------------------------------------------- 1 | id : 'guest'; 29 | 30 | $feed->setCache( 31 | config('blogetc.rssfeed.cache_in_minutes', 60), 32 | 'blogetc-'.$request->getFeedType().$user_or_guest 33 | ); 34 | 35 | if (!$feed->isCached()) { 36 | $this->makeFreshFeed($feed); 37 | } 38 | 39 | return $feed->render($request->getFeedType()); 40 | } 41 | 42 | /** 43 | * @param $feed 44 | */ 45 | protected function makeFreshFeed(Feed $feed) 46 | { 47 | $posts = Post::orderBy('posted_at', 'desc') 48 | ->limit(config('blogetc.rssfeed.posts_to_show_in_rss_feed', 10)) 49 | ->with('author') 50 | ->get(); 51 | 52 | $this->setupFeed($feed, $posts); 53 | 54 | /** @var Post $post */ 55 | foreach ($posts as $post) { 56 | $feed->add($post->title, 57 | $post->authorString(), 58 | $post->url(), 59 | $post->posted_at, 60 | $post->short_description, 61 | $post->generateIntroduction() 62 | ); 63 | } 64 | } 65 | 66 | /** 67 | * @param $posts 68 | * 69 | * @return mixed 70 | */ 71 | protected function setupFeed(Feed $feed, $posts) 72 | { 73 | $feed->title = config('app.name').' Blog'; 74 | $feed->description = config('blogetc.rssfeed.description', 'Our blog RSS feed'); 75 | $feed->link = route('blogetc.index'); 76 | $feed->setDateFormat('carbon'); 77 | $feed->pubdate = isset($posts[0]) ? $posts[0]->posted_at : Carbon::now()->subYear(); 78 | $feed->lang = config('blogetc.rssfeed.language', 'en'); 79 | $feed->setShortening(config('blogetc.rssfeed.should_shorten_text', true)); 80 | $feed->setTextLimit(config('blogetc.rssfeed.text_limit', 100)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Controllers/PostsController.php: -------------------------------------------------------------------------------- 1 | postsService = $postsService; 38 | $this->categoriesService = $categoriesService; 39 | $this->captchaService = $captchaService; 40 | } 41 | 42 | /** 43 | * Show the search results. 44 | */ 45 | public function search(SearchRequest $request): \Illuminate\Contracts\View\View 46 | { 47 | // Laravel full text search (swisnl/laravel-fulltext) disabled due to poor Laravel 8 support. 48 | // If you wish to add it, copy the code in this method that was in commit 9aff6c37d130. 49 | 50 | // The LIKE query is not efficient. Search can be disabled in config. 51 | $searchResults = Post::where('title', 'LIKE', '%'.$request->get('s').'%')->limit(100)->get(); 52 | 53 | // Map it so the post is actually accessible with ->indexable, for backwards compatibility in old view files. 54 | $searchResultsMappedWithIndexable = $searchResults->map(function (Post $post) { 55 | return new class($post) { 56 | public $indexable; 57 | 58 | public function __construct(Post $post) 59 | { 60 | $this->indexable = $post; 61 | } 62 | }; 63 | }); 64 | 65 | return view('blogetc::search', [ 66 | 'title' => 'Search results for '.e($request->searchQuery()), 67 | 'query' => $request->searchQuery(), 68 | 'search_results' => $searchResultsMappedWithIndexable, 69 | ]); 70 | } 71 | 72 | /** 73 | * @deprecated - use showCategory() instead 74 | */ 75 | public function view_category($categorySlug): \Illuminate\Contracts\View\View 76 | { 77 | return $this->showCategory($categorySlug); 78 | } 79 | 80 | /** 81 | * View posts in a category. 82 | */ 83 | public function showCategory($categorySlug): \Illuminate\Contracts\View\View 84 | { 85 | return $this->index($categorySlug); 86 | } 87 | 88 | /** 89 | * Show blog posts 90 | * If $categorySlug is set, then only show from that category. 91 | * 92 | * @param string $categorySlug 93 | * 94 | * @return mixed 95 | */ 96 | public function index(string $categorySlug = null) 97 | { 98 | // the published_at + is_published are handled by BlogEtcPublishedScope, and don't take effect if the logged 99 | // in user can manage log posts 100 | $title = config('blogetc.blog_index_title', 'Viewing blog'); 101 | 102 | // default category ID 103 | $categoryID = null; 104 | 105 | if ($categorySlug) { 106 | // get the category 107 | $category = $this->categoriesService->findBySlug($categorySlug); 108 | 109 | // get category ID to send to service 110 | $categoryID = $category->id; 111 | 112 | // TODO - make configurable 113 | $title = config('blogetc.blog_index_category_title', 'Viewing blog posts in ').$category->category_name; 114 | } 115 | 116 | $posts = $this->postsService->indexPaginated(config('blogetc.per_page'), $categoryID); 117 | 118 | return view('blogetc::index', [ 119 | 'posts' => $posts, 120 | 'title' => $title, 121 | 'blogetc_category' => $category ?? null, 122 | ]); 123 | } 124 | 125 | /** 126 | * @deprecated - use show() 127 | */ 128 | public function viewSinglePost(Request $request, string $blogPostSlug) 129 | { 130 | return $this->show($request, $blogPostSlug); 131 | } 132 | 133 | /** 134 | * View a single post and (if enabled) comments. 135 | */ 136 | public function show(Request $request, string $postSlug): \Illuminate\View\View 137 | { 138 | $blogPost = $this->postsService->findBySlug($postSlug); 139 | 140 | $usingCaptcha = $this->captchaService->getCaptchaObject(); 141 | 142 | if (null !== $usingCaptcha && method_exists($usingCaptcha, 'runCaptchaBeforeShowingPosts')) { 143 | $usingCaptcha->runCaptchaBeforeShowingPosts($request, $blogPost); 144 | } 145 | 146 | return view( 147 | 'blogetc::single_post', 148 | [ 149 | 'post' => $blogPost, 150 | 'captcha' => $usingCaptcha, 151 | 'comments' => $blogPost->comments->load('user'), 152 | ] 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Events/BlogPostAdded.php: -------------------------------------------------------------------------------- 1 | blogEtcPost = $post; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Events/BlogPostEdited.php: -------------------------------------------------------------------------------- 1 | blogEtcPost = $post; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Events/BlogPostWillBeDeleted.php: -------------------------------------------------------------------------------- 1 | blogEtcPost = $post; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Events/CategoryAdded.php: -------------------------------------------------------------------------------- 1 | blogEtcCategory = $category; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Events/CategoryEdited.php: -------------------------------------------------------------------------------- 1 | blogEtcCategory = $category; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Events/CategoryWillBeDeleted.php: -------------------------------------------------------------------------------- 1 | blogEtcCategory = $category; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Events/CommentAdded.php: -------------------------------------------------------------------------------- 1 | blogEtcPost = $post; 26 | $this->newComment = $newComment; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Events/CommentApproved.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 25 | // you can get the blog post via $comment->post 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Events/CommentWillBeDeleted.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Events/UploadedImage.php: -------------------------------------------------------------------------------- 1 | image_filename = $image_filename; 40 | $this->blogEtcPost = $blogEtcPost; 41 | $this->image = $image; 42 | $this->source = $source; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Exceptions/BlogEtcAuthGateNotImplementedException.php: -------------------------------------------------------------------------------- 1 | is_admin === true; 19 | // or: 20 | // return $model->email === 'your-email@your-site.com'; 21 | // }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Exceptions/CategoryNotFoundException.php: -------------------------------------------------------------------------------- 1 | define(Category::class, static function (Faker $faker) { 13 | return [ 14 | 'category_name' => $faker->sentence, 15 | 'slug' => Str::slug($faker->sentence), 16 | 'category_description' => $faker->paragraph, 17 | ]; 18 | }); 19 | -------------------------------------------------------------------------------- /src/Factories/CommentFactory.php: -------------------------------------------------------------------------------- 1 | define(Comment::class, static function (Faker $faker) { 13 | return [ 14 | 'blog_etc_post_id' => static function () { 15 | return factory(Post::class)->create()->id; 16 | }, 17 | 'user_id' => null, 18 | 'ip' => $faker->ipv4, 19 | 'author_name' => $faker->name, 20 | 'comment' => $faker->sentence, 21 | 'approved' => true, 22 | ]; 23 | }); 24 | -------------------------------------------------------------------------------- /src/Factories/PostFactory.php: -------------------------------------------------------------------------------- 1 | define(Post::class, static function (Faker $faker) { 14 | return [ 15 | 'title' => $faker->sentence, 16 | 'slug' => $faker->uuid, 17 | 'subtitle' => $faker->sentence, 18 | 'meta_desc' => $faker->paragraph, 19 | 'post_body' => $faker->paragraphs(5, true), 20 | 'posted_at' => Carbon::now()->subWeek(), 21 | 'is_published' => true, 22 | 'short_description' => $faker->paragraph, 23 | 'seo_title' => $faker->sentence, 24 | 'user_id' => null, 25 | ]; 26 | }); 27 | 28 | // Non published state. 29 | $factory->state(Post::class, 'not_published', [ 30 | 'is_published' => false, 31 | ]); 32 | 33 | // Post in future. 34 | $factory->state(Post::class, 'in_future', static function (Faker $faker) { 35 | return [ 36 | 'posted_at' => $faker->dateTimeBetween('now', '+2 years'), 37 | ]; 38 | }); 39 | -------------------------------------------------------------------------------- /src/Gates/DefaultAddCommentsGate.php: -------------------------------------------------------------------------------- 1 | canManageBlogEtcPosts(); 17 | } 18 | 19 | throw new BlogEtcAuthGateNotImplementedException(); 20 | }; 21 | -------------------------------------------------------------------------------- /src/Gates/GateTypes.php: -------------------------------------------------------------------------------- 1 | to auto insert the links to rss feed. 89 | */ 90 | public static function rssHtmlTag(): string 91 | { 92 | return '' 94 | .''; 96 | } 97 | 98 | /** 99 | * This method is depreciated. Just use the config() directly. 100 | * 101 | * @deprecated 102 | */ 103 | public static function image_sizes(): array 104 | { 105 | return config('blogetc.image_sizes'); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Interfaces/BaseRequestInterface.php: -------------------------------------------------------------------------------- 1 | ). 9 | * 10 | * @return string 11 | */ 12 | public function captcha_field_name(); 13 | 14 | /** 15 | * What view file should we use for the captcha field? 16 | * 17 | * @return string 18 | */ 19 | public function view(); 20 | 21 | /** 22 | * What rules should we use for the validation for this field? 23 | * 24 | * @return array 25 | */ 26 | public function rules(); 27 | 28 | // // optional methods, which are run if method_exists($captcha,'...'): 29 | // // do a search in the project to see how they are used. 30 | 31 | // /** 32 | // * executed when viewing single post 33 | // * @return void 34 | // */ 35 | // public function runCaptchaBeforeShowingPosts(); 36 | // 37 | // /** 38 | // * executed when posting new comment 39 | // * @return void 40 | // */ 41 | // public function runCaptchaBeforeAddingComment(); 42 | } 43 | -------------------------------------------------------------------------------- /src/Interfaces/LegacyGetImageFileInterface.php: -------------------------------------------------------------------------------- 1 | belongsToMany( 24 | Post::class, 25 | 'blog_etc_post_categories', 26 | 'blog_etc_category_id', 27 | 'blog_etc_post_id' 28 | ); 29 | } 30 | 31 | /** 32 | * Returns the public facing URL of showing blog posts in this category. 33 | * 34 | * @return string 35 | */ 36 | public function url(): string 37 | { 38 | return route('blogetc.view_category', $this->slug); 39 | } 40 | 41 | /** 42 | * @deprecated - use editUrl() 43 | */ 44 | public function edit_url(): string 45 | { 46 | return $this->editUrl(); 47 | } 48 | 49 | /** 50 | * Returns the URL for an admin user to edit this category. 51 | */ 52 | public function editUrl(): string 53 | { 54 | return route('blogetc.admin.categories.edit_category', $this->id); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Models/Comment.php: -------------------------------------------------------------------------------- 1 | 'boolean', 14 | ]; 15 | 16 | public $fillable = [ 17 | 'comment', 18 | 'author_name', 19 | ]; 20 | 21 | protected $table = 'blog_etc_comments'; 22 | 23 | /** 24 | * The "booting" method of the model. 25 | * 26 | * @return void 27 | */ 28 | protected static function boot() 29 | { 30 | parent::boot(); 31 | 32 | static::addGlobalScope(new BlogCommentApprovedAndDefaultOrderScope()); 33 | } 34 | 35 | /** 36 | * The associated Post for this comment.. 37 | * 38 | * @return BelongsTo 39 | */ 40 | public function post(): BelongsTo 41 | { 42 | return $this->belongsTo(Post::class, 'blog_etc_post_id'); 43 | } 44 | 45 | /** 46 | * Comment author user (if set). 47 | * 48 | * @return BelongsTo 49 | */ 50 | public function user(): BelongsTo 51 | { 52 | return $this->belongsTo(User::class); 53 | } 54 | 55 | /** 56 | * Return author string (either from the User (via ->user_id), or the submitted author_name value. 57 | * 58 | * @return string 59 | */ 60 | public function author() 61 | { 62 | if ($this->user_id) { 63 | $field = config('blogetc.comments.user_field_for_author_name', 'name'); 64 | 65 | return optional($this->user)->$field; 66 | } 67 | 68 | return $this->author_name; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Models/UploadedPhoto.php: -------------------------------------------------------------------------------- 1 | 'array', 13 | ]; 14 | 15 | public $fillable = [ 16 | 'image_title', 17 | 'uploader_id', 18 | 'source', 19 | 'uploaded_images', 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /src/Repositories/CategoriesRepository.php: -------------------------------------------------------------------------------- 1 | model = $model; 22 | } 23 | 24 | /** 25 | * Return all blog etc categories, ordered by category_name, paginated. 26 | */ 27 | public function indexPaginated(int $perPage = 25): LengthAwarePaginator 28 | { 29 | return $this->query() 30 | ->orderBy('category_name') 31 | ->paginate($perPage); 32 | } 33 | 34 | /** 35 | * Return new instance of the Query Builder for this model. 36 | */ 37 | public function query(): Builder 38 | { 39 | return $this->model->newQuery(); 40 | } 41 | 42 | /** 43 | * Find and return a blog etc category. 44 | */ 45 | public function find(int $categoryID): Category 46 | { 47 | try { 48 | return $this->query()->findOrFail($categoryID); 49 | } catch (ModelNotFoundException $e) { 50 | throw new CategoryNotFoundException('Unable to find a blog category with ID: '.$categoryID); 51 | } 52 | } 53 | 54 | /** 55 | * Find and return a blog etc category, based on its slug. 56 | */ 57 | public function findBySlug(string $categorySlug): Category 58 | { 59 | try { 60 | return $this->query()->where('slug', $categorySlug)->firstOrFail(); 61 | } catch (ModelNotFoundException $e) { 62 | throw new CategoryNotFoundException('Unable to find a blog category with slug: '.$categorySlug); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Repositories/CommentsRepository.php: -------------------------------------------------------------------------------- 1 | model = $model; 24 | } 25 | 26 | /** 27 | * Approve a blog comment. 28 | */ 29 | public function approve(int $blogCommentID): Comment 30 | { 31 | $comment = $this->find($blogCommentID, false); 32 | $comment->approved = true; 33 | $comment->save(); 34 | 35 | return $comment; 36 | } 37 | 38 | /** 39 | * Find and return a comment by ID. 40 | * 41 | * If $onlyApproved is true, then it will only return an approved comment 42 | * If it is false then it can return it even if not yet approved 43 | */ 44 | public function find(int $blogEtcCommentID, bool $onlyApproved = true): Comment 45 | { 46 | try { 47 | $queryBuilder = $this->query(true); 48 | 49 | if (!$onlyApproved) { 50 | $queryBuilder->withoutGlobalScopes(); 51 | } 52 | 53 | return $queryBuilder->findOrFail($blogEtcCommentID); 54 | } catch (ModelNotFoundException $e) { 55 | throw new CommentNotFoundException('Unable to find blog post comment with ID: '.$blogEtcCommentID); 56 | } 57 | } 58 | 59 | /** 60 | * Return new instance of the Query Builder for this model. 61 | */ 62 | public function query(bool $eagerLoad = false): Builder 63 | { 64 | $queryBuilder = $this->model->newQuery(); 65 | 66 | if (true === $eagerLoad) { 67 | $queryBuilder->with('post'); 68 | } 69 | 70 | return $queryBuilder; 71 | } 72 | 73 | /** 74 | * Create a comment. 75 | */ 76 | public function create( 77 | Post $post, 78 | array $attributes, 79 | string $ip = null, 80 | string $authorWebsite = null, 81 | string $authorEmail = null, 82 | int $userID = null, 83 | bool $autoApproved = false 84 | ): Comment { 85 | // TODO - inject the model object, put into repo, generate $attributes 86 | $newComment = new Comment($attributes); 87 | 88 | $newComment->ip = $ip; 89 | $newComment->author_website = $authorWebsite; 90 | $newComment->author_email = $authorEmail; 91 | $newComment->user_id = $userID; 92 | $newComment->approved = $autoApproved; 93 | 94 | $post->comments()->save($newComment); 95 | 96 | return $newComment; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Repositories/PostsRepository.php: -------------------------------------------------------------------------------- 1 | model = $model; 29 | } 30 | 31 | /** 32 | * Return blog posts ordered by posted_at, paginated. 33 | * 34 | * @param int $categoryID 35 | */ 36 | public function indexPaginated(int $perPage = 10, int $categoryID = null): LengthAwarePaginator 37 | { 38 | $query = $this->query(true) 39 | ->orderBy('posted_at', 'desc'); 40 | 41 | if ($categoryID > 0) { 42 | $query->whereHas('categories', static function (Builder $query) use ($categoryID) { 43 | $query->where('blog_etc_post_categories.blog_etc_category_id', $categoryID); 44 | })->get(); 45 | } 46 | 47 | return $query->paginate($perPage); 48 | } 49 | 50 | /** 51 | * Return new instance of the Query Builder for this model. 52 | */ 53 | public function query(bool $eagerLoad = false): Builder 54 | { 55 | $queryBuilder = $this->model->newQuery(); 56 | 57 | if (true === $eagerLoad) { 58 | $queryBuilder->with(['categories']); 59 | } 60 | 61 | return $queryBuilder; 62 | } 63 | 64 | /** 65 | * Return posts for RSS feed. 66 | * 67 | * @return Builder[]|Collection 68 | */ 69 | public function rssItems(): Collection 70 | { 71 | return $this->query(false) 72 | ->orderBy('posted_at', 'desc') 73 | ->limit(config('blogetc.rssfeed.posts_to_show_in_rss_feed')) 74 | ->with('author') 75 | ->get(); 76 | } 77 | 78 | /** 79 | * Find a blog etc post by slug 80 | * If cannot find, throw exception. 81 | */ 82 | public function findBySlug(string $slug): Post 83 | { 84 | try { 85 | // the published_at + is_published are handled by BlogEtcPublishedScope, and don't take effect if the 86 | // logged in user can manage log posts 87 | return $this->query(true) 88 | ->where('slug', $slug) 89 | ->firstOrFail(); 90 | } catch (ModelNotFoundException $e) { 91 | throw new PostNotFoundException('Unable to find blog post with slug: '.$slug); 92 | } 93 | } 94 | 95 | /** 96 | * Find a blog etc post by ID 97 | * If cannot find, throw exception. 98 | */ 99 | public function findById(int $id): Post 100 | { 101 | try { 102 | // the published_at + is_published are handled by BlogEtcPublishedScope, and don't take effect if the 103 | // logged in user can manage log posts 104 | return $this->query(true) 105 | ->where('id', $id) 106 | ->firstOrFail(); 107 | } catch (ModelNotFoundException $e) { 108 | throw new PostNotFoundException('Unable to find blog post with id: '.$id); 109 | } 110 | } 111 | 112 | /** 113 | * Create a new BlogEtcPost post. 114 | */ 115 | public function create(array $attributes): Post 116 | { 117 | return $this->query()->create($attributes); 118 | } 119 | 120 | /** 121 | * Delete a post. 122 | * 123 | * @throws Exception 124 | */ 125 | public function delete(int $postID): bool 126 | { 127 | $post = $this->find($postID); 128 | 129 | return (bool) $post->delete(); 130 | } 131 | 132 | /** 133 | * Find a blog etc post by ID 134 | * If cannot find, throw exception. 135 | */ 136 | public function find(int $blogEtcPostID): Post 137 | { 138 | try { 139 | return $this->query(true)->findOrFail($blogEtcPostID); 140 | } catch (ModelNotFoundException $e) { 141 | throw new PostNotFoundException('Unable to find blog post with ID: '.$blogEtcPostID); 142 | } 143 | } 144 | 145 | /** 146 | * Update image sizes (or in theory any attribute) on a blog etc post. 147 | * 148 | * TODO - currently untested. 149 | * 150 | * @param array $uploadedImages 151 | */ 152 | public function updateImageSizes(Post $post, ?array $uploadedImages): Post 153 | { 154 | if (!empty($uploadedImages)) { 155 | // does not use update() here as it would require fillable for each field - and in theory someone 156 | // might want to add more image sizes. 157 | foreach ($uploadedImages as $size => $imageName) { 158 | $post->$size = $imageName; 159 | } 160 | $post->save(); 161 | } 162 | 163 | return $post; 164 | } 165 | 166 | /** 167 | * Search for posts. 168 | * 169 | * This is a rough implementation - proper full text search has been removed in current version. 170 | */ 171 | public function search(string $search, int $max = 25): Collection 172 | { 173 | $query = $this->query(true)->limit($max); 174 | 175 | trim($search) 176 | ? $query->where('title', 'like', '%'.$search) 177 | : $query->where('title', ''); 178 | 179 | return $query->get(); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Repositories/UploadedPhotosRepository.php: -------------------------------------------------------------------------------- 1 | model = $model; 24 | } 25 | 26 | /** 27 | * Create a new Uploaded Photo row in the database. 28 | */ 29 | public function create(array $attributes): UploadedPhoto 30 | { 31 | return $this->query()->create($attributes); 32 | } 33 | 34 | /** 35 | * Return new instance of the Query Builder for this model. 36 | */ 37 | public function query(): Builder 38 | { 39 | return $this->model->newQuery(); 40 | } 41 | 42 | /** 43 | * Delete a uploaded photo from the database. 44 | */ 45 | public function delete(int $uploadedPhotoID): ?bool 46 | { 47 | $uploadedPhoto = $this->find($uploadedPhotoID); 48 | 49 | return $uploadedPhoto->delete(); 50 | } 51 | 52 | /** 53 | * Find a blog etc uploaded photo by ID. 54 | * 55 | * If cannot find, throw exception. 56 | */ 57 | public function find(int $uploadedPhotoID): UploadedPhoto 58 | { 59 | try { 60 | return $this->query()->findOrFail($uploadedPhotoID); 61 | } catch (ModelNotFoundException $e) { 62 | throw new UploadedPhotoNotFoundException('Unable to find Uploaded Photo with ID: '.$uploadedPhotoID); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Requests/AddNewCommentRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'min:3', 'max:1000'], 23 | 'author_name' => ['string', 'min:1', 'max:50'], 24 | 'author_email' => ['string', 'nullable', 'min:1', 'max:254', 'email'], 25 | 'author_website' => ['string', 'nullable', 'min:'.strlen('http://a.b'), 'max:175', 'active_url'], 26 | ]; 27 | 28 | $return['author_name'][] = Auth::check() && config('blogetc.comments.save_user_id_if_logged_in', true) 29 | ? 'nullable' 30 | : 'required'; 31 | 32 | if (config('blogetc.captcha.captcha_enabled')) { 33 | /** @var string $captcha_class */ 34 | $captcha_class = config('blogetc.captcha.captcha_type'); 35 | 36 | /** @var CaptchaInterface $captcha */ 37 | $captcha = new $captcha_class(); 38 | 39 | $return[$captcha->captcha_field_name()] = $captcha->rules(); 40 | } 41 | 42 | // in case you need to implement something custom, you can use this... 43 | if (config('blogetc.comments.rules') && is_callable(config('blogetc.comments.rules'))) { 44 | /** @var callable $func */ 45 | $func = config('blogetc.comments.rules'); 46 | $return = $func($return); 47 | } 48 | 49 | if (config('blogetc.comments.require_author_email')) { 50 | $return['author_email'][] = 'required'; 51 | } 52 | 53 | return $return; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Requests/BaseAdminRequest.php: -------------------------------------------------------------------------------- 1 | ['nullable', 'date'], 44 | 'title' => ['required', 'string', 'min:1', 'max:255'], 45 | 'subtitle' => ['nullable', 'string', 'min:1', 'max:255'], 46 | 'post_body' => ['required_without:use_view_file', 'max:2000000'], //medium text 47 | 'meta_desc' => ['nullable', 'string', 'min:1', 'max:1000'], 48 | 'short_description' => ['nullable', 'string', 'max:30000'], 49 | 'slug' => [ 50 | 'nullable', 51 | 'string', 52 | 'min:1', 53 | 'max:150', 54 | 'alpha_dash', // this field should have some additional rules, which is done in the subclasses. 55 | ], 56 | 'category' => ['nullable', 'array'], 57 | ]; 58 | 59 | // is use_custom_view_files true? 60 | if (config('blogetc.use_custom_view_files')) { 61 | $return['use_view_file'] = ['nullable', 'string', 'alpha_num', 'min:1', 'max:75']; 62 | } else { 63 | // use_view_file is disabled, so give an empty if anything is submitted via this function: 64 | $return['use_view_file'] = ['string', $disabled_use_view_file]; 65 | } 66 | 67 | // some additional rules for uploaded images 68 | foreach ((array) config('blogetc.image_sizes') as $size => $image_detail) { 69 | if ($image_detail['enabled'] && config('blogetc.image_upload_enabled')) { 70 | $return[$size] = ['nullable', 'image']; 71 | } else { 72 | // was not enabled (or all images are disabled), so show an error if it was submitted: 73 | $return[$size] = $show_error_if_has_value; 74 | } 75 | } 76 | 77 | return $return; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Requests/BaseRequest.php: -------------------------------------------------------------------------------- 1 | method()) { 18 | // No rules are required for deleting. 19 | return []; 20 | } 21 | $rules = [ 22 | 'category_name' => ['required', 'string', 'min:1', 'max:200'], 23 | 'slug' => ['required', 'alpha_dash', 'max:100', 'min:1'], 24 | 'category_description' => ['nullable', 'string', 'min:1', 'max:5000'], 25 | ]; 26 | 27 | if (Request::METHOD_POST === $this->method()) { 28 | $rules['slug'][] = Rule::unique('blog_etc_categories', 'slug'); 29 | } 30 | 31 | if (in_array($this->method(), [Request::METHOD_PUT, Request::METHOD_PATCH], true)) { 32 | $rules['slug'][] = Rule::unique('blog_etc_categories', 'slug') 33 | ->ignore($this->route()->parameter('categoryId')); 34 | } 35 | 36 | return $rules; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Requests/CreateBlogEtcPostRequest.php: -------------------------------------------------------------------------------- 1 | baseBlogPostRules(); 23 | $return['slug'][] = Rule::unique('blog_etc_posts', 'slug'); 24 | 25 | return $return; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Requests/DeleteBlogEtcPostRequest.php: -------------------------------------------------------------------------------- 1 | [Rule::in(['rss', 'atom'])], 27 | ]; 28 | } 29 | 30 | /** 31 | * Is this request for an RSS feed or Atom feed? defaults to atom. 32 | * 33 | * @return string 34 | */ 35 | public function getFeedType(): string 36 | { 37 | return 'rss' === $this->get('type') 38 | ? 'rss' 39 | : 'atom'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Requests/SearchRequest.php: -------------------------------------------------------------------------------- 1 | ['nullable', 'string', 'min:3', 'max:40'], 27 | ]; 28 | } 29 | 30 | /** 31 | * Return the query that user searched for. 32 | */ 33 | public function searchQuery(): string 34 | { 35 | return $this->get('s', ''); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Requests/Traits/HasCategoriesTrait.php: -------------------------------------------------------------------------------- 1 | get('category') || !is_array($this->get('category'))) { 20 | return []; 21 | } 22 | 23 | //$this->get("category") is an array of category SLUGs and we need IDs 24 | 25 | // check they are valid, return the IDs 26 | // limit to 1000 ... just in case someone submits with too many for the web server. No error is given if they submit more than 1k. 27 | $vals = Category::whereIn('id', array_keys($this->get('category')))->select('id')->limit(1000)->get(); 28 | $vals = array_values($vals->pluck('id')->toArray()); 29 | 30 | return $vals; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Requests/Traits/HasImageUploadTrait.php: -------------------------------------------------------------------------------- 1 | file($size)) { 17 | return $this->file($size); 18 | } 19 | 20 | // not found? lets cycle through all the images and see if anything was submitted, and use that instead 21 | foreach (config('blogetc.image_sizes') as $image_size_name => $image_size_info) { 22 | if ($this->file($image_size_name)) { 23 | return $this->file($image_size_name); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Requests/UpdateBlogEtcPostRequest.php: -------------------------------------------------------------------------------- 1 | baseBlogPostRules(); 23 | $return['slug'][] = Rule::unique('blog_etc_posts', 'slug')->ignore($this->route()->parameter('blogPostId')); 24 | 25 | return $return; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Requests/UploadImageRequest.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'required', 12 | 'array', 13 | ], 14 | 'sizes_to_upload.*' => [ 15 | 'string', 16 | 'max:100', 17 | ], 18 | 'upload' => [ 19 | 'required', 20 | 'image', 21 | ], 22 | 'image_title' => [ 23 | 'required', 24 | 'string', 25 | 'min:1', 26 | 'max:150', 27 | ], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Scopes/BlogCommentApprovedAndDefaultOrderScope.php: -------------------------------------------------------------------------------- 1 | orderBy('id', 'asc'); 21 | $builder->limit(config('blogetc.comments.max_num_of_comments_to_show', 500)); 22 | $builder->where('approved', true); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Scopes/BlogEtcPublishedScope.php: -------------------------------------------------------------------------------- 1 | where('is_published', true); 23 | $builder->where('posted_at', '<=', Carbon::now()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Services/CaptchaService.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 31 | } 32 | 33 | /** 34 | * Return paginated collection of categories. 35 | */ 36 | public function indexPaginated(int $perPage = 25): LengthAwarePaginator 37 | { 38 | return $this->repository->indexPaginated($perPage); 39 | } 40 | 41 | /** 42 | * Find and return a blog etc category, based on its slug. 43 | */ 44 | public function findBySlug(string $categorySlug): Category 45 | { 46 | return $this->repository->findBySlug($categorySlug); 47 | } 48 | 49 | /** 50 | * Create a new Category entry. 51 | */ 52 | public function create(array $attributes): Category 53 | { 54 | $newCategory = new Category($attributes); 55 | $newCategory->save(); 56 | 57 | event(new CategoryAdded($newCategory)); 58 | 59 | return $newCategory; 60 | } 61 | 62 | /** 63 | * Update an existing Category entry. 64 | */ 65 | public function update(int $categoryID, array $attributes): Category 66 | { 67 | $category = $this->find($categoryID); 68 | $category->fill($attributes); 69 | $category->save(); 70 | 71 | event(new CategoryEdited($category)); 72 | 73 | return $category; 74 | } 75 | 76 | /** 77 | * Find and return a blog etc category from it's ID. 78 | */ 79 | public function find(int $categoryID): Category 80 | { 81 | return $this->repository->find($categoryID); 82 | } 83 | 84 | /** 85 | * Delete a BlogEtcCategory. 86 | * 87 | * @throws Exception 88 | */ 89 | public function delete(int $categoryID): void 90 | { 91 | $category = $this->find($categoryID); 92 | 93 | event(new CategoryWillBeDeleted($category)); 94 | 95 | $category->delete(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Services/CommentsService.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 35 | } 36 | 37 | /** 38 | * BlogEtcCategoriesRepository repository - for query heavy method. 39 | */ 40 | public function repository(): CommentsRepository 41 | { 42 | return $this->repository; 43 | } 44 | 45 | /** 46 | * Create a new comment. 47 | */ 48 | public function create( 49 | Post $blogEtcPost, 50 | array $attributes, 51 | string $ip = null, 52 | int $userID = null 53 | ): Comment { 54 | $ip = config('blogetc.comments.save_ip_address') 55 | ? $ip : null; 56 | 57 | $authorWebsite = config('blogetc.comments.ask_for_author_website') && !empty($attributes['author_website']) 58 | ? $attributes['author_website'] 59 | : null; 60 | 61 | $authorEmail = config('blogetc.comments.ask_for_author_website') && !empty($attributes['author_email']) 62 | ? $attributes['author_email'] 63 | : null; 64 | 65 | $userID = config('blogetc.comments.save_user_id_if_logged_in') 66 | ? $userID 67 | : null; 68 | 69 | $approved = $this->autoApproved(); 70 | 71 | $newComment = $this->repository->create( 72 | $blogEtcPost, 73 | $attributes, 74 | $ip, 75 | $authorWebsite, 76 | $authorEmail, 77 | $userID, 78 | $approved 79 | ); 80 | 81 | event(new CommentAdded($blogEtcPost, $newComment)); 82 | 83 | return $newComment; 84 | } 85 | 86 | /** 87 | * Are comments auto approved? 88 | */ 89 | protected function autoApproved(): bool 90 | { 91 | return true === config('blogetc.comments.auto_approve_comments', true); 92 | } 93 | 94 | /** 95 | * Approve a blog comment. 96 | */ 97 | public function approve(int $blogCommentID): Comment 98 | { 99 | $comment = $this->repository->approve($blogCommentID); 100 | event(new CommentApproved($comment)); 101 | 102 | return $comment; 103 | } 104 | 105 | /** 106 | * Delete a blog comment. 107 | * 108 | * Returns the now deleted comment object 109 | * 110 | * @throws Exception 111 | */ 112 | public function delete(int $blogCommentID): Comment 113 | { 114 | $comment = $this->find($blogCommentID, false); 115 | event(new CommentWillBeDeleted($comment)); 116 | $comment->delete(); 117 | 118 | return $comment; 119 | } 120 | 121 | /** 122 | * Find and return a comment by ID. 123 | */ 124 | public function find(int $blogEtcCommentID, bool $onlyApproved = true): Comment 125 | { 126 | return $this->repository->find($blogEtcCommentID, $onlyApproved); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Services/FeedService.php: -------------------------------------------------------------------------------- 1 | postsService = $postsService; 28 | } 29 | 30 | /** 31 | * Build the Feed object and populate it with blog posts. 32 | */ 33 | public function getFeed(Feed $feed, string $feedType): Response 34 | { 35 | // RSS feed is cached. Admin/writer users might see different content, so 36 | // use a different cache for different users. 37 | 38 | // This should not be a problem unless your site has many logged in users. 39 | // (Use check(), as it is possible for user to be logged in without having an ID (depending on how the guard 40 | // is set up...) 41 | $userOrGuest = Auth::check() 42 | ? 'logged-in-'.Auth::id() 43 | : 'guest'; 44 | 45 | $key = 'blogetc-'.$feedType.$userOrGuest; 46 | 47 | $feed->setCache( 48 | config('blogetc.rssfeed.cache_in_minutes', 60), 49 | $key 50 | ); 51 | 52 | if (!$feed->isCached()) { 53 | $this->makeFreshFeed($feed); 54 | } 55 | 56 | return $feed->render($feedType); 57 | } 58 | 59 | /** 60 | * Create fresh feed by passing latest blog posts. 61 | * 62 | * @param $feed 63 | */ 64 | protected function makeFreshFeed(Feed $feed): void 65 | { 66 | $blogPosts = $this->postsService->rssItems(); 67 | 68 | $this->setupFeed( 69 | $feed, 70 | $this->pubDate($blogPosts) 71 | ); 72 | 73 | /** @var Post $blogPost */ 74 | foreach ($blogPosts as $blogPost) { 75 | $feed->add( 76 | $blogPost->title, 77 | $blogPost->authorString(), 78 | $blogPost->url(), 79 | $blogPost->posted_at, 80 | $blogPost->short_description 81 | ); 82 | } 83 | } 84 | 85 | /** 86 | * Basic set up of the Feed object. 87 | */ 88 | protected function setupFeed(Feed $feed, Carbon $pubDate): Feed 89 | { 90 | $feed->title = config('blogetc.rssfeed.title'); 91 | $feed->description = config('blogetc.rssfeed.description'); 92 | $feed->link = route('blogetc.index'); 93 | $feed->lang = config('blogetc.rssfeed.language'); 94 | $feed->setShortening(config('blogetc.rssfeed.should_shorten_text')); 95 | $feed->setTextLimit(config('blogetc.rssfeed.text_limit')); 96 | $feed->setDateFormat('carbon'); 97 | $feed->pubdate = $pubDate; 98 | 99 | return $feed; 100 | } 101 | 102 | /** 103 | * Return the first post posted_at date, or if none exist then return today. 104 | */ 105 | protected function pubDate(Collection $blogPosts): Carbon 106 | { 107 | return $blogPosts->first() 108 | ? $blogPosts->first()->posted_at 109 | : Carbon::now(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Views/blogetc/captcha/basic.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 5 | 7 |
8 | -------------------------------------------------------------------------------- /src/Views/blogetc/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends("layouts.app",['title'=>$title]) 2 | @section("content") 3 |
4 |
5 | @can(\WebDevEtc\BlogEtc\Gates\GateTypes::MANAGE_BLOG_ADMIN) 6 |
7 |

8 | You are logged in as a blog admin user. 9 |
10 | 12 | 13 | Go To Blog Admin Panel 14 | 15 |

16 |
17 | @endcan 18 | 19 | @if(isset($blogetc_category) && $blogetc_category) 20 |

21 | Viewing Category: {{$blogetc_category->category_name}} 22 |

23 | @if($blogetc_category->category_description) 24 |

{{$blogetc_category->category_description}}

25 | @endif 26 | @endif 27 | 28 | @forelse($posts as $post) 29 | @include("blogetc::partials.index_loop") 30 | @empty 31 |
No posts
32 | @endforelse 33 | 34 |
35 | {{$posts->appends( [] )->links()}} 36 |
37 | @include("blogetc::sitewide.search_form") 38 |
39 |
40 | @endsection 41 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/add_comment_form.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | use WebDevEtc\BlogEtc\Captcha\CaptchaAbstract; 3 | use WebDevEtc\BlogEtc\Models\Post; 4 | /** @var Post $post */ 5 | /** @var CaptchaAbstract $captcha */ 6 | @endphp 7 | @can(\WebDevEtc\BlogEtc\Gates\GateTypes::ADD_COMMENT) 8 |
9 |
Add a comment
10 |
11 | @csrf 12 | 13 |
14 | 15 | 16 | 23 | 24 |
25 | 26 |
27 |
28 | @if(config("blogetc.comments.save_user_id_if_logged_in", true) === false || !Auth::check()) 29 |
30 |
31 | 32 | 40 |
41 |
42 | 43 | @if(config("blogetc.comments.ask_for_author_email")) 44 |
45 |
46 | 49 | 57 |
58 |
59 | @endif 60 | @endif 61 | 62 | @if(config("blogetc.comments.ask_for_author_website")) 63 |
64 |
65 | 68 | 75 |
76 |
77 | 78 | @endif 79 |
80 |
81 | 82 | @if($captcha) 83 | {{-- Captcha is enabled. Load the type class and then include the view as defined in the captcha class. --}} 84 | @include($captcha->view()) 85 | @endif 86 | 87 |
88 | 89 |
90 |
91 |
92 | @endcan 93 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/author.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \WebDevEtc\BlogEtc\Models\Post $post */ 3 | @endphp 4 | 5 | by {{$post->author->name}} 6 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/built_in_comments.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \WebDevEtc\BlogEtc\Models\Comment[] $comments */ 3 | @endphp 4 | @forelse($comments as $comment) 5 |
6 |
7 | {{ $comment->author() }} 8 | @if(config('blogetc.comments.ask_for_author_website') && $comment->author_website) 9 | (website) 10 | @endif 11 | 12 | {{ $comment->created_at->diffForHumans() }} 13 | 14 |
15 |
16 |

{!! nl2br(e($comment->comment)) !!}

17 |
18 |
19 | @empty 20 |
21 | No comments yet! 22 | @can(\WebDevEtc\BlogEtc\Gates\GateTypes::ADD_COMMENT) 23 | Why don't you be the first? 24 | @endcan 25 |
26 | @endforelse 27 | 28 | @if(count($comments) >= config('blogetc.comments.max_num_of_comments_to_show', 500)) 29 |

30 | Only the first {{ config('blogetc.comments.max_num_of_comments_to_show', 500) }} comments are shown. 31 |

32 | @endif 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/categories.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \WebDevEtc\BlogEtc\Models\Post $post */ 3 | @endphp 4 |
5 | @foreach($post->categories as $category) 6 | 7 | {{ $category->category_name }} 8 | 9 | @endforeach 10 |
11 | 12 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/custom_comments.blade.php: -------------------------------------------------------------------------------- 1 |
2 | Error! custom_comments: You must customise this by creating a file in 3 | /resources/views/vendor/blogetc/partials/custom_comments.blade.php 4 |
5 | 6 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/disqus_comments.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \WebDevEtc\BlogEtc\Models\Post $post */ 3 | @endphp 4 |
5 | 6 | 18 | 20 | 21 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/full_post_details.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \WebDevEtc\BlogEtc\Models\Post $post */ 3 | @endphp 4 | @can(\WebDevEtc\BlogEtc\Gates\GateTypes::MANAGE_BLOG_ADMIN) 5 | 6 | Edit Post 7 | 8 | @endcan 9 | 10 |

{{$post->title}}

11 |
{{$post->subtitle}}
12 | 13 | {!! $post->imageTag('medium', false, 'd-block mx-auto') !!} 14 | 15 |

16 | {!! $post->renderBody() !!} 17 | 18 | {{--@if(config("blogetc.use_custom_view_files") && $post->use_view_file)--}} 19 | {{-- // use a custom blade file for the output of those blog post--}} 20 | {{-- @include("blogetc::partials.use_view_file")--}} 21 | {{--@else--}} 22 | {{-- {!! $post->post_body !!} // unsafe, echoing the plain html/js--}} 23 | {{-- {{ $post->post_body }} // for safe escaping --}} 24 | {{--@endif--}} 25 |

26 | 27 |
28 | 29 | @if($post->posted_at) 30 | Posted {{ $post->posted_at->diffForHumans() }} 31 | @endif 32 | 33 | @includeWhen($post->author, 'blogetc::partials.author', ['post'=>$post]) 34 | @includeWhen($post->categories, 'blogetc::partials.categories', ['post'=>$post]) 35 | 36 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/index_loop.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \WebDevEtc\BlogEtc\Models\Post $post */ 3 | @endphp 4 | {{--Used on the index page (so shows a small summary--}} 5 | {{--See the guide on webdevetc.com for how to copy these files to your /resources/views/ directory--}} 6 | {{--https://webdevetc.com/laravel/packages/blogetc-blog-system-for-your-laravel-app/help-documentation/laravel-blog-package-blogetc#guide_to_views--}} 7 | 8 |
9 | 10 |
11 | {!! $post->imageTag('medium', true, '') !!} 12 |
13 |
14 |

{{$post->title}}

15 |
{{$post->subtitle}}
16 | 17 | @if(config('blogetc.show_full_post_on_index')) 18 | {!! $post->renderBody() !!} 19 | @else 20 |

{!! $post->generateIntroduction(400) !!}

21 | @endif 22 | 23 |
24 | View Post 25 |
26 |
27 |
28 | 29 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/search_form.blade.php: -------------------------------------------------------------------------------- 1 | {{--This is only included for backwards compatibility. It will be removed at a future stage.--}} 2 | @include('blogetc::sitewide.search_form') 3 | 4 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/show_comments.blade.php: -------------------------------------------------------------------------------- 1 | @switch(config("blogetc.comments.type_of_comments_to_show","built_in")) 2 | 3 | @case("built_in") 4 | @include("blogetc::partials.built_in_comments") 5 | @include("blogetc::partials.add_comment_form") 6 | @break 7 | 8 | @case("disqus") 9 | @include("blogetc::partials.disqus_comments") 10 | @break 11 | 12 | 13 | @case("custom") 14 | @include("blogetc::partials.custom_comments") 15 | @break 16 | 17 | @case("disabled") 18 | 21 | @break 22 | 23 | @default 24 |
25 | Invalid comment type_of_comments_to_show config option 26 |
27 | @endswitch 28 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/show_errors.blade.php: -------------------------------------------------------------------------------- 1 | @if (isset($errors) && count($errors)) 2 |
3 | Sorry, but there was an error: 4 | 9 |
10 | @endif 11 | -------------------------------------------------------------------------------- /src/Views/blogetc/partials/use_view_file.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \WebDevEtc\BlogEtc\Models\Post $post */ 3 | @endphp 4 | @if(View::exists($post->bladeViewFile())) 5 | {{--view file existed, so include it.--}} 6 | @include("custom_blog_posts." . $post->use_view_file, ['post' =>$post]) 7 | @else 8 | {{-- the view file wasn't there. Show a detailed error if user is logged in and can manage the blog, otherwise show generic error.--}} 9 | 10 | @can(\WebDevEtc\BlogEtc\Gates\GateTypes::MANAGE_BLOG_ADMIN) 11 |
12 | Custom blog post blade view file 13 | ({{$post->bladeViewFile()}}) not found. 14 | 17 | See Laravel Blog Package help here. 18 | 19 |
20 | @else 21 |
22 | Sorry, but there is an error showing that blog post. Please come back later. 23 |
24 | @endcan 25 | @endif 26 | 27 | -------------------------------------------------------------------------------- /src/Views/blogetc/saved_comment.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app',['title'=>'Saved comment']) 2 | @section('content') 3 |
4 |

5 | Thanks! Your comment has been saved! 6 |

7 | 8 | @if(!config('blogetc.comments.auto_approve_comments', false)) 9 |

10 | After an admin user approves the comment, it'll appear on the site! 11 |

12 | @endif 13 | 14 | 15 | Back to blog post 16 | 17 |
18 | @endsection 19 | 20 | -------------------------------------------------------------------------------- /src/Views/blogetc/search.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app',['title' => $title]) 2 | @section('content') 3 | 4 | {{--https://webdevetc.com/laravel/packages/blogetc-blog-system-for-your-laravel-app/help-documentation/laravel-blog-package-blogetc#guide_to_views--}} 5 | 6 |
7 |
8 |

Search Results for {{ $query }}

9 | 10 | @forelse($search_results as $result) 11 |

Search result #{{ $loop->iteration }}

12 | @include('blogetc::partials.index_loop', ['post' => $result->indexable]) 13 | @empty 14 |
Sorry, but there were no results!
15 | @endforelse 16 | 17 | @include('blogetc::sitewide.search_form') 18 |
19 |
20 | @endsection 21 | 22 | -------------------------------------------------------------------------------- /src/Views/blogetc/single_post.blade.php: -------------------------------------------------------------------------------- 1 | @extends("layouts.app",['title'=>$post->genSeoTitle()]) 2 | @section("content") 3 | {{--https://webdevetc.com/laravel/packages/blogetc-blog-system-for-your-laravel-app/help-documentation/laravel-blog-package-blogetc#guide_to_views--}} 4 |
5 |
6 |
7 | @include("blogetc::partials.show_errors") 8 | @include("blogetc::partials.full_post_details") 9 | 10 | @if(config("blogetc.comments.type_of_comments_to_show","built_in") !== 'disabled') 11 |
12 |

Comments

13 | @include("blogetc::partials.show_comments") 14 |
15 | @endif 16 |
17 |
18 |
19 | @endsection 20 | 21 | -------------------------------------------------------------------------------- /src/Views/blogetc/sitewide/random_posts.blade.php: -------------------------------------------------------------------------------- 1 |
Random Posts
2 | 12 | 13 | -------------------------------------------------------------------------------- /src/Views/blogetc/sitewide/recent_posts.blade.php: -------------------------------------------------------------------------------- 1 |
Recent Posts
2 | 12 | 13 | -------------------------------------------------------------------------------- /src/Views/blogetc/sitewide/search_form.blade.php: -------------------------------------------------------------------------------- 1 | @if (config('blogetc.search.search_enabled') ) 2 |
3 |
4 |

Search for something in our blog:

5 | 6 | 7 |
8 |
9 | @endif 10 | -------------------------------------------------------------------------------- /src/Views/blogetc/sitewide/show_all_categories.blade.php: -------------------------------------------------------------------------------- 1 |
Post Categories
2 | 10 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/categories/add_category.blade.php: -------------------------------------------------------------------------------- 1 | @extends('blogetc_admin::layouts.admin_layout') 2 | @section('title', 'BlogEtc - Add Category') 3 | @section('content') 4 |
Admin - Add Category
5 | 6 |
7 | @csrf 8 | @include('blogetc_admin::categories.form', ['category' => new \WebDevEtc\BlogEtc\Models\Category()]) 9 | 10 | 11 |
12 | @endsection 13 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/categories/deleted_category.blade.php: -------------------------------------------------------------------------------- 1 | @extends('blogetc_admin::layouts.admin_layout') 2 | @section('title','Category Deleted') 3 | @section('content') 4 |

5 | Category deleted 6 |

7 | @endsection 8 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/categories/edit_category.blade.php: -------------------------------------------------------------------------------- 1 | @extends('blogetc_admin::layouts.admin_layout') 2 | @section('title', 'Edit Category ' . $category->category_name) 3 | @section('content') 4 |
Admin - Edit Category
5 | 6 |
8 | @csrf 9 | @method('patch') 10 | @include('blogetc_admin::categories.form', ['category' => $category]) 11 | 12 | 13 |
14 | @endsection 15 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/categories/form.blade.php: -------------------------------------------------------------------------------- 1 | 24 |
25 | 26 | 27 | category_name)}}" 35 | > 36 | The name of the category 37 |
38 | 39 | 40 |
41 | 42 | slug)}}" 53 | > 54 | 55 | 56 | Letters, numbers, dash only. The slug i.e. {{ route("blogetc.view_category", "") }}/this_part. 57 | This must be unique (two categories can't share the same slug). 58 | 59 |
60 | 61 |
62 | 63 | 66 |
67 | 68 | 71 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/categories/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('blogetc_admin::layouts.admin_layout') 2 | @section('content') 3 | @forelse ($categories as $category) 4 |
5 |
6 |
{{$category->category_name}}
7 | 8 | View Posts in this category 9 | 10 | Edit Category 11 |
id)}}" 15 | class="float-right"> 16 | @csrf 17 | @method('DELETE') 18 | 19 |
20 |
21 |
22 | @empty 23 |
None found, why don't you add one?
24 | @endforelse 25 | 26 |
27 | {{ $categories->links() }} 28 |
29 | @endsection 30 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/comments/index.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \WebDevEtc\BlogEtc\Models\Comment[] $comments */ 3 | @endphp 4 | @extends('blogetc_admin::layouts.admin_layout') 5 | @section('title', 'BlogEtc Manage Comments') 6 | @section('content') 7 | @forelse ($comments as $comment) 8 |
9 |
10 |
11 | {{$comment->author()}} commented on: 12 | 13 | @if($comment->post) 14 | {{$comment->post->title}} 15 | @else 16 | Unknown blog post 17 | @endif 18 | 19 | on {{$comment->created_at}}
20 | 21 |

{{$comment->comment}}

22 | @if($comment->post) 23 | 24 | 25 | View Post 26 | 27 | 28 | 29 | Edit Post 30 | 31 | @endif 32 | 33 | @if(!$comment->approved) 34 | {{--APPROVE BUTTON--}} 35 |
id)}}" 36 | class="float-right"> 37 | @csrf 38 | @method("PATCH") 39 | 40 |
41 | @endif 42 | 43 | {{--DELETE BUTTON--}} 44 |
id)}}" 47 | class="float-right"> 48 | @csrf 49 | @method("DELETE") 50 | 51 |
52 |
53 |
54 | @empty 55 |
None found
56 | @endforelse 57 | 58 |
59 | {{ $comments->links() }} 60 |
61 | @endsection 62 | 63 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/imageupload/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('blogetc_admin::layouts.admin_layout') 2 | @section('title','Blog Etc Admin - Upload Images') 3 | @section('content') 4 |
Admin - Upload Images
5 | 6 |

You can use this to upload images.

7 |
8 | @csrf 9 |
10 | 11 | Image Title 12 | 14 |
15 |
16 | 17 | Upload image 18 | 20 |
21 | 22 |
23 | 24 |
25 | 27 | 28 |
29 | @foreach((array)config('blogetc.image_sizes') as $size => $image_size_details) 30 |
31 | 32 | 34 |
35 | @endforeach 36 |
37 |
38 | 39 | 40 |
41 |
42 | @endsection 43 | 44 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/imageupload/delete-post-image.blade.php: -------------------------------------------------------------------------------- 1 | @extends('blogetc_admin::layouts.admin_layout') 2 | @section('content') 3 |

Are you sure you want to delete the featured image for the selected post?

4 | 5 |
6 | @method('DELETE') 7 | @csrf 8 | 9 |
10 | @endsection 11 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/imageupload/deleted-post-image.blade.php: -------------------------------------------------------------------------------- 1 | @extends('blogetc_admin::layouts.admin_layout') 2 | @section('content') 3 |

Deleted featured images for the selected post.

4 | @endsection 5 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/imageupload/index.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var UploadedPhoto[] $uploaded_photos */ 3 | use WebDevEtc\BlogEtc\Models\UploadedPhoto;$uploadedPhoto = $uploaded_photos 4 | @endphp 5 | @extends('blogetc_admin::layouts.admin_layout') 6 | @section('content') 7 |
Admin - Uploaded Images
8 | 9 |

You can view all previously uploaded images here.

10 | 11 |

It includes one thumbnail per photo - the smallest image is selected.

12 | 13 | 22 | @foreach($uploaded_photos as $uploadedPhoto) 23 | 24 |
25 |

Image ID: {{$uploadedPhoto->id}}: {{$uploadedPhoto->image_title ?? "Untitled Photo"}}

26 |

27 | 28 | Uploaded {{$uploadedPhoto->created_at->diffForHumans()}} 29 |

30 | 31 |
32 |
33 |
34 | uploaded_images as $file_key => $file) { 38 | $id = 'uploaded_' . ($uploadedPhoto->id) . '_' . $file_key; ?> 39 | 40 |
41 |
42 | {{$file_key}} - {{$file['w']}} x {{$file['h']}}: 43 |
44 |

[link] / show 50 |

51 | 52 |
53 |
54 | 61 | 68 | 69 | 78 |
79 |
80 |
81 | @if($smallest) 82 |
83 | 86 | 88 | 89 |
90 | 91 | @else 92 |
93 | No image found 94 |
95 | @endif 96 |
97 |
98 |
99 | @endforeach 100 | 101 |
102 | {{ $uploaded_photos->links() }} 103 |
104 | @endsection 105 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/imageupload/uploaded.blade.php: -------------------------------------------------------------------------------- 1 | @extends('blogetc_admin::layouts.admin_layout') 2 | @section('content') 3 |
Admin - Upload Images
4 | 5 |

Upload was successful.

6 | 7 | @forelse($images as $image) 8 |
9 |

{{ $image['filename'] }}

10 |
11 | {{ $image['w'] . 'x' . $image['h'] }} 12 |
13 | 14 | 15 | 19 | 20 | 22 | "}}"> 24 |
25 | @empty 26 |
27 | No image was processed 28 |
29 | @endforelse 30 | @endsection 31 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends("blogetc_admin::layouts.admin_layout") 2 | @section("content") 3 |
Admin - Manage Blog Posts
4 | 5 | @forelse($posts as $post) 6 |
7 |
8 |
{{ $post->title }}
9 |
{{$post->subtitle}}
10 |

{{$post->html}}

11 | 12 | {!! $post->imageTag('thumbnail', false, 'float-right') !!} 13 | 14 |
15 |
Author
16 |
{{$post->author_string()}}
17 |
Posted at
18 |
{{$post->posted_at}}
19 |
Is published?
20 |
21 | {!!($post->is_published ? "Yes" : 'No')!!} 22 |
23 | 24 |
Categories
25 |
26 | @if(count($post->categories)) 27 | @foreach($post->categories as $category) 28 | 29 | 30 | 31 | {{$category->category_name}} 32 | 33 | @endforeach 34 | @else No Categories 35 | @endif 36 | 37 |
38 |
39 | 40 | 41 | @if($post->use_view_file) 42 |
Uses Custom Viewfile:
43 |
44 | View file:
45 | {{$post->use_view_file}} 46 | @php 47 | $viewfile = resource_path('views/custom_blog_posts/' . $post->use_view_file . '.blade.php'); 48 | @endphp 49 |
50 | Full filename: 51 |
52 | 53 | {{$viewfile}} 54 | 55 | 56 | @if(!file_exists($viewfile)) 57 |
Warning! The custom view file does not exist. Create the 58 | file for this post to display correctly. 59 |
60 | @endif 61 | 62 |
63 | @endif 64 | 65 | 66 | 68 | View Post 69 | 70 | 71 | Edit Post 72 |
74 | @csrf 75 | 76 | 80 |
81 |
82 |
83 | @empty 84 |
No posts to show you. Why don't you add one?
85 | @endforelse 86 | 87 |
88 | {{$posts->appends( [] )->links()}} 89 |
90 | @endsection 91 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/layouts/admin_layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | BlogEtcPost Blog Admin - {{ config('app.name') }} 11 | 12 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | @if(file_exists(public_path('blogetc_admin_css.css'))) 26 | 27 | @else 28 | 29 | {{--Edited your css/app.css file? Uncomment these lines to use plain bootstrap:--}} 30 | {{----}} 31 | {{----}} 32 | @endif 33 | 34 | 35 |
36 | 79 | 80 |
81 | 82 |
83 |
84 |
85 | @include("blogetc_admin::layouts.sidebar") 86 |
87 |
88 | 89 | 90 | @if (isset($errors) && count($errors)) 91 |
92 | Sorry, but there was an error: 93 |
    94 | @foreach($errors->all() as $error) 95 |
  • {{ $error }}
  • 96 | @endforeach 97 |
98 |
99 | @endif 100 | {{--REPLACING THIS FILE WITH YOUR OWN LAYOUT FILE? Don't forget to include the following section!--}} 101 | @if(\WebDevEtc\BlogEtc\Helpers::hasFlashedMessage()) 102 |
103 |

{{\WebDevEtc\BlogEtc\Helpers::pullFlashedMessage() }}

104 |
105 | @endif 106 | 107 | @yield('content') 108 |
109 |
110 |
111 |
112 |
113 | 114 |
115 | Laravel Blog Package provided by Webdevetc 116 |
117 | 118 | 119 | @if( config("blogetc.use_wysiwyg") && config("blogetc.echo_html") && (in_array( Request::route()->getName() ,[ 'blogetc.admin.create_post' , 'blogetc.admin.edit_post' ]))) 120 | 121 | 122 | 127 | @endif 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/layouts/sidebar.blade.php: -------------------------------------------------------------------------------- 1 |

WebDevEtc.com BlogEtc Admin Panel

2 |

Welcome to the admin panel for your blog posts.

3 | 4 | 117 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/posts/add_post.blade.php: -------------------------------------------------------------------------------- 1 | @extends('blogetc_admin::layouts.admin_layout') 2 | @section('content') 3 |
Admin - Add post
4 | 5 |
6 | @csrf 7 | @include("blogetc_admin::posts.form", ['post' => new \WebDevEtc\BlogEtc\Models\Post()]) 8 | 9 |
10 | @endsection 11 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/posts/deleted_post.blade.php: -------------------------------------------------------------------------------- 1 | @extends("blogetc_admin::layouts.admin_layout") 2 | @section("content") 3 |
4 | Deleted that post 5 |
6 | 7 | Back to posts overview 8 | 9 |
10 | 11 | $image_size_info) { 14 | if (!$deletedPost->$image_size) { 15 | continue; 16 | } 17 | $images_to_delete[] = $image_size; 18 | }?> 19 | 20 | @if(count($images_to_delete)) 21 |

However, the following images were not deleted:

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | @foreach($images_to_delete as $image_size) 33 | 34 | 44 | 50 | 53 | 54 | @endforeach 55 | 56 |
Image/linkFilename / filesizeFull location
35 | $image_size) }}" 36 | target="_blank" class="btn btn-primary m-1"> 37 | view 38 | 39 | 40 | Uploaded image$image_size) }}" 41 | width="100" /> 42 | 43 | {{$deletedPost->$image_size}} 45 | {{--check filesize returns something, so we don't divide by 0--}} 46 | @if(filesize(public_path(config("blogetc.blog_upload_dir","blog_images")."/".$deletedPost->$image_size))) 47 | ({{ (round(filesize(public_path(config("blogetc.blog_upload_dir","blog_images")."/".$deletedPost->$image_size)) / 1000 ,1)). " kb"}}) 48 | @endif 49 | 51 | {{ public_path(config("blogetc.blog_upload_dir","blog_images")."/".$deletedPost->$image_size) }} 52 |
57 |

58 | Please manually remove those files from the filesystem if desired. 59 |

60 | @endif 61 | @endsection 62 | 63 | -------------------------------------------------------------------------------- /src/Views/blogetc_admin/posts/edit_post.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \WebDevEtc\BlogEtc\Models\Post $post */ 3 | @endphp 4 | @extends('blogetc_admin::layouts.admin_layout') 5 | @section('content') 6 |
Admin - Editing post 7 | 8 | View post 9 | 10 |
11 | 12 |
13 | @csrf 14 | @method('patch') 15 | @include('blogetc_admin::posts.form', ['post' => $post]) 16 | 17 |
18 | @endsection 19 | -------------------------------------------------------------------------------- /src/routes.php: -------------------------------------------------------------------------------- 1 | ['web'], 'namespace' => '\WebDevEtc\BlogEtc\Controllers'], static function () { 4 | /* The main public facing blog routes - show all posts, view a category, rss feed, view a single post, also the add comment route */ 5 | Route::group(['prefix' => config('blogetc.blog_prefix', 'blog')], static function () { 6 | Route::get('/', 'PostsController@index')->name('blogetc.index'); 7 | Route::get('/search', 'PostsController@search')->name('blogetc.search'); 8 | Route::get('/feed', 'BlogEtcRssFeedController@feed')->name('blogetc.feed'); 9 | Route::get('/category/{categorySlug}', 'PostsController@showCategory')->name('blogetc.view_category'); 10 | Route::get('/{blogPostSlug}', 'PostsController@show')->name('blogetc.single'); 11 | 12 | Route::group(['middleware' => 'throttle:10,3'], static function () { 13 | Route::post('save_comment/{blogPostSlug}', 'CommentsController@store')->name('blogetc.comments.add_new_comment'); 14 | }); 15 | }); 16 | 17 | /* Admin backend routes - CRUD for posts, categories, and approving/deleting submitted comments */ 18 | Route::group(['prefix' => config('blogetc.admin_prefix', 'blog_admin')], static function () { 19 | Route::get('/', 'Admin\ManagePostsController@index')->name('blogetc.admin.index'); 20 | 21 | Route::get('/add_post', 'Admin\ManagePostsController@create')->name('blogetc.admin.create_post'); 22 | Route::post('/add_post', 'Admin\ManagePostsController@store')->name('blogetc.admin.store_post'); 23 | 24 | Route::get('/edit_post/{blogPostId}', 'Admin\ManagePostsController@edit')->name('blogetc.admin.edit_post'); 25 | Route::patch('/edit_post/{blogPostId}', 'Admin\ManagePostsController@update')->name('blogetc.admin.update_post'); 26 | 27 | Route::group(['prefix' => 'image_uploads'], static function () { 28 | Route::get('/', 'Admin\ManageUploadsController@index')->name('blogetc.admin.images.all'); 29 | 30 | Route::get('/upload', 'Admin\ManageUploadsController@create')->name('blogetc.admin.images.upload'); 31 | Route::post('/upload', 'Admin\ManageUploadsController@store')->name('blogetc.admin.images.store'); 32 | 33 | Route::get('/post/{postId}/delete-images', 'Admin\ManageUploadsController@deletePostImage')->name('blogetc.admin.images.delete-post-image'); 34 | Route::delete('/post/{postId}/delete-images', 'Admin\ManageUploadsController@deletePostImageConfirmed')->name('blogetc.admin.images.delete-post-image-confirmed'); 35 | }); 36 | 37 | Route::delete('/delete_post/{blogPostId}', 'Admin\ManagePostsController@destroy')->name('blogetc.admin.destroy_post'); 38 | 39 | Route::group(['prefix' => 'comments'], static function () { 40 | Route::get('/', 'Admin\ManageCommentsController@index')->name('blogetc.admin.comments.index'); 41 | Route::patch('/{commentId}', 'Admin\ManageCommentsController@approve')->name('blogetc.admin.comments.approve'); 42 | 43 | Route::delete('/{commentId}', 'Admin\ManageCommentsController@destroy')->name('blogetc.admin.comments.delete'); 44 | }); 45 | 46 | Route::group(['prefix' => 'categories'], static function () { 47 | Route::get('/', 'Admin\ManageCategoriesController@index')->name('blogetc.admin.categories.index'); 48 | 49 | Route::get('/add_category', 'Admin\ManageCategoriesController@create')->name('blogetc.admin.categories.create_category'); 50 | Route::post('/add_category', 'Admin\ManageCategoriesController@store')->name('blogetc.admin.categories.store_category'); 51 | 52 | Route::get('/edit_category/{categoryId}', 'Admin\ManageCategoriesController@edit')->name('blogetc.admin.categories.edit_category'); 53 | Route::patch('/edit_category/{categoryId}', 'Admin\ManageCategoriesController@update')->name('blogetc.admin.categories.update_category'); 54 | 55 | Route::delete('/delete_category/{categoryId}', 'Admin\ManageCategoriesController@destroy')->name('blogetc.admin.categories.destroy_category'); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/Feature/Controllers/Admin/ManageCommentsControllerTest.php: -------------------------------------------------------------------------------- 1 | featureSetUp(); 17 | } 18 | 19 | public function testNonLoggedInUserForbidden(): void 20 | { 21 | $response = $this->get(route('blogetc.admin.comments.index')); 22 | $response->assertUnauthorized(); 23 | } 24 | 25 | public function testNonLoggedInUserForbiddenWithGate(): void 26 | { 27 | $this->setAdminGate(); 28 | $response = $this->get(route('blogetc.admin.comments.index')); 29 | $response->assertUnauthorized(); 30 | } 31 | 32 | public function testGatedAdminUserCanViewIndex(): void 33 | { 34 | $this->beAdminUserWithGate(); 35 | $response = $this->get(route('blogetc.admin.comments.index')); 36 | $response->assertOk(); 37 | } 38 | 39 | public function testGatedNonAdminUserCannotViewIndex(): void 40 | { 41 | $this->beNonAdminUserWithGate(); 42 | $response = $this->get(route('blogetc.admin.comments.index')); 43 | $response->assertUnauthorized(); 44 | } 45 | 46 | public function testLegacyAdminUserCanViewIndex(): void 47 | { 48 | $this->beLegacyAdminUser(); 49 | $response = $this->get(route('blogetc.admin.comments.index')); 50 | $response->assertOk(); 51 | } 52 | 53 | public function testLegacyNonAdminUserCannotViewIndex(): void 54 | { 55 | $this->beLegacyNonAdminUser(); 56 | $response = $this->get(route('blogetc.admin.comments.index')); 57 | $response->assertUnauthorized(); 58 | } 59 | 60 | public function testCommentsIndex(): void 61 | { 62 | $comment = factory(Comment::class)->create(); 63 | 64 | $this->beLegacyAdminUser(); 65 | $response = $this->get(route('blogetc.admin.comments.index')); 66 | 67 | $response->assertSee($comment->comment); 68 | } 69 | 70 | public function testApproveComment(): void 71 | { 72 | $this->beAdminUserWithGate(); 73 | $comment = factory(Comment::class)->create(['approved'=> false]); 74 | 75 | $response = $this->patch(route('blogetc.admin.comments.approve', $comment->id)); 76 | 77 | $response->assertSessionHasNoErrors()->assertRedirect(); 78 | $this->assertDatabaseHas('blog_etc_comments', ['id' => $comment->id, 'approved' => true]); 79 | } 80 | 81 | public function testApproveNonExistingComment(): void 82 | { 83 | $this->beAdminUserWithGate(); 84 | $response = $this->patch(route('blogetc.admin.comments.approve', 0)); 85 | $response->assertNotFound(); 86 | } 87 | 88 | public function testDenyComment(): void 89 | { 90 | $this->beAdminUserWithGate(); 91 | $response = $this->delete(route('blogetc.admin.comments.delete', 0)); 92 | $response->assertNotFound(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Feature/Controllers/Admin/ManageUploadsControllerTest.php: -------------------------------------------------------------------------------- 1 | featureSetUp(); 16 | } 17 | 18 | public function testNonLoggedInUserForbidden(): void 19 | { 20 | $response = $this->get(route('blogetc.admin.images.all')); 21 | $response->assertUnauthorized(); 22 | } 23 | 24 | public function testNonLoggedInUserForbiddenWithGate(): void 25 | { 26 | $this->setAdminGate(); 27 | $response = $this->get(route('blogetc.admin.images.all')); 28 | $response->assertUnauthorized(); 29 | } 30 | 31 | public function testGatedAdminUserCanViewIndex(): void 32 | { 33 | $this->withoutExceptionHandling(); 34 | $this->beAdminUserWithGate(); 35 | $response = $this->get(route('blogetc.admin.images.all')); 36 | $response->assertOk(); 37 | } 38 | 39 | public function testGatedNonAdminUserCannotViewIndex(): void 40 | { 41 | $this->beNonAdminUserWithGate(); 42 | $response = $this->get(route('blogetc.admin.images.all')); 43 | $response->assertUnauthorized(); 44 | } 45 | 46 | public function testLegacyAdminUserCanViewIndex(): void 47 | { 48 | $this->beLegacyAdminUser(); 49 | $response = $this->get(route('blogetc.admin.images.all')); 50 | $response->assertOk(); 51 | } 52 | 53 | public function testLegacyNonAdminUserCannotViewIndex(): void 54 | { 55 | $this->beLegacyNonAdminUser(); 56 | $response = $this->get(route('blogetc.admin.images.all')); 57 | $response->assertUnauthorized(); 58 | } 59 | 60 | // TODO test upload form & storing upload. 61 | } 62 | -------------------------------------------------------------------------------- /tests/Feature/Controllers/CommentsControllerTest.php: -------------------------------------------------------------------------------- 1 | withoutExceptionHandling(); 29 | $post = factory(Post::class)->create(); 30 | $this->beLegacyAdminUser(); 31 | 32 | $url = route('blogetc.comments.add_new_comment', $post->slug); 33 | 34 | $params = [ 35 | 'comment' => $this->faker->sentence, 36 | 'author_name' => $this->faker->name, 37 | 'author_email' => $this->faker->safeEmail, 38 | 'author_website' => 'http://'.$this->faker->safeEmailDomain, 39 | ]; 40 | 41 | $response = $this->postJson($url, $params); 42 | 43 | $this->assertSame(Response::HTTP_CREATED, $response->getStatusCode()); 44 | 45 | $postResponse = $this->get(route('blogetc.single', $post->slug)); 46 | $postResponse->assertSee($params['comment']); 47 | } 48 | 49 | /** 50 | * Test the store method for saving a new comment. 51 | */ 52 | public function testDisabledCommentsStore(): void 53 | { 54 | config(['blogetc.comments.type_of_comments_to_show' => 'disabled']); 55 | 56 | $post = factory(Post::class)->create(); 57 | 58 | $url = route('blogetc.comments.add_new_comment', $post->slug); 59 | 60 | $params = [ 61 | 'comment' => $this->faker->sentence, 62 | 'author_name' => $this->faker->name, 63 | 'author_email' => $this->faker->safeEmail, 64 | 'author_website' => 'http://'.$this->faker->safeEmailDomain, 65 | ]; 66 | 67 | $response = $this->postJson($url, $params); 68 | 69 | $response->assertForbidden(); 70 | 71 | $this->assertDatabaseMissing('blog_etc_comments', ['comment' => $params['comment']]); 72 | } 73 | 74 | /** 75 | * Setup the feature test. 76 | */ 77 | protected function setUp(): void 78 | { 79 | parent::setUp(); 80 | 81 | $this->featureSetUp(); 82 | 83 | config(['blogetc.comments.type_of_comments_to_show' => 'built_in']); 84 | config(['blogetc.comments.auto_approve_comments' => true]); 85 | config(['blogetc.captcha.captcha_enabled' => false]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Feature/Controllers/FeedControllerTest.php: -------------------------------------------------------------------------------- 1 | get(route('blogetc.feed')); 20 | 21 | $response->assertOk() 22 | ->assertHeader('content-type', 'application/atom+xml; charset=utf-8'); 23 | } 24 | 25 | /** 26 | * Test the feed includes a recent post. 27 | */ 28 | public function testIncludesRecentPost(): void 29 | { 30 | $post = factory(Post::class)->create(); 31 | 32 | $response = $this->get(route('blogetc.feed')); 33 | 34 | $response->assertOk() 35 | ->assertSee($post->title); 36 | } 37 | 38 | /** 39 | * Test the feed does not include posts not published. 40 | */ 41 | public function testExcludesUnpublishedPosts(): void 42 | { 43 | $post = factory(Post::class)->state('not_published')->create(); 44 | 45 | $response = $this->get(route('blogetc.feed')); 46 | 47 | $response->assertOk() 48 | ->assertDontSee($post->title); 49 | } 50 | 51 | /** 52 | * Test the feed does not include posts not published. 53 | */ 54 | public function testExcludesFuturePosts(): void 55 | { 56 | $post = factory(Post::class)->state('in_future')->create(); 57 | 58 | $response = $this->get(route('blogetc.feed')); 59 | 60 | $response->assertOk() 61 | ->assertDontSee($post->title); 62 | } 63 | 64 | /** 65 | * Test works for logged in users. 66 | */ 67 | public function testLoggedIn(): void 68 | { 69 | $this->beLegacyNonAdminUser(); 70 | 71 | $response = $this->get(route('blogetc.feed')); 72 | 73 | $response->assertOk() 74 | ->assertHeader('content-type', 'application/atom+xml; charset=utf-8'); 75 | } 76 | 77 | /** 78 | * Test that logged in users which pass the blog-etc-admin gate can see unpublished posts. 79 | */ 80 | public function testLoggedInCanSeeUnpublishedPosts(): void 81 | { 82 | $this->beLegacyAdminUser(); 83 | 84 | $post = factory(Post::class)->state('not_published')->create(); 85 | 86 | $response = $this->get(route('blogetc.feed')); 87 | 88 | $response->assertOk()->assertSee($post->title); 89 | } 90 | 91 | /** 92 | * Test that logged in users which pass the blog-etc-admin gate can see unpublished posts. 93 | */ 94 | public function testLoggedInCanSeeFuturePosts(): void 95 | { 96 | $this->beLegacyAdminUser(); 97 | 98 | $post = factory(Post::class)->state('in_future')->create(); 99 | 100 | $response = $this->get(route('blogetc.feed')); 101 | 102 | $response->assertOk() 103 | ->assertSee($post->title); 104 | } 105 | 106 | /** 107 | * RSS is cached. 108 | * If viewing it with an admin user then a guest user, the guest user should not see the cached admin results. 109 | */ 110 | public function testLoggedInCacheDoesNotShowToNonLoggedInUsers(): void 111 | { 112 | $this->beLegacyAdminUser(); 113 | 114 | $post = factory(Post::class)->state('not_published')->create(); 115 | 116 | $adminResponse = $this->get(route('blogetc.feed')); 117 | 118 | $adminResponse->assertOk() 119 | ->assertSee($post->title); 120 | 121 | Auth::logout(); 122 | 123 | $guestResponse = $this->get(route('blogetc.feed')); 124 | $guestResponse->assertOk() 125 | ->assertDontSee($post->title); 126 | } 127 | 128 | /** 129 | * Setup the feature test. 130 | */ 131 | protected function setUp(): void 132 | { 133 | parent::setUp(); 134 | 135 | $this->featureSetUp(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/Feature/Repositories/CategoriesRepositoryTest.php: -------------------------------------------------------------------------------- 1 | featureSetUp(); 26 | Category::truncate(); 27 | 28 | $this->categoriesRepository = resolve(CategoriesRepository::class); 29 | } 30 | 31 | public function testIndexPaginated() 32 | { 33 | factory(Category::class, 30)->create(); 34 | 35 | $response = $this->categoriesRepository->indexPaginated(25); 36 | 37 | $this->assertSame(30, $response->total()); 38 | $this->assertSame(2, $response->lastPage()); 39 | } 40 | 41 | public function testFind() 42 | { 43 | $category = factory(Category::class)->create(); 44 | $response = $this->categoriesRepository->find($category->id); 45 | $this->assertTrue($category->is($response)); 46 | } 47 | 48 | public function testFindNonExisting() 49 | { 50 | $this->expectException(CategoryNotFoundException::class); 51 | 52 | $this->categoriesRepository->find(0); 53 | } 54 | 55 | public function testFindBySlug() 56 | { 57 | $category = factory(Category::class)->create(); 58 | $response = $this->categoriesRepository->findBySlug($category->slug); 59 | $this->assertTrue($category->is($response)); 60 | } 61 | 62 | public function testFindBySlugNonExisting() 63 | { 64 | $this->expectException(CategoryNotFoundException::class); 65 | 66 | $this->categoriesRepository->findBySlug('non-existing'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Feature/Repositories/CommentsRepositoryTest.php: -------------------------------------------------------------------------------- 1 | featureSetUp(); 26 | Comment::truncate(); 27 | 28 | $this->commentsRepository = resolve(CommentsRepository::class); 29 | } 30 | 31 | public function testApprove() 32 | { 33 | $comment = factory(Comment::class)->create(['approved' => false]); 34 | 35 | $this->commentsRepository->approve($comment->id); 36 | 37 | $comment->refresh(); 38 | 39 | $this->assertTrue($comment->fresh()->approved); 40 | } 41 | 42 | /** 43 | * Approving an already approved comment should still work. 44 | */ 45 | public function testApproveAlreadyApproved() 46 | { 47 | $comment = factory(Comment::class)->create(['approved' => true]); 48 | $this->commentsRepository->approve($comment->id); 49 | $this->assertTrue($comment->fresh()->approved); 50 | } 51 | 52 | public function testApproveNonExistingComment() 53 | { 54 | $this->expectException(CommentNotFoundException::class); 55 | $this->commentsRepository->approve(0); 56 | } 57 | 58 | public function testFind() 59 | { 60 | $comment = factory(Comment::class)->create(); 61 | 62 | $response = $this->commentsRepository->find($comment->id); 63 | 64 | $this->assertTrue($comment->is($response)); 65 | } 66 | 67 | public function testFindApproved() 68 | { 69 | $comment = factory(Comment::class)->create(['approved' => true]); 70 | $response = $this->commentsRepository->find($comment->id, true); 71 | $this->assertTrue($comment->is($response)); 72 | } 73 | 74 | public function testFindNonApproved() 75 | { 76 | $comment = factory(Comment::class)->create(['approved' => false]); 77 | $this->expectException(CommentNotFoundException::class); 78 | $this->commentsRepository->find($comment->id, true); 79 | } 80 | 81 | public function testFindNonExisting() 82 | { 83 | $this->expectException(CommentNotFoundException::class); 84 | $response = $this->commentsRepository->find(0); 85 | } 86 | 87 | public function testCreate() 88 | { 89 | $post = factory(Post::class)->create(); 90 | 91 | $commentText = $this->faker->sentence; 92 | $this->commentsRepository->create($post, ['comment' => $commentText], '127.0.0.1', null, null, null, false); 93 | 94 | $this->assertDatabaseHas('blog_etc_comments', ['comment' => $commentText, 'blog_etc_post_id' => $post->id, 'approved' => false]); 95 | } 96 | 97 | public function testCreateAutoApproved() 98 | { 99 | $post = factory(Post::class)->create(); 100 | 101 | $commentText = $this->faker->sentence; 102 | $this->commentsRepository->create($post, ['comment' => $commentText], '127.0.0.1', null, null, null, true); 103 | 104 | $this->assertDatabaseHas('blog_etc_comments', ['comment' => $commentText, 'approved' => true]); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Feature/Services/CaptchaServiceTest.php: -------------------------------------------------------------------------------- 1 | featureSetUp(); 23 | 24 | $this->captchaService = resolve(CaptchaService::class); 25 | } 26 | 27 | public function testGetCaptchaObjectDisabled(): void 28 | { 29 | Config::set('blogetc.captcha.captcha_enabled', false); 30 | 31 | $result = $this->captchaService->getCaptchaObject(); 32 | 33 | $this->assertNull($result); 34 | } 35 | 36 | public function testGetCaptchaObjectEnabled(): void 37 | { 38 | Config::set('blogetc.captcha.captcha_enabled', true); 39 | Config::set('blogetc.captcha.captcha_type', Basic::class); 40 | 41 | $result = $this->captchaService->getCaptchaObject(); 42 | 43 | $this->assertInstanceOf(Basic::class, $result); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Feature/Services/CategoriesServiceTest.php: -------------------------------------------------------------------------------- 1 | featureSetUp(); 23 | 24 | $this->categoriesService = resolve(CategoriesService::class); 25 | 26 | Category::truncate(); 27 | } 28 | 29 | public function testIndexPaginated(): void 30 | { 31 | factory(Category::class, 25)->create(); 32 | $result = $this->categoriesService->indexPaginated(10); 33 | 34 | $this->assertSame(25, $result->total()); 35 | $this->assertSame(3, $result->lastPage()); 36 | } 37 | 38 | public function testFindBySlug(): void 39 | { 40 | $category = factory(Category::class)->create(); 41 | 42 | $result = $this->categoriesService->findBySlug($category->slug); 43 | 44 | $this->assertTrue($category->is($result)); 45 | } 46 | 47 | public function testFindBySlugNotFound(): void 48 | { 49 | $this->expectException(CategoryNotFoundException::class); 50 | $this->categoriesService->findBySlug('not-found'); 51 | } 52 | 53 | public function testCreate(): void 54 | { 55 | $attributes = factory(Category::class)->make()->toArray(); 56 | 57 | $result = $this->categoriesService->create($attributes); 58 | 59 | $this->assertInstanceOf(Category::class, $result); 60 | 61 | $this->assertDatabaseHas('blog_etc_categories', $attributes); 62 | } 63 | 64 | public function testUpdate(): void 65 | { 66 | $category = factory(Category::class)->create(); 67 | 68 | $updatedCategory = $this->categoriesService->update($category->id, ['category_name' => 'updated']); 69 | 70 | $this->assertSame('updated', $updatedCategory->category_name); 71 | 72 | $this->assertDatabaseHas('blog_etc_categories', ['id' => $category->id, 'category_name' => 'updated']); 73 | } 74 | 75 | public function testUpdateNotFound(): void 76 | { 77 | $this->expectException(CategoryNotFoundException::class); 78 | 79 | $this->categoriesService->update(0, ['category_name' => 'updated']); 80 | } 81 | 82 | public function testFind(): void 83 | { 84 | $category = factory(Category::class)->create(); 85 | 86 | $result = $this->categoriesService->find($category->id); 87 | 88 | $this->assertTrue($category->is($result)); 89 | } 90 | 91 | public function testFindNotFound(): void 92 | { 93 | $this->expectException(CategoryNotFoundException::class); 94 | $this->categoriesService->find(0); 95 | } 96 | 97 | public function testDelete(): void 98 | { 99 | $category = factory(Category::class)->create(); 100 | 101 | $this->categoriesService->delete($category->id); 102 | 103 | $this->assertDatabaseMissing('blog_etc_categories', ['id' => $category->id]); 104 | } 105 | 106 | public function testDeleteNotFound(): void 107 | { 108 | $this->expectException(CategoryNotFoundException::class); 109 | 110 | $this->categoriesService->delete(0); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/Feature/Services/CommentsServiceTest.php: -------------------------------------------------------------------------------- 1 | featureSetUp(); 24 | 25 | $this->commentsService = resolve(CommentsService::class); 26 | Comment::truncate(); 27 | } 28 | 29 | public function testApprove() 30 | { 31 | $comment = factory(Comment::class)->create(['approved' => false]); 32 | 33 | $this->commentsService->approve($comment->id); 34 | 35 | $comment->refresh(); 36 | 37 | $this->assertTrue($comment->fresh()->approved); 38 | } 39 | 40 | /** 41 | * Approving an already approved comment should still work. 42 | */ 43 | public function testApproveAlreadyApproved() 44 | { 45 | $comment = factory(Comment::class)->create(['approved' => true]); 46 | $this->commentsService->approve($comment->id); 47 | $this->assertTrue($comment->fresh()->approved); 48 | } 49 | 50 | public function testApproveNonExistingComment() 51 | { 52 | $this->expectException(CommentNotFoundException::class); 53 | $this->commentsService->approve(0); 54 | } 55 | 56 | public function testFind() 57 | { 58 | $comment = factory(Comment::class)->create(); 59 | 60 | $response = $this->commentsService->find($comment->id); 61 | 62 | $this->assertTrue($comment->is($response)); 63 | } 64 | 65 | public function testFindApproved() 66 | { 67 | $comment = factory(Comment::class)->create(['approved' => true]); 68 | $response = $this->commentsService->find($comment->id, true); 69 | $this->assertTrue($comment->is($response)); 70 | } 71 | 72 | public function testFindNonApproved() 73 | { 74 | $comment = factory(Comment::class)->create(['approved' => false]); 75 | $this->expectException(CommentNotFoundException::class); 76 | $this->commentsService->find($comment->id, true); 77 | } 78 | 79 | public function testFindNonExisting() 80 | { 81 | $this->expectException(CommentNotFoundException::class); 82 | $this->commentsService->find(0); 83 | } 84 | 85 | public function testCreate() 86 | { 87 | $post = factory(Post::class)->create(); 88 | 89 | $commentText = $this->faker->sentence; 90 | $this->commentsService->create($post, ['comment' => $commentText], '127.0.0.1', null); 91 | 92 | $this->assertDatabaseHas('blog_etc_comments', ['comment' => $commentText, 'blog_etc_post_id' => $post->id, 'approved' => false]); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Feature/Services/FeedServiceTest.php: -------------------------------------------------------------------------------- 1 | create(); 24 | 25 | $response = $this->feedService->getFeed($feed, 'rss'); 26 | 27 | $this->assertInstanceOf(Response::class, $response); 28 | } 29 | 30 | /** 31 | * Setup the feature test. 32 | */ 33 | protected function setUp(): void 34 | { 35 | parent::setUp(); 36 | 37 | $this->featureSetUp(); 38 | Post::truncate(); 39 | 40 | $this->feedService = resolve(FeedService::class); 41 | } 42 | 43 | // Todo: test content, test logged in vs logged out, test cache, test empty posts 44 | } 45 | -------------------------------------------------------------------------------- /tests/Feature/Services/PostsServiceTest.php: -------------------------------------------------------------------------------- 1 | featureSetUp(); 25 | 26 | $this->postsService = resolve(PostsService::class); 27 | Post::truncate(); 28 | } 29 | 30 | public function testIndexPaginated() 31 | { 32 | factory(Post::class, 25)->create(); 33 | 34 | $response = $this->postsService->indexPaginated(10, null); 35 | 36 | $this->assertSame(25, $response->total()); 37 | $this->assertSame(3, $response->lastPage()); 38 | } 39 | 40 | public function testIndexPaginatedUnpublished() 41 | { 42 | factory(Post::class)->create(['is_published' => false]); 43 | factory(Post::class)->create(['posted_at' => Carbon::now()->addHour()]); 44 | 45 | $response = $this->postsService->indexPaginated(10, null); 46 | 47 | $this->assertSame(0, $response->total()); 48 | } 49 | 50 | public function testIndexPaginatedCategoryWithNoPosts() 51 | { 52 | $category = factory(Category::class)->create(); 53 | 54 | factory(Post::class, 25)->create(); 55 | 56 | $response = $this->postsService->indexPaginated(10, $category->id); 57 | 58 | $this->assertSame(0, $response->total()); 59 | } 60 | 61 | public function testRssItems() 62 | { 63 | factory(Post::class, 11)->create(); 64 | 65 | $response = $this->postsService->rssItems(); 66 | 67 | $this->assertCount(10, $response); 68 | } 69 | 70 | public function testRssItemsDoesNotIncludeUnpublished() 71 | { 72 | factory(Post::class)->create(['is_published' => false]); 73 | factory(Post::class)->create(['posted_at' => Carbon::now()->addHour()]); 74 | 75 | $response = $this->postsService->rssItems(); 76 | 77 | $this->assertEmpty($response); 78 | } 79 | 80 | public function testSearch(): void 81 | { 82 | [$post1, $post2] = factory(Post::class, 2)->create(['title' => 'an example title']); 83 | 84 | $response = $this->postsService->search($post1->title); 85 | 86 | $this->assertCount(2, $response); 87 | 88 | $this->assertTrue($response[0]->is($post1)); 89 | } 90 | 91 | public function testSearchDoesNotIncludeUnpublished(): void 92 | { 93 | factory(Post::class)->create(['title' => 'test-unpublished', 'is_published' => false]); 94 | factory(Post::class)->create(['title' => 'test-unpublished', 'posted_at' => Carbon::now()->addDay()]); 95 | $published = factory(Post::class)->create(['title' => 'test-unpublished']); 96 | 97 | $response = $this->postsService->search('test-unpublished'); 98 | 99 | $this->assertCount(1, $response); 100 | 101 | $this->assertTrue($response[0]->is($published)); 102 | } 103 | 104 | public function testFindBySlug(): void 105 | { 106 | $post = factory(Post::class)->create(); 107 | 108 | $response = $this->postsService->findBySlug($post->slug); 109 | 110 | $this->assertTrue($post->is($response)); 111 | } 112 | 113 | public function testFindBySlugFails(): void 114 | { 115 | $this->expectException(PostNotFoundException::class); 116 | $this->postsService->findBySlug('invalid'); 117 | } 118 | 119 | public function testFindBySlugFailsWhenEmpty(): void 120 | { 121 | $this->expectException(PostNotFoundException::class); 122 | $this->postsService->findBySlug(''); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 |

Test layout app - in case main layouts.app does not exist

2 | 3 | @yield('content') 4 | --------------------------------------------------------------------------------