]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'patching-v0.27'
authorDan Brown <redacted>
Wed, 16 Oct 2019 15:37:29 +0000 (16:37 +0100)
committerDan Brown <redacted>
Wed, 16 Oct 2019 15:37:29 +0000 (16:37 +0100)
323 files changed:
.env.example.complete
.github/workflows/phpunit.yml [new file with mode: 0644]
.gitignore
.travis.yml [deleted file]
app/Actions/Activity.php
app/Actions/ActivityService.php
app/Actions/ViewService.php
app/Application.php
app/Auth/Access/EmailConfirmationService.php
app/Auth/Access/SocialAuthService.php
app/Auth/Access/UserInviteService.php
app/Auth/Access/UserTokenService.php
app/Auth/Permissions/PermissionService.php
app/Auth/Permissions/PermissionsRepo.php
app/Auth/Role.php
app/Auth/User.php
app/Auth/UserRepo.php
app/Config/app.php
app/Config/auth.php
app/Config/broadcasting.php
app/Config/cache.php
app/Config/database.php
app/Config/debugbar.php
app/Config/dompdf.php
app/Config/hashing.php [new file with mode: 0644]
app/Config/logging.php [new file with mode: 0644]
app/Config/mail.php
app/Config/queue.php
app/Config/services.php
app/Config/session.php
app/Config/setting-defaults.php
app/Entities/Book.php
app/Entities/BookChild.php [new file with mode: 0644]
app/Entities/Bookshelf.php
app/Entities/BreadcrumbsViewComposer.php
app/Entities/Chapter.php
app/Entities/Entity.php
app/Entities/EntityProvider.php
app/Entities/ExportService.php
app/Entities/HasCoverImage.php [new file with mode: 0644]
app/Entities/Managers/BookContents.php [new file with mode: 0644]
app/Entities/Managers/EntityContext.php [moved from app/Entities/EntityContextManager.php with 53% similarity]
app/Entities/Managers/PageContent.php [new file with mode: 0644]
app/Entities/Managers/PageEditActivity.php [new file with mode: 0644]
app/Entities/Managers/TrashCan.php [new file with mode: 0644]
app/Entities/Page.php
app/Entities/PageRevision.php
app/Entities/Repos/BaseRepo.php [new file with mode: 0644]
app/Entities/Repos/BookRepo.php [new file with mode: 0644]
app/Entities/Repos/BookshelfRepo.php [new file with mode: 0644]
app/Entities/Repos/ChapterRepo.php [new file with mode: 0644]
app/Entities/Repos/EntityRepo.php [deleted file]
app/Entities/Repos/PageRepo.php
app/Entities/SearchService.php
app/Entities/SlugGenerator.php [new file with mode: 0644]
app/Exceptions/MoveOperationException.php [new file with mode: 0644]
app/Exceptions/SortOperationException.php [new file with mode: 0644]
app/Exceptions/UserTokenExpiredException.php
app/Exceptions/UserTokenNotFoundException.php
app/Facades/Permissions.php [new file with mode: 0644]
app/Http/Controllers/AttachmentController.php
app/Http/Controllers/Auth/ConfirmEmailController.php
app/Http/Controllers/Auth/ForgotPasswordController.php
app/Http/Controllers/Auth/RegisterController.php
app/Http/Controllers/Auth/ResetPasswordController.php
app/Http/Controllers/Auth/UserInviteController.php
app/Http/Controllers/BookController.php
app/Http/Controllers/BookExportController.php [new file with mode: 0644]
app/Http/Controllers/BookSortController.php [new file with mode: 0644]
app/Http/Controllers/BookshelfController.php
app/Http/Controllers/ChapterController.php
app/Http/Controllers/ChapterExportController.php [new file with mode: 0644]
app/Http/Controllers/CommentController.php
app/Http/Controllers/Controller.php
app/Http/Controllers/HomeController.php
app/Http/Controllers/Images/ImageController.php
app/Http/Controllers/PageController.php
app/Http/Controllers/PageExportController.php [new file with mode: 0644]
app/Http/Controllers/PageRevisionController.php [new file with mode: 0644]
app/Http/Controllers/PageTemplateController.php
app/Http/Controllers/PermissionController.php
app/Http/Controllers/SearchController.php
app/Http/Controllers/SettingController.php
app/Http/Controllers/UserController.php
app/Http/Kernel.php
app/Http/Middleware/CheckForMaintenanceMode.php [new file with mode: 0644]
app/Http/Middleware/GlobalViewData.php [new file with mode: 0644]
app/Http/Middleware/TrustProxies.php
app/Http/Middleware/VerifyCsrfToken.php
app/Http/Request.php
app/Providers/AppServiceProvider.php
app/Providers/CustomFacadeProvider.php
app/Uploads/Attachment.php
app/Uploads/AttachmentService.php
app/Uploads/ImageService.php
app/helpers.php
bootstrap/app.php
composer.json
composer.lock
database/factories/ModelFactory.php
database/seeds/DatabaseSeeder.php
database/seeds/DummyContentSeeder.php
database/seeds/LargeContentSeeder.php
dev/docker/Dockerfile
package-lock.json
phpunit.xml
public/web.config [new file with mode: 0644]
readme.md
resources/icons/add-circle.svg [moved from resources/assets/icons/add-circle.svg with 100% similarity]
resources/icons/add.svg [moved from resources/assets/icons/add.svg with 100% similarity]
resources/icons/attach.svg [moved from resources/assets/icons/attach.svg with 100% similarity]
resources/icons/auth/azure.svg [moved from resources/assets/icons/auth/azure.svg with 100% similarity]
resources/icons/auth/discord.svg [moved from resources/assets/icons/auth/discord.svg with 100% similarity]
resources/icons/auth/facebook.svg [moved from resources/assets/icons/auth/facebook.svg with 100% similarity]
resources/icons/auth/github.svg [moved from resources/assets/icons/auth/github.svg with 100% similarity]
resources/icons/auth/gitlab.svg [moved from resources/assets/icons/auth/gitlab.svg with 100% similarity]
resources/icons/auth/google.svg [moved from resources/assets/icons/auth/google.svg with 100% similarity]
resources/icons/auth/okta.svg [moved from resources/assets/icons/auth/okta.svg with 100% similarity]
resources/icons/auth/slack.svg [moved from resources/assets/icons/auth/slack.svg with 100% similarity]
resources/icons/auth/twitch.svg [moved from resources/assets/icons/auth/twitch.svg with 100% similarity]
resources/icons/auth/twitter.svg [moved from resources/assets/icons/auth/twitter.svg with 100% similarity]
resources/icons/back.svg [moved from resources/assets/icons/back.svg with 100% similarity]
resources/icons/book.svg [moved from resources/assets/icons/book.svg with 100% similarity]
resources/icons/books.svg [moved from resources/assets/icons/books.svg with 100% similarity]
resources/icons/bookshelf.svg [moved from resources/assets/icons/bookshelf.svg with 100% similarity]
resources/icons/cancel.svg [moved from resources/assets/icons/cancel.svg with 100% similarity]
resources/icons/caret-down.svg [moved from resources/assets/icons/caret-down.svg with 100% similarity]
resources/icons/caret-left-circle.svg [moved from resources/assets/icons/caret-left-circle.svg with 100% similarity]
resources/icons/caret-right-circle.svg [moved from resources/assets/icons/caret-right-circle.svg with 100% similarity]
resources/icons/caret-right.svg [moved from resources/assets/icons/caret-right.svg with 100% similarity]
resources/icons/chapter.svg [moved from resources/assets/icons/chapter.svg with 100% similarity]
resources/icons/check-circle.svg [moved from resources/assets/icons/check-circle.svg with 100% similarity]
resources/icons/check.svg [moved from resources/assets/icons/check.svg with 100% similarity]
resources/icons/chevron-down.svg [moved from resources/assets/icons/chevron-down.svg with 100% similarity]
resources/icons/chevron-right.svg [moved from resources/assets/icons/chevron-right.svg with 100% similarity]
resources/icons/chevron-up.svg [moved from resources/assets/icons/chevron-up.svg with 100% similarity]
resources/icons/close.svg [moved from resources/assets/icons/close.svg with 100% similarity]
resources/icons/comment.svg [moved from resources/assets/icons/comment.svg with 100% similarity]
resources/icons/copy.svg [moved from resources/assets/icons/copy.svg with 100% similarity]
resources/icons/danger.svg [moved from resources/assets/icons/danger.svg with 100% similarity]
resources/icons/delete.svg [moved from resources/assets/icons/delete.svg with 100% similarity]
resources/icons/drawing.svg [moved from resources/assets/icons/drawing.svg with 100% similarity]
resources/icons/edit.svg [moved from resources/assets/icons/edit.svg with 100% similarity]
resources/icons/expand-text.svg [moved from resources/assets/icons/expand-text.svg with 100% similarity]
resources/icons/export.svg [moved from resources/assets/icons/export.svg with 100% similarity]
resources/icons/file.svg [moved from resources/assets/icons/file.svg with 100% similarity]
resources/icons/folder.svg [moved from resources/assets/icons/folder.svg with 100% similarity]
resources/icons/grid.svg [moved from resources/assets/icons/grid.svg with 100% similarity]
resources/icons/grip.svg [moved from resources/assets/icons/grip.svg with 100% similarity]
resources/icons/history.svg [moved from resources/assets/icons/history.svg with 100% similarity]
resources/icons/image.svg [moved from resources/assets/icons/image.svg with 100% similarity]
resources/icons/images.svg [moved from resources/assets/icons/images.svg with 100% similarity]
resources/icons/include.svg [moved from resources/assets/icons/include.svg with 100% similarity]
resources/icons/info-filled.svg [moved from resources/assets/icons/info-filled.svg with 100% similarity]
resources/icons/info.svg [moved from resources/assets/icons/info.svg with 100% similarity]
resources/icons/link.svg [moved from resources/assets/icons/link.svg with 100% similarity]
resources/icons/list.svg [moved from resources/assets/icons/list.svg with 100% similarity]
resources/icons/lock-open.svg [moved from resources/assets/icons/lock-open.svg with 100% similarity]
resources/icons/lock.svg [moved from resources/assets/icons/lock.svg with 100% similarity]
resources/icons/login.svg [moved from resources/assets/icons/login.svg with 100% similarity]
resources/icons/logout.svg [moved from resources/assets/icons/logout.svg with 100% similarity]
resources/icons/more.svg [moved from resources/assets/icons/more.svg with 100% similarity]
resources/icons/new-user.svg [moved from resources/assets/icons/new-user.svg with 100% similarity]
resources/icons/open-book.svg [moved from resources/assets/icons/open-book.svg with 100% similarity]
resources/icons/page.svg [moved from resources/assets/icons/page.svg with 100% similarity]
resources/icons/permission.svg [moved from resources/assets/icons/permission.svg with 100% similarity]
resources/icons/popular.svg [moved from resources/assets/icons/popular.svg with 100% similarity]
resources/icons/reply.svg [moved from resources/assets/icons/reply.svg with 100% similarity]
resources/icons/save.svg [moved from resources/assets/icons/save.svg with 100% similarity]
resources/icons/search.svg [moved from resources/assets/icons/search.svg with 100% similarity]
resources/icons/settings.svg [moved from resources/assets/icons/settings.svg with 100% similarity]
resources/icons/sort-down.svg [moved from resources/assets/icons/sort-down.svg with 100% similarity]
resources/icons/sort-up.svg [moved from resources/assets/icons/sort-up.svg with 100% similarity]
resources/icons/sort.svg [moved from resources/assets/icons/sort.svg with 100% similarity]
resources/icons/spanner.svg [moved from resources/assets/icons/spanner.svg with 100% similarity]
resources/icons/star-circle.svg [moved from resources/assets/icons/star-circle.svg with 100% similarity]
resources/icons/star.svg [moved from resources/assets/icons/star.svg with 100% similarity]
resources/icons/swap-vertical.svg [moved from resources/assets/icons/swap-vertical.svg with 100% similarity]
resources/icons/tag.svg [moved from resources/assets/icons/tag.svg with 100% similarity]
resources/icons/template.svg [moved from resources/assets/icons/template.svg with 100% similarity]
resources/icons/time.svg [moved from resources/assets/icons/time.svg with 100% similarity]
resources/icons/user.svg [moved from resources/assets/icons/user.svg with 100% similarity]
resources/icons/users-add.svg [moved from resources/assets/icons/users-add.svg with 100% similarity]
resources/icons/users.svg [moved from resources/assets/icons/users.svg with 100% similarity]
resources/icons/view.svg [moved from resources/assets/icons/view.svg with 100% similarity]
resources/icons/warning.svg [moved from resources/assets/icons/warning.svg with 100% similarity]
resources/js/components/back-to-top.js [moved from resources/assets/js/components/back-to-top.js with 100% similarity]
resources/js/components/book-sort.js [moved from resources/assets/js/components/book-sort.js with 100% similarity]
resources/js/components/breadcrumb-listing.js [moved from resources/assets/js/components/breadcrumb-listing.js with 100% similarity]
resources/js/components/chapter-toggle.js [moved from resources/assets/js/components/chapter-toggle.js with 100% similarity]
resources/js/components/collapsible.js [moved from resources/assets/js/components/collapsible.js with 100% similarity]
resources/js/components/custom-checkbox.js [moved from resources/assets/js/components/custom-checkbox.js with 100% similarity]
resources/js/components/dropdown.js [moved from resources/assets/js/components/dropdown.js with 100% similarity]
resources/js/components/editor-toolbox.js [moved from resources/assets/js/components/editor-toolbox.js with 100% similarity]
resources/js/components/entity-permissions-editor.js [moved from resources/assets/js/components/entity-permissions-editor.js with 100% similarity]
resources/js/components/entity-selector-popup.js [moved from resources/assets/js/components/entity-selector-popup.js with 100% similarity]
resources/js/components/entity-selector.js [moved from resources/assets/js/components/entity-selector.js with 100% similarity]
resources/js/components/expand-toggle.js [moved from resources/assets/js/components/expand-toggle.js with 100% similarity]
resources/js/components/header-mobile-toggle.js [moved from resources/assets/js/components/header-mobile-toggle.js with 100% similarity]
resources/js/components/homepage-control.js [moved from resources/assets/js/components/homepage-control.js with 100% similarity]
resources/js/components/image-picker.js [moved from resources/assets/js/components/image-picker.js with 100% similarity]
resources/js/components/index.js [moved from resources/assets/js/components/index.js with 100% similarity]
resources/js/components/list-sort-control.js [moved from resources/assets/js/components/list-sort-control.js with 100% similarity]
resources/js/components/markdown-editor.js [moved from resources/assets/js/components/markdown-editor.js with 100% similarity]
resources/js/components/new-user-password.js [moved from resources/assets/js/components/new-user-password.js with 100% similarity]
resources/js/components/notification.js [moved from resources/assets/js/components/notification.js with 100% similarity]
resources/js/components/overlay.js [moved from resources/assets/js/components/overlay.js with 100% similarity]
resources/js/components/page-comments.js [moved from resources/assets/js/components/page-comments.js with 100% similarity]
resources/js/components/page-display.js [moved from resources/assets/js/components/page-display.js with 100% similarity]
resources/js/components/page-picker.js [moved from resources/assets/js/components/page-picker.js with 100% similarity]
resources/js/components/permissions-table.js [moved from resources/assets/js/components/permissions-table.js with 100% similarity]
resources/js/components/setting-app-color-picker.js [moved from resources/assets/js/components/setting-app-color-picker.js with 100% similarity]
resources/js/components/shelf-sort.js [moved from resources/assets/js/components/shelf-sort.js with 100% similarity]
resources/js/components/sidebar.js [moved from resources/assets/js/components/sidebar.js with 100% similarity]
resources/js/components/template-manager.js [moved from resources/assets/js/components/template-manager.js with 100% similarity]
resources/js/components/toggle-switch.js [moved from resources/assets/js/components/toggle-switch.js with 100% similarity]
resources/js/components/tri-layout.js [moved from resources/assets/js/components/tri-layout.js with 100% similarity]
resources/js/components/wysiwyg-editor.js [moved from resources/assets/js/components/wysiwyg-editor.js with 100% similarity]
resources/js/index.js [moved from resources/assets/js/index.js with 100% similarity]
resources/js/services/animations.js [moved from resources/assets/js/services/animations.js with 100% similarity]
resources/js/services/code.js [moved from resources/assets/js/services/code.js with 100% similarity]
resources/js/services/dates.js [moved from resources/assets/js/services/dates.js with 100% similarity]
resources/js/services/dom.js [moved from resources/assets/js/services/dom.js with 100% similarity]
resources/js/services/drawio.js [moved from resources/assets/js/services/drawio.js with 100% similarity]
resources/js/services/events.js [moved from resources/assets/js/services/events.js with 100% similarity]
resources/js/services/http.js [moved from resources/assets/js/services/http.js with 100% similarity]
resources/js/services/translations.js [moved from resources/assets/js/services/translations.js with 100% similarity]
resources/js/services/util.js [moved from resources/assets/js/services/util.js with 100% similarity]
resources/js/vues/attachment-manager.js [moved from resources/assets/js/vues/attachment-manager.js with 100% similarity]
resources/js/vues/code-editor.js [moved from resources/assets/js/vues/code-editor.js with 100% similarity]
resources/js/vues/components/autosuggest.js [moved from resources/assets/js/vues/components/autosuggest.js with 100% similarity]
resources/js/vues/components/dropzone.js [moved from resources/assets/js/vues/components/dropzone.js with 100% similarity]
resources/js/vues/entity-dashboard.js [moved from resources/assets/js/vues/entity-dashboard.js with 100% similarity]
resources/js/vues/image-manager.js [moved from resources/assets/js/vues/image-manager.js with 100% similarity]
resources/js/vues/page-editor.js [moved from resources/assets/js/vues/page-editor.js with 100% similarity]
resources/js/vues/search.js [moved from resources/assets/js/vues/search.js with 100% similarity]
resources/js/vues/tag-manager.js [moved from resources/assets/js/vues/tag-manager.js with 100% similarity]
resources/js/vues/vues.js [moved from resources/assets/js/vues/vues.js with 100% similarity]
resources/lang/ar/auth.php
resources/lang/cs/auth.php
resources/lang/de/auth.php
resources/lang/en/auth.php
resources/lang/en/passwords.php
resources/lang/en/validation.php
resources/lang/es/auth.php
resources/lang/es_AR/auth.php
resources/lang/fr/auth.php
resources/lang/hu/auth.php
resources/lang/it/auth.php
resources/lang/ja/auth.php
resources/lang/kr/auth.php
resources/lang/nl/auth.php
resources/lang/pl/auth.php
resources/lang/pt_BR/auth.php
resources/lang/ru/auth.php
resources/lang/sk/auth.php
resources/lang/sv/auth.php
resources/lang/uk/auth.php
resources/lang/zh_CN/auth.php
resources/lang/zh_TW/auth.php
resources/sass/_animations.scss [moved from resources/assets/sass/_animations.scss with 100% similarity]
resources/sass/_blocks.scss [moved from resources/assets/sass/_blocks.scss with 100% similarity]
resources/sass/_buttons.scss [moved from resources/assets/sass/_buttons.scss with 100% similarity]
resources/sass/_codemirror.scss [moved from resources/assets/sass/_codemirror.scss with 100% similarity]
resources/sass/_colors.scss [moved from resources/assets/sass/_colors.scss with 100% similarity]
resources/sass/_components.scss [moved from resources/assets/sass/_components.scss with 100% similarity]
resources/sass/_forms.scss [moved from resources/assets/sass/_forms.scss with 100% similarity]
resources/sass/_header.scss [moved from resources/assets/sass/_header.scss with 100% similarity]
resources/sass/_html.scss [moved from resources/assets/sass/_html.scss with 100% similarity]
resources/sass/_layout.scss [moved from resources/assets/sass/_layout.scss with 100% similarity]
resources/sass/_lists.scss [moved from resources/assets/sass/_lists.scss with 100% similarity]
resources/sass/_mixins.scss [moved from resources/assets/sass/_mixins.scss with 100% similarity]
resources/sass/_pages.scss [moved from resources/assets/sass/_pages.scss with 100% similarity]
resources/sass/_reset.scss [moved from resources/assets/sass/_reset.scss with 100% similarity]
resources/sass/_spacing.scss [moved from resources/assets/sass/_spacing.scss with 100% similarity]
resources/sass/_tables.scss [moved from resources/assets/sass/_tables.scss with 100% similarity]
resources/sass/_text.scss [moved from resources/assets/sass/_text.scss with 100% similarity]
resources/sass/_tinymce.scss [moved from resources/assets/sass/_tinymce.scss with 100% similarity]
resources/sass/_variables.scss [moved from resources/assets/sass/_variables.scss with 100% similarity]
resources/sass/export-styles.scss [moved from resources/assets/sass/export-styles.scss with 100% similarity]
resources/sass/print-styles.scss [moved from resources/assets/sass/print-styles.scss with 100% similarity]
resources/sass/styles.scss [moved from resources/assets/sass/styles.scss with 100% similarity]
resources/views/auth/login.blade.php
resources/views/auth/register.blade.php
resources/views/books/export.blade.php
resources/views/books/list.blade.php
resources/views/errors/404.blade.php
resources/views/form/entity-permissions.blade.php
resources/views/pages/markdown-editor.blade.php
resources/views/pages/wysiwyg-editor.blade.php
resources/views/partials/breadcrumbs.blade.php
resources/views/shelves/form.blade.php
resources/views/shelves/show.blade.php
routes/web.php
storage/framework/cache/.gitignore
tests/Auth/AuthTest.php
tests/Auth/LdapTest.php
tests/Auth/SocialAuthTest.php
tests/Auth/UserInviteTest.php
tests/BrowserKitTest.php
tests/CommandsTest.php
tests/Entity/BookShelfTest.php
tests/Entity/CommentSettingTest.php
tests/Entity/EntityTest.php
tests/Entity/ExportTest.php
tests/Entity/MarkdownTest.php
tests/Entity/PageContentTest.php
tests/Entity/PageDraftTest.php
tests/Entity/PageRevisionTest.php
tests/Entity/SortTest.php
tests/Entity/TagTest.php
tests/LanguageTest.php
tests/Permissions/RestrictionsTest.php
tests/Permissions/RolesTest.php
tests/SharedTestHelpers.php
tests/Unit/ConfigTest.php
tests/Unit/PageRepoTest.php [deleted file]
tests/Unit/UrlTest.php
tests/Uploads/AttachmentTest.php
tests/Uploads/ImageTest.php
tests/UserProfileTest.php
version
webpack.config.js

index 829a7509b2a3fa31229017180f776e2347679adb..c4c3f0b85ff1192839395d1cc7edb3b965802b8f 100644 (file)
@@ -89,7 +89,7 @@ REDIS_SERVERS=127.0.0.1:6379:0
 # Queue driver to use
 # Queue not really currently used but may be configurable in the future.
 # Would advise not to change this for now.
-QUEUE_DRIVER=sync
+QUEUE_CONNECTION=sync
 
 # Storage system to use
 # Can be 'local', 'local_secure' or 's3'
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
new file mode 100644 (file)
index 0000000..922aa50
--- /dev/null
@@ -0,0 +1,26 @@
+name: phpunit
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        php: [7.2, 7.3]
+    steps:
+    - uses: actions/checkout@v1
+    - name: Setup Database
+      run: |
+        mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
+        mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
+        mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
+        mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
+    - name: Install composer dependencies & Test
+      run: composer install --prefer-dist --no-interaction --ansi
+    - name: Migrate and seed the database
+      run: |
+        php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
+        php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
+    - name: phpunit
+      run: php${{ matrix.php }} ./vendor/bin/phpunit
index 1b53cbe7ac52416e90f3844194aab4a262c48d20..e5579e4a62f21ca24e6f804fdee275b9655519b6 100644 (file)
@@ -21,4 +21,5 @@ nbproject
 .buildpath
 .project
 .settings/
-webpack-stats.json
\ No newline at end of file
+webpack-stats.json
+.phpunit.result.cache
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644 (file)
index 29727f4..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-dist: trusty
-sudo: false
-language: php
-php:
-  - 7.0.20
-  - 7.1.9
-
-cache:
-  directories:
-    - $HOME/.composer/cache
-
-before_script:
-  - mysql -u root -e 'create database `bookstack-test`;'
-  - mysql -u root -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
-  - mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
-  - mysql -u root -e "FLUSH PRIVILEGES;"
-  - phpenv config-rm xdebug.ini
-  - composer install --prefer-dist --no-interaction
-  - php artisan clear-compiled -n
-  - php artisan optimize -n
-  - php artisan migrate --force -n --database=mysql_testing
-  - php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
-
-after_failure:
-  - cat storage/logs/laravel.log
-
-script:
-  - phpunit
index 1ae1811e11461dbd94674c791b0c75143596d8b2..05f0129ddff7c322f8f900a3f5bdf4e6dba65fda 100644 (file)
@@ -3,13 +3,18 @@
 namespace BookStack\Actions;
 
 use BookStack\Auth\User;
+use BookStack\Entities\Entity;
 use BookStack\Model;
 
 /**
- * @property string  key
- * @property \User   user
- * @property \Entity entity
- * @property string  extra
+ * @property string $key
+ * @property User $user
+ * @property Entity $entity
+ * @property string $extra
+ * @property string $entity_type
+ * @property int $entity_id
+ * @property int $user_id
+ * @property int $book_id
  */
 class Activity extends Model
 {
index f4f82a6f4dfbbac6b63c2fb42a15fad0631f77e9..f56f1ca57e03c304c4f4fdcd7201293afe45f8ba 100644 (file)
@@ -1,8 +1,8 @@
 <?php namespace BookStack\Actions;
 
 use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Book;
 use BookStack\Entities\Entity;
-use Session;
 
 class ActivityService
 {
@@ -12,7 +12,7 @@ class ActivityService
 
     /**
      * ActivityService constructor.
-     * @param \BookStack\Actions\Activity $activity
+     * @param Activity $activity
      * @param PermissionService $permissionService
      */
     public function __construct(Activity $activity, PermissionService $permissionService)
@@ -24,52 +24,57 @@ class ActivityService
 
     /**
      * Add activity data to database.
-     * @param Entity $entity
-     * @param        $activityKey
+     * @param \BookStack\Entities\Entity $entity
+     * @param string $activityKey
      * @param int $bookId
-     * @param bool $extra
      */
-    public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
+    public function add(Entity $entity, string $activityKey, int $bookId = null)
     {
-        $activity = $this->activity->newInstance();
-        $activity->user_id = $this->user->id;
-        $activity->book_id = $bookId;
-        $activity->key = strtolower($activityKey);
-        if ($extra !== false) {
-            $activity->extra = $extra;
-        }
+        $activity = $this->newActivityForUser($activityKey, $bookId);
         $entity->activity()->save($activity);
         $this->setNotification($activityKey);
     }
 
     /**
-     * Adds a activity history with a message & without binding to a entity.
-     * @param            $activityKey
+     * Adds a activity history with a message, without binding to a entity.
+     * @param string $activityKey
+     * @param string $message
      * @param int $bookId
-     * @param bool|false $extra
      */
-    public function addMessage($activityKey, $bookId = 0, $extra = false)
+    public function addMessage(string $activityKey, string $message, int $bookId = null)
     {
-        $this->activity->user_id = $this->user->id;
-        $this->activity->book_id = $bookId;
-        $this->activity->key = strtolower($activityKey);
-        if ($extra !== false) {
-            $this->activity->extra = $extra;
-        }
-        $this->activity->save();
+        $this->newActivityForUser($activityKey, $bookId)->forceFill([
+            'extra' => $message
+        ])->save();
+
         $this->setNotification($activityKey);
     }
 
+    /**
+     * Get a new activity instance for the current user.
+     * @param string $key
+     * @param int|null $bookId
+     * @return Activity
+     */
+    protected function newActivityForUser(string $key, int $bookId = null)
+    {
+        return $this->activity->newInstance()->forceFill([
+            'key' => strtolower($key),
+            'user_id' => $this->user->id,
+            'book_id' => $bookId ?? 0,
+        ]);
+    }
 
     /**
      * Removes the entity attachment from each of its activities
      * and instead uses the 'extra' field with the entities name.
      * Used when an entity is deleted.
-     * @param Entity $entity
+     * @param \BookStack\Entities\Entity $entity
      * @return mixed
      */
     public function removeEntity(Entity $entity)
     {
+        // TODO - Rewrite to db query.
         $activities = $entity->activity;
         foreach ($activities as $activity) {
             $activity->extra = $entity->name;
@@ -90,7 +95,11 @@ class ActivityService
     {
         $activityList = $this->permissionService
             ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
-            ->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
+            ->orderBy('created_at', 'desc')
+            ->with('user', 'entity')
+            ->skip($count * $page)
+            ->take($count)
+            ->get();
 
         return $this->filterSimilar($activityList);
     }
@@ -98,7 +107,7 @@ class ActivityService
     /**
      * Gets the latest activity for an entity, Filtering out similar
      * items to prevent a message activity list.
-     * @param Entity $entity
+     * @param \BookStack\Entities\Entity $entity
      * @param int $count
      * @param int $page
      * @return array
@@ -171,7 +180,7 @@ class ActivityService
         $notificationTextKey = 'activities.' . $activityKey . '_notification';
         if (trans()->has($notificationTextKey)) {
             $message = trans($notificationTextKey);
-            Session::flash('success', $message);
+            session()->flash('success', $message);
         }
     }
 }
index 532f31c423ca06ae026bb931f03deb84e16bfc87..324bfaa4ef9fb14c722148529c4bb895692a12e8 100644 (file)
@@ -1,8 +1,10 @@
 <?php namespace BookStack\Actions;
 
 use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Book;
 use BookStack\Entities\Entity;
 use BookStack\Entities\EntityProvider;
+use DB;
 use Illuminate\Support\Collection;
 
 class ViewService
@@ -13,8 +15,8 @@ class ViewService
 
     /**
      * ViewService constructor.
-     * @param \BookStack\Actions\View $view
-     * @param \BookStack\Auth\Permissions\PermissionService $permissionService
+     * @param View $view
+     * @param PermissionService $permissionService
      * @param EntityProvider $entityProvider
      */
     public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
@@ -26,7 +28,7 @@ class ViewService
 
     /**
      * Add a view to the given entity.
-     * @param Entity $entity
+     * @param \BookStack\Entities\Entity $entity
      * @return int
      */
     public function add(Entity $entity)
@@ -43,7 +45,7 @@ class ViewService
         }
 
         // Otherwise create new view count
-        $entity->views()->save($this->view->create([
+        $entity->views()->save($this->view->newInstance([
             'user_id' => $user->id,
             'views' => 1
         ]));
@@ -59,12 +61,12 @@ class ViewService
      * @param string $action - used for permission checking
      * @return Collection
      */
-    public function getPopular(int $count = 10, int $page = 0, $filterModels = null, string $action = 'view')
+    public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
     {
         $skipCount = $count * $page;
         $query = $this->permissionService
             ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
-            ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
+            ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
             ->groupBy('viewable_id', 'viewable_type')
             ->orderBy('view_count', 'desc');
 
index 8c56e9dac135e63179b945f0285ac7c8bc04c2e6..499fdeaa691ce8a0442b2494c9f46b09edf42f27 100644 (file)
@@ -13,7 +13,11 @@ class Application extends \Illuminate\Foundation\Application
      */
     public function configPath($path = '')
     {
-        return $this->basePath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'Config'.($path ? DIRECTORY_SEPARATOR.$path : $path);
+        return $this->basePath
+            . DIRECTORY_SEPARATOR
+            . 'app'
+            . DIRECTORY_SEPARATOR
+            . 'Config'
+            . ($path ? DIRECTORY_SEPARATOR.$path : $path);
     }
-
-}
\ No newline at end of file
+}
index a94c54d19b4258c7e6fd771c4fdf71c9aa11df1a..9aa3b9b98b56cf0d56453ae9e2de18c48c4d4524 100644 (file)
@@ -36,5 +36,4 @@ class EmailConfirmationService extends UserTokenService
         return setting('registration-confirmation')
             || setting('registration-restrict');
     }
-
 }
index 0d46b9f882d5ebe6afe1d08fb0d7b4714c9eeb74..9c8d1a81f43ebb8f30d66807a14349cab2882a39 100644 (file)
@@ -5,6 +5,7 @@ use BookStack\Auth\UserRepo;
 use BookStack\Exceptions\SocialDriverNotConfigured;
 use BookStack\Exceptions\SocialSignInAccountNotUsed;
 use BookStack\Exceptions\UserRegistrationException;
+use Illuminate\Support\Str;
 use Laravel\Socialite\Contracts\Factory as Socialite;
 use Laravel\Socialite\Contracts\User as SocialUser;
 
@@ -104,6 +105,7 @@ class SocialAuthService
         $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
         $isLoggedIn = auth()->check();
         $currentUser = user();
+        $titleCaseDriver = Str::title($socialDriver);
 
         // When a user is not logged in and a matching SocialAccount exists,
         // Simply log the user into the application.
@@ -117,26 +119,26 @@ class SocialAuthService
         if ($isLoggedIn && $socialAccount === null) {
             $this->fillSocialAccount($socialDriver, $socialUser);
             $currentUser->socialAccounts()->save($this->socialAccount);
-            session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
+            session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
             return redirect($currentUser->getEditUrl());
         }
 
         // When a user is logged in and the social account exists and is already linked to the current user.
         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
-            session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
+            session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
             return redirect($currentUser->getEditUrl());
         }
 
         // When a user is logged in, A social account exists but the users do not match.
         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
-            session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
+            session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
             return redirect($currentUser->getEditUrl());
         }
 
         // Otherwise let the user know this social account is not used by anyone.
-        $message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
+        $message = trans('errors.social_account_not_used', ['socialAccount' => $titleCaseDriver]);
         if (setting('registration-enabled')) {
-            $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
+            $message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
         }
         
         throw new SocialSignInAccountNotUsed($message, '/login');
@@ -157,7 +159,7 @@ class SocialAuthService
             abort(404, trans('errors.social_driver_not_found'));
         }
         if (!$this->checkDriverConfigured($driver)) {
-            throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
+            throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($socialDriver)]));
         }
 
         return $driver;
@@ -244,7 +246,7 @@ class SocialAuthService
     public function detachSocialAccount($socialDriver)
     {
         user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
-        session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
+        session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
         return redirect(user()->getEditUrl());
     }
 
index 8e04d7b224b6039fb0c843e343aee9a1958dd371..20519fc7d4b98ae9f772a5387deb7fdfe9b84984 100644 (file)
@@ -19,5 +19,4 @@ class UserInviteService extends UserTokenService
         $token = $this->createTokenForUser($user);
         $user->notify(new UserInvite($token));
     }
-
 }
index 34f3b285110aa1aa799ef56492f799949e3f5893..a1defbf62d439a4bdacdef971271db1dde532afd 100644 (file)
@@ -5,6 +5,7 @@ use BookStack\Exceptions\UserTokenExpiredException;
 use BookStack\Exceptions\UserTokenNotFoundException;
 use Carbon\Carbon;
 use Illuminate\Database\Connection as Database;
+use Illuminate\Support\Str;
 use stdClass;
 
 class UserTokenService
@@ -73,9 +74,9 @@ class UserTokenService
      */
     protected function generateToken() : string
     {
-        $token = str_random(24);
+        $token = Str::random(24);
         while ($this->tokenExists($token)) {
-            $token = str_random(25);
+            $token = Str::random(25);
         }
         return $token;
     }
@@ -130,5 +131,4 @@ class UserTokenService
         return Carbon::now()->subHours($this->expiryTime)
             ->gt(new Carbon($tokenEntry->created_at));
     }
-
-}
\ No newline at end of file
+}
index a5ab4ea9a8e51109c7225bc82ccca1799470205d..97cc1ca241e84209f235136550378f3cb8f43f81 100644 (file)
@@ -215,7 +215,6 @@ class PermissionService
      * @param Collection $books
      * @param array $roles
      * @param bool $deleteOld
-     * @throws \Throwable
      */
     protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
     {
@@ -634,42 +633,40 @@ class PermissionService
     }
 
     /**
-     * Get the children of a book in an efficient single query, Filtered by the permission system.
-     * @param integer $book_id
-     * @param bool $filterDrafts
-     * @param bool $fetchPageContent
-     * @return QueryBuilder
+     * Limited the given entity query so that the query will only
+     * return items that the user has permission for the given ability.
      */
-    public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
+    public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
     {
-        $entities = $this->entityProvider;
-        $pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
-            ->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
-                $query->where('draft', '=', 0);
-                if (!$filterDrafts) {
-                    $query->orWhere(function ($query) {
-                        $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
+        $this->clean();
+        return $query->where(function (Builder $parentQuery) use ($ability) {
+            $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
+                $permissionQuery->whereIn('role_id', $this->getRoles())
+                    ->where('action', '=', $ability)
+                    ->where(function (Builder $query) {
+                        $query->where('has_permission', '=', true)
+                            ->orWhere(function (Builder $query) {
+                                $query->where('has_permission_own', '=', true)
+                                    ->where('created_by', '=', $this->currentUser()->id);
+                            });
                     });
-                }
-            });
-        $chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
-        $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
-            ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
-
-        // Add joint permission filter
-        $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
-            ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
-            ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
-            ->where(function ($query) {
-                $query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
-                    $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
-                });
             });
-        $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
+        });
+    }
 
-        $query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
-        $this->clean();
-        return  $query;
+    /**
+     * Extend the given page query to ensure draft items are not visible
+     * unless created by the given user.
+     */
+    public function enforceDraftVisiblityOnQuery(Builder $query): Builder
+    {
+        return $query->where(function (Builder $query) {
+            $query->where('draft', '=', false)
+                ->orWhere(function (Builder $query) {
+                    $query->where('draft', '=', true)
+                        ->where('created_by', '=', $this->currentUser()->id);
+                });
+        });
     }
 
     /**
@@ -684,12 +681,11 @@ class PermissionService
         if (strtolower($entityType) === 'page') {
             // Prevent drafts being visible to others.
             $query = $query->where(function ($query) {
-                $query->where('draft', '=', false);
-                if ($this->currentUser()) {
-                    $query->orWhere(function ($query) {
-                        $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
+                $query->where('draft', '=', false)
+                    ->orWhere(function ($query) {
+                        $query->where('draft', '=', true)
+                            ->where('created_by', '=', $this->currentUser()->id);
                     });
-                }
             });
         }
 
index 18d5089bec2f89dcaea0d9258a448d9463041b1b..56ef193015f7103a98bb440ec60498e5bf765c25 100644 (file)
@@ -3,6 +3,7 @@
 use BookStack\Auth\Permissions;
 use BookStack\Auth\Role;
 use BookStack\Exceptions\PermissionsException;
+use Illuminate\Support\Str;
 
 class PermissionsRepo
 {
@@ -66,7 +67,7 @@ class PermissionsRepo
         $role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
         // Prevent duplicate names
         while ($this->role->where('name', '=', $role->name)->count() > 0) {
-            $role->name .= strtolower(str_random(2));
+            $role->name .= strtolower(Str::random(2));
         }
         $role->save();
 
@@ -136,7 +137,7 @@ class PermissionsRepo
         // Prevent deleting admin role or default registration role.
         if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
             throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
-        } else if ($role->id == setting('registration-role')) {
+        } else if ($role->id === intval(setting('registration-role'))) {
             throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
         }
 
index 917d8aa26192c0388fac58fa5970267f3e25969c..712f5299b7b1d2ff5485179621aeddf9a1c5efbd 100644 (file)
@@ -75,7 +75,7 @@ class Role extends Model
      */
     public static function getRole($roleName)
     {
-        return static::where('name', '=', $roleName)->first();
+        return static::query()->where('name', '=', $roleName)->first();
     }
 
     /**
@@ -85,7 +85,7 @@ class Role extends Model
      */
     public static function getSystemRole($roleName)
     {
-        return static::where('system_name', '=', $roleName)->first();
+        return static::query()->where('system_name', '=', $roleName)->first();
     }
 
     /**
@@ -94,6 +94,15 @@ class Role extends Model
      */
     public static function visible()
     {
-        return static::where('hidden', '=', false)->orderBy('name')->get();
+        return static::query()->where('hidden', '=', false)->orderBy('name')->get();
+    }
+
+    /**
+     * Get the roles that can be restricted.
+     * @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
+     */
+    public static function restrictable()
+    {
+        return static::query()->where('system_name', '!=', 'admin')->get();
     }
 }
index 7ad14d9f0c2a495646eb4a4151f958a4a5e2fbf2..bce418a7421fb40ddeb34b0e0dacd192d7048e2a 100644 (file)
@@ -53,13 +53,24 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     protected $permissions;
 
+    /**
+     * This holds the default user when loaded.
+     * @var null|User
+     */
+    protected static $defaultUser = null;
+
     /**
      * Returns the default public user.
      * @return User
      */
     public static function getDefault()
     {
-        return static::where('system_name', '=', 'public')->first();
+        if (!is_null(static::$defaultUser)) {
+            return static::$defaultUser;
+        }
+        
+        static::$defaultUser = static::where('system_name', '=', 'public')->first();
+        return static::$defaultUser;
     }
 
     /**
index dec973f6c7acf67721e4dd31b20516c798714f48..a903e2c38324e27c1d646655d9758d8e1febb471 100644 (file)
@@ -1,32 +1,31 @@
 <?php namespace BookStack\Auth;
 
 use Activity;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Page;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Exceptions\UserUpdateException;
 use BookStack\Uploads\Image;
 use Exception;
 use Illuminate\Database\Eloquent\Builder;
 use Images;
+use Log;
 
 class UserRepo
 {
 
     protected $user;
     protected $role;
-    protected $entityRepo;
 
     /**
      * UserRepo constructor.
-     * @param User $user
-     * @param Role $role
-     * @param EntityRepo $entityRepo
      */
-    public function __construct(User $user, Role $role, EntityRepo $entityRepo)
+    public function __construct(User $user, Role $role)
     {
         $this->user = $user;
         $this->role = $role;
-        $this->entityRepo = $entityRepo;
     }
 
     /**
@@ -81,7 +80,7 @@ class UserRepo
      * Creates a new user and attaches a role to them.
      * @param array $data
      * @param boolean $verifyEmail
-     * @return \BookStack\Auth\User
+     * @return User
      */
     public function registerNew(array $data, $verifyEmail = false)
     {
@@ -121,7 +120,7 @@ class UserRepo
 
     /**
      * Checks if the give user is the only admin.
-     * @param \BookStack\Auth\User $user
+     * @param User $user
      * @return bool
      */
     public function isOnlyAdmin(User $user)
@@ -175,7 +174,7 @@ class UserRepo
      * Create a new basic instance of user.
      * @param array $data
      * @param boolean $verifyEmail
-     * @return \BookStack\Auth\User
+     * @return User
      */
     public function create(array $data, $verifyEmail = false)
     {
@@ -189,7 +188,7 @@ class UserRepo
 
     /**
      * Remove the given user from storage, Delete all related content.
-     * @param \BookStack\Auth\User $user
+     * @param User $user
      * @throws Exception
      */
     public function destroy(User $user)
@@ -206,7 +205,7 @@ class UserRepo
 
     /**
      * Get the latest activity for a user.
-     * @param \BookStack\Auth\User $user
+     * @param User $user
      * @param int $count
      * @param int $page
      * @return array
@@ -218,36 +217,35 @@ class UserRepo
 
     /**
      * Get the recently created content for this given user.
-     * @param \BookStack\Auth\User $user
-     * @param int $count
-     * @return mixed
      */
-    public function getRecentlyCreated(User $user, $count = 20)
+    public function getRecentlyCreated(User $user, int $count = 20): array
     {
-        $createdByUserQuery = function (Builder $query) use ($user) {
-            $query->where('created_by', '=', $user->id);
+        $query = function (Builder $query) use ($user, $count) {
+            return $query->orderBy('created_at', 'desc')
+                ->where('created_by', '=', $user->id)
+                ->take($count)
+                ->get();
         };
 
         return [
-            'pages'    => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
-            'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
-            'books'    => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
-            'shelves'  => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
+            'pages'    => $query(Page::visible()->where('draft', '=', false)),
+            'chapters' => $query(Chapter::visible()),
+            'books'    => $query(Book::visible()),
+            'shelves'  => $query(Bookshelf::visible()),
         ];
     }
 
     /**
      * Get asset created counts for the give user.
-     * @param \BookStack\Auth\User $user
-     * @return array
      */
-    public function getAssetCounts(User $user)
+    public function getAssetCounts(User $user): array
     {
+        $createdBy = ['created_by' => $user->id];
         return [
-            'pages'    => $this->entityRepo->getUserTotalCreated('page', $user),
-            'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
-            'books'    => $this->entityRepo->getUserTotalCreated('book', $user),
-            'shelves'    => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
+            'pages'    =>  Page::visible()->where($createdBy)->count(),
+            'chapters'    =>  Chapter::visible()->where($createdBy)->count(),
+            'books'    =>  Book::visible()->where($createdBy)->count(),
+            'shelves'    =>  Bookshelf::visible()->where($createdBy)->count(),
         ];
     }
 
@@ -260,16 +258,6 @@ class UserRepo
         return $this->role->newQuery()->orderBy('name', 'asc')->get();
     }
 
-    /**
-     * Get all the roles which can be given restricted access to
-     * other entities in the system.
-     * @return mixed
-     */
-    public function getRestrictableRoles()
-    {
-        return $this->role->where('system_name', '!=', 'admin')->get();
-    }
-
     /**
      * Get an avatar image for a user and set it as their avatar.
      * Returns early if avatars disabled or not set in config.
@@ -288,7 +276,7 @@ class UserRepo
             $user->save();
             return true;
         } catch (Exception $e) {
-            \Log::error('Failed to save user avatar image');
+            Log::error('Failed to save user avatar image');
             return false;
         }
     }
index 88052e94caf0f56c7d4bbf29e77d5e85b5ac1971..50e3bdb6a02ab2329f9756dec30b480a88e18a18 100755 (executable)
@@ -57,6 +57,9 @@ return [
     //  Application Fallback Locale
     'fallback_locale' => 'en',
 
+    // Faker Locale
+    'faker_locale' => 'en_GB',
+
     // Enable right-to-left text control.
     'rtl' => false,
 
@@ -72,10 +75,6 @@ return [
     // Encryption cipher
     'cipher' => 'AES-256-CBC',
 
-    // Logging configuration
-    // Options: single, daily, syslog, errorlog
-    'log' => env('APP_LOGGING', 'single'),
-
     // Application Services Provides
     'providers' => [
 
@@ -137,6 +136,7 @@ return [
 
         // Laravel
         'App'       => Illuminate\Support\Facades\App::class,
+        'Arr'       => Illuminate\Support\Arr::class,
         'Artisan'   => Illuminate\Support\Facades\Artisan::class,
         'Auth'      => Illuminate\Support\Facades\Auth::class,
         'Blade'     => Illuminate\Support\Facades\Blade::class,
@@ -166,6 +166,7 @@ return [
         'Schema'    => Illuminate\Support\Facades\Schema::class,
         'Session'   => Illuminate\Support\Facades\Session::class,
         'Storage'   => Illuminate\Support\Facades\Storage::class,
+        'Str'       => Illuminate\Support\Str::class,
         'URL'       => Illuminate\Support\Facades\URL::class,
         'Validator' => Illuminate\Support\Facades\Validator::class,
         'View'      => Illuminate\Support\Facades\View::class,
@@ -181,6 +182,7 @@ return [
         'Setting'  => BookStack\Facades\Setting::class,
         'Views'    => BookStack\Facades\Views::class,
         'Images'   => BookStack\Facades\Images::class,
+        'Permissions' => BookStack\Facades\Permissions::class,
 
     ],
 
index 7bf1ae7722c958cfd8940c4d30d43af78a76dbf6..5535a6f9ce88b1c5ea6fd972d212f00337dfdb24 100644 (file)
@@ -36,6 +36,7 @@ return [
         'api' => [
             'driver' => 'token',
             'provider' => 'users',
+            'hash' => false,
         ],
     ],
 
@@ -69,4 +70,4 @@ return [
         ],
     ],
 
-];
\ No newline at end of file
+];
index 3d9eb78f95abd18bdb79d785a693de3f174641a6..7aaaa5693fe1cf907e07da54880710f802c9694a 100644 (file)
@@ -24,9 +24,13 @@ return [
 
         'pusher' => [
             'driver' => 'pusher',
-            'key' => env('PUSHER_KEY'),
-            'secret' => env('PUSHER_SECRET'),
+            'key' => env('PUSHER_APP_KEY'),
+            'secret' => env('PUSHER_APP_SECRET'),
             'app_id' => env('PUSHER_APP_ID'),
+            'options' => [
+                'cluster' => env('PUSHER_APP_CLUSTER'),
+                'useTLS' => true,
+            ],
         ],
 
         'redis' => [
@@ -38,6 +42,11 @@ return [
             'driver' => 'log',
         ],
 
+        'null' => [
+            'driver' => 'null',
+        ],
+
+
     ],
 
 ];
index 43f420457eba43f7b757e48bf0960b6e5a55a2b8..33d3a1a0bb7b02e6471f1088492fa7b5078e0519 100644 (file)
@@ -14,8 +14,12 @@ if (env('CACHE_DRIVER') === 'memcached') {
     $memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
     foreach ($memcachedServers as $index => $memcachedServer) {
         $memcachedServerDetails = explode(':', $memcachedServer);
-        if (count($memcachedServerDetails) < 2) $memcachedServerDetails[] = '11211';
-        if (count($memcachedServerDetails) < 3) $memcachedServerDetails[] = '100';
+        if (count($memcachedServerDetails) < 2) {
+            $memcachedServerDetails[] = '11211';
+        }
+        if (count($memcachedServerDetails) < 3) {
+            $memcachedServerDetails[] = '100';
+        }
         $memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
     }
 }
@@ -62,6 +66,6 @@ return [
 
     // Cache key prefix
     // Used to prevent collisions in shared cache systems.
-    'prefix' => env('CACHE_PREFIX', 'bookstack'),
+    'prefix' => env('CACHE_PREFIX', 'bookstack_cache'),
 
 ];
index 93a44854f092a8d166b9ce994a41da9dacb67a99..ed654ffb9172b4789a62c922d971adb8f550976d 100644 (file)
 // REDIS
 // Split out configuration into an array
 if (env('REDIS_SERVERS', false)) {
-
     $redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
     $redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
-    $redisConfig = [];
+    $redisConfig = ['client' => 'predis'];
     $cluster = count($redisServers) > 1;
 
     if ($cluster) {
@@ -59,14 +58,9 @@ return [
     // Many of those shown here are unsupported by BookStack.
     'connections' => [
 
-        'sqlite' => [
-            'driver'   => 'sqlite',
-            'database' => storage_path('database.sqlite'),
-            'prefix'   => '',
-        ],
-
         'mysql' => [
             'driver'    => 'mysql',
+            'url' => env('DATABASE_URL'),
             'host'      => $mysql_host,
             'database'  => env('DB_DATABASE', 'forge'),
             'username'  => env('DB_USERNAME', 'forge'),
@@ -76,43 +70,28 @@ return [
             'charset'   => 'utf8mb4',
             'collation' => 'utf8mb4_unicode_ci',
             'prefix'    => '',
+            'prefix_indexes' => true,
             'strict'    => false,
             'engine' => null,
+            'options' => extension_loaded('pdo_mysql') ? array_filter([
+                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
+            ]) : [],
         ],
 
         'mysql_testing' => [
             'driver'    => 'mysql',
+            'url' => env('TEST_DATABASE_URL'),
             'host'      => '127.0.0.1',
             'database'  => 'bookstack-test',
             'username'  => env('MYSQL_USER', 'bookstack-test'),
             'password'  => env('MYSQL_PASSWORD', 'bookstack-test'),
-            'charset'   => 'utf8',
-            'collation' => 'utf8_unicode_ci',
+            'charset'   => 'utf8mb4',
+            'collation' => 'utf8mb4_unicode_ci',
             'prefix'    => '',
+            'prefix_indexes' => true,
             'strict'    => false,
         ],
 
-        'pgsql' => [
-            'driver'   => 'pgsql',
-            'host'     => env('DB_HOST', 'localhost'),
-            'database' => env('DB_DATABASE', 'forge'),
-            'username' => env('DB_USERNAME', 'forge'),
-            'password' => env('DB_PASSWORD', ''),
-            'charset'  => 'utf8',
-            'prefix'   => '',
-            'schema'   => 'public',
-        ],
-
-        'sqlsrv' => [
-            'driver'   => 'sqlsrv',
-            'host'     => env('DB_HOST', 'localhost'),
-            'database' => env('DB_DATABASE', 'forge'),
-            'username' => env('DB_USERNAME', 'forge'),
-            'password' => env('DB_PASSWORD', ''),
-            'charset'  => 'utf8',
-            'prefix'   => '',
-        ],
-
     ],
 
     // Migration Repository Table
index ec942dcd3bbbdacbab3c98fd764a0fab4b6be325..fe624eb7d460c2fa185344f7dd86f3d1906e1bcb 100644 (file)
@@ -79,6 +79,7 @@ return [
         'files'           => false, // Show the included files
         'config'          => false, // Display config settings
         'cache'           => false, // Display cache events
+        'models'          => true, // Display models
     ],
 
      // Configure some DataCollectors
index 77f0cff9c73738cd40eb61c54fe56b3053a5c76a..87be53df52b373d7c701a590f3051c7066fff711 100644 (file)
@@ -69,7 +69,7 @@ return [
          * should be an absolute path.
          * This is only checked on command line call by dompdf.php, but not by
          * direct class use like:
-         * $dompdf = new DOMPDF();     $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
+         * $dompdf = new DOMPDF();  $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
          */
         "DOMPDF_CHROOT" => realpath(base_path()),
 
diff --git a/app/Config/hashing.php b/app/Config/hashing.php
new file mode 100644 (file)
index 0000000..756718c
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * Hashing configuration options.
+ *
+ * Changes to these config files are not supported by BookStack and may break upon updates.
+ * Configuration should be altered via the `.env` file or environment variables.
+ * Do not edit this file unless you're happy to maintain any changes yourself.
+ */
+
+return [
+
+    // Default Hash Driver
+    // This option controls the default hash driver that will be used to hash
+    // passwords for your application. By default, the bcrypt algorithm is used.
+    // Supported: "bcrypt", "argon", "argon2id"
+    'driver' => 'bcrypt',
+
+    // Bcrypt Options
+    // Here you may specify the configuration options that should be used when
+    // passwords are hashed using the Bcrypt algorithm. This will allow you
+    // to control the amount of time it takes to hash the given password.
+    'bcrypt' => [
+        'rounds' => env('BCRYPT_ROUNDS', 10),
+    ],
+
+    // Argon Options
+    // Here you may specify the configuration options that should be used when
+    // passwords are hashed using the Argon algorithm. These will allow you
+    // to control the amount of time it takes to hash the given password.
+    'argon' => [
+        'memory' => 1024,
+        'threads' => 2,
+        'time' => 2,
+    ],
+
+];
diff --git a/app/Config/logging.php b/app/Config/logging.php
new file mode 100644 (file)
index 0000000..0b55dc2
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+use Monolog\Handler\NullHandler;
+use Monolog\Handler\StreamHandler;
+
+/**
+ * Logging configuration options.
+ *
+ * Changes to these config files are not supported by BookStack and may break upon updates.
+ * Configuration should be altered via the `.env` file or environment variables.
+ * Do not edit this file unless you're happy to maintain any changes yourself.
+ */
+
+return [
+
+    // Default Log Channel
+    // This option defines the default log channel that gets used when writing
+    // messages to the logs. The name specified in this option should match
+    // one of the channels defined in the "channels" configuration array.
+    'default' => env('LOG_CHANNEL', 'single'),
+
+    // Log Channels
+    // Here you may configure the log channels for your application. Out of
+    // the box, Laravel uses the Monolog PHP logging library. This gives
+    // you a variety of powerful log handlers / formatters to utilize.
+    // Available Drivers: "single", "daily", "slack", "syslog",
+    //                    "errorlog", "monolog",
+    //                    "custom", "stack"
+    'channels' => [
+        'stack' => [
+            'driver' => 'stack',
+            'channels' => ['daily'],
+            'ignore_exceptions' => false,
+        ],
+
+        'single' => [
+            'driver' => 'single',
+            'path' => storage_path('logs/laravel.log'),
+            'level' => 'debug',
+            'days' => 14,
+        ],
+
+        'daily' => [
+            'driver' => 'daily',
+            'path' => storage_path('logs/laravel.log'),
+            'level' => 'debug',
+            'days' => 7,
+        ],
+
+        'slack' => [
+            'driver' => 'slack',
+            'url' => env('LOG_SLACK_WEBHOOK_URL'),
+            'username' => 'Laravel Log',
+            'emoji' => ':boom:',
+            'level' => 'critical',
+        ],
+
+        'stderr' => [
+            'driver' => 'monolog',
+            'handler' => StreamHandler::class,
+            'with' => [
+                'stream' => 'php://stderr',
+            ],
+        ],
+
+        'syslog' => [
+            'driver' => 'syslog',
+            'level' => 'debug',
+        ],
+
+        'errorlog' => [
+            'driver' => 'errorlog',
+            'level' => 'debug',
+        ],
+
+        'null' => [
+            'driver' => 'monolog',
+            'handler' => NullHandler::class,
+        ],
+    ],
+
+];
index 49407bd8eae1f34d08931795f95c4d893ee63a09..a91bdf23797ef182325da8f49d621d541f1bc8d2 100644 (file)
@@ -23,7 +23,7 @@ return [
     // Global "From" address & name
     'from' => [
         'address' => env('MAIL_FROM', '[email protected]'),
-        'name' => env('MAIL_FROM_NAME','BookStack')
+        'name' => env('MAIL_FROM_NAME', 'BookStack')
     ],
 
     // Email encryption protocol
@@ -46,4 +46,10 @@ return [
         ],
     ],
 
+    // Log Channel
+    // If you are using the "log" driver, you may specify the logging channel
+    // if you prefer to keep mail messages separate from other log entries
+    // for simpler reading. Otherwise, the default channel will be used.
+    'log_channel' => env('MAIL_LOG_CHANNEL'),
+
 ];
index 721eac13685cccaebc05b4e9ec417289017c4929..46f6962c5f327f4fd644f029d3159eece4f6701a 100644 (file)
@@ -12,11 +12,12 @@ return [
 
     // Default driver to use for the queue
     // Options: null, sync, redis
-    'default' => env('QUEUE_DRIVER', 'sync'),
+    'default' => env('QUEUE_CONNECTION', 'sync'),
 
     // Queue connection configuration
     'connections' => [
 
+
         'sync' => [
             'driver' => 'sync',
         ],
@@ -25,38 +26,15 @@ return [
             'driver' => 'database',
             'table' => 'jobs',
             'queue' => 'default',
-            'expire' => 60,
-        ],
-
-        'beanstalkd' => [
-            'driver' => 'beanstalkd',
-            'host'   => 'localhost',
-            'queue'  => 'default',
-            'ttr'    => 60,
-        ],
-
-        'sqs' => [
-            'driver' => 'sqs',
-            'key'    => 'your-public-key',
-            'secret' => 'your-secret-key',
-            'queue'  => 'your-queue-url',
-            'region' => 'us-east-1',
-        ],
-
-        'iron' => [
-            'driver'  => 'iron',
-            'host'    => 'mq-aws-us-east-1.iron.io',
-            'token'   => 'your-token',
-            'project' => 'your-project-id',
-            'queue'   => 'your-queue-name',
-            'encrypt' => true,
+            'retry_after' => 90,
         ],
 
         'redis' => [
             'driver' => 'redis',
             'connection' => 'default',
-            'queue'  => 'default',
-            'expire' => 60,
+            'queue' => env('REDIS_QUEUE', 'default'),
+            'retry_after' => 90,
+            'block_for' => null,
         ],
 
     ],
index 97cb71ddc73f0cf623907936fe43b02356496269..923015f6e62815e66b388a21f6e9d4b0b2282388 100644 (file)
@@ -22,23 +22,6 @@ return [
     // Callback URL for social authentication methods
     'callback_url' => env('APP_URL', false),
 
-    'mailgun'  => [
-        'domain' => '',
-        'secret' => '',
-    ],
-
-    'ses'      => [
-        'key'    => '',
-        'secret' => '',
-        'region' => 'us-east-1',
-    ],
-
-    'stripe'   => [
-        'model'  => \BookStack\Auth\User::class,
-        'key'    => '',
-        'secret' => '',
-    ],
-
     'github'   => [
         'client_id'     => env('GITHUB_APP_ID', false),
         'client_secret' => env('GITHUB_APP_SECRET', false),
@@ -98,8 +81,8 @@ return [
     'okta' => [
         'client_id' => env('OKTA_APP_ID'),
         'client_secret' => env('OKTA_APP_SECRET'),
-        'redirect' => env('APP_URL') . '/login/service/okta/callback', 
-        'base_url' => env('OKTA_BASE_URL'), 
+        'redirect' => env('APP_URL') . '/login/service/okta/callback',
+        'base_url' => env('OKTA_BASE_URL'),
         'name'          => 'Okta',
         'auto_register' => env('OKTA_AUTO_REGISTER', false),
         'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
@@ -143,10 +126,10 @@ return [
         'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
         'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
         'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
-               'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
-               'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
-               'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false),
-               'tls_insecure' => env('LDAP_TLS_INSECURE', false),
-       ]
+        'user_to_groups' => env('LDAP_USER_TO_GROUPS', false),
+        'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
+        'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
+        'tls_insecure' => env('LDAP_TLS_INSECURE', false),
+    ]
 
 ];
index bdb5e554b3238f274478e835f5dd867c4b737183..37f1627bb5f5c151ae01748cdc06b8f5c7e7eeb2 100644 (file)
@@ -35,13 +35,18 @@ return [
     // Session database table, if database driver is in use
     'table' => 'sessions',
 
+    // Session Cache Store
+    // When using the "apc" or "memcached" session drivers, you may specify a
+    // cache store that should be used for these sessions. This value must
+    // correspond with one of the application's configured cache stores.
+    'store' => null,
+
     // Session Sweeping Lottery
     // Some session drivers must manually sweep their storage location to get
     // rid of old sessions from storage. Here are the chances that it will
     // happen on a given request. By default, the odds are 2 out of 100.
     'lottery' => [2, 100],
 
-
     // Session Cookie Name
     // Here you may change the name of the cookie used to identify a session
     // instance by ID. The name specified here will get used every time a
index 4a135573bd5ba78cfd7bc814fc1ec9c2489698c0..c6080df1db6df81b434a935dde272c0f038c5c0a 100644 (file)
@@ -19,4 +19,4 @@ return [
     'app-custom-head'      => false,
     'registration-enabled' => false,
 
-];
\ No newline at end of file
+];
index 7d3d5e4ae98bf0e670c436dba1df93fb7c86d92b..4e54457b80391aa223442d92e065f376a12b86a9 100644 (file)
@@ -1,22 +1,25 @@
 <?php namespace BookStack\Entities;
 
 use BookStack\Uploads\Image;
+use Exception;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Support\Collection;
 
-class Book extends Entity
+/**
+ * Class Book
+ * @property string $description
+ * @property int $image_id
+ * @property Image|null $cover
+ * @package BookStack\Entities
+ */
+class Book extends Entity implements HasCoverImage
 {
     public $searchFactor = 2;
 
     protected $fillable = ['name', 'description', 'image_id'];
 
-    /**
-     * Get the morph class for this model.
-     * @return string
-     */
-    public function getMorphClass()
-    {
-        return 'BookStack\\Book';
-    }
-
     /**
      * Get the url for this book.
      * @param string|bool $path
@@ -45,7 +48,7 @@ class Book extends Entity
 
         try {
             $cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
-        } catch (\Exception $err) {
+        } catch (Exception $err) {
             $cover = $default;
         }
         return $cover;
@@ -53,16 +56,23 @@ class Book extends Entity
 
     /**
      * Get the cover image of the book
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
      */
-    public function cover()
+    public function cover(): BelongsTo
     {
         return $this->belongsTo(Image::class, 'image_id');
     }
 
+    /**
+     * Get the type of the image model that is used when storing a cover image.
+     */
+    public function coverImageTypeKey(): string
+    {
+        return 'cover_book';
+    }
+
     /**
      * Get all pages within this book.
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     * @return HasMany
      */
     public function pages()
     {
@@ -71,7 +81,7 @@ class Book extends Entity
 
     /**
      * Get the direct child pages of this book.
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     * @return HasMany
      */
     public function directPages()
     {
@@ -80,7 +90,7 @@ class Book extends Entity
 
     /**
      * Get all chapters within this book.
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     * @return HasMany
      */
     public function chapters()
     {
@@ -89,7 +99,7 @@ class Book extends Entity
 
     /**
      * Get the shelves this book is contained within.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+     * @return BelongsToMany
      */
     public function shelves()
     {
@@ -97,22 +107,24 @@ class Book extends Entity
     }
 
     /**
-     * Get an excerpt of this book's description to the specified length or less.
-     * @param int $length
-     * @return string
+     * Get the direct child items within this book.
+     * @return Collection
      */
-    public function getExcerpt(int $length = 100)
+    public function getDirectChildren(): Collection
     {
-        $description = $this->description;
-        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
+        $pages = $this->directPages()->visible()->get();
+        $chapters = $this->chapters()->visible()->get();
+        return $pages->contact($chapters)->sortBy('priority')->sortByDesc('draft');
     }
 
     /**
-     * Return a generalised, common raw query that can be 'unioned' across entities.
+     * Get an excerpt of this book's description to the specified length or less.
+     * @param int $length
      * @return string
      */
-    public function entityRawQuery()
+    public function getExcerpt(int $length = 100)
     {
-        return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+        $description = $this->description;
+        return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
     }
 }
diff --git a/app/Entities/BookChild.php b/app/Entities/BookChild.php
new file mode 100644 (file)
index 0000000..6eac437
--- /dev/null
@@ -0,0 +1,60 @@
+<?php namespace BookStack\Entities;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * Class BookChild
+ * @property int $book_id
+ * @property int $priority
+ * @property Book $book
+ * @method Builder whereSlugs(string $bookSlug, string $childSlug)
+ */
+class BookChild extends Entity
+{
+
+    /**
+     * Scope a query to find items where the the child has the given childSlug
+     * where its parent has the bookSlug.
+     */
+    public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug)
+    {
+        return $query->with('book')
+            ->whereHas('book', function (Builder $query) use ($bookSlug) {
+                $query->where('slug', '=', $bookSlug);
+            })
+            ->where('slug', '=', $childSlug);
+    }
+
+    /**
+     * Get the book this page sits in.
+     * @return BelongsTo
+     */
+    public function book(): BelongsTo
+    {
+        return $this->belongsTo(Book::class);
+    }
+
+    /**
+     * Change the book that this entity belongs to.
+     */
+    public function changeBook(int $newBookId): Entity
+    {
+        $this->book_id = $newBookId;
+        $this->refreshSlug();
+        $this->save();
+        $this->refresh();
+
+        // Update related activity
+        $this->activity()->update(['book_id' => $newBookId]);
+
+        // Update all child pages if a chapter
+        if ($this instanceof Chapter) {
+            foreach ($this->pages as $page) {
+                $page->changeBook($newBookId);
+            }
+        }
+
+        return $this;
+    }
+}
index db6685688b3bcd2e6c449338f7ac9e83eef58803..62c7e2fe4f56c3e0a78f91105d6ba3c0da6dfad9 100644 (file)
@@ -1,8 +1,10 @@
 <?php namespace BookStack\Entities;
 
 use BookStack\Uploads\Image;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
-class Bookshelf extends Entity
+class Bookshelf extends Entity implements HasCoverImage
 {
     protected $table = 'bookshelves';
 
@@ -10,15 +12,6 @@ class Bookshelf extends Entity
 
     protected $fillable = ['name', 'description', 'image_id'];
 
-    /**
-     * Get the morph class for this model.
-     * @return string
-     */
-    public function getMorphClass()
-    {
-        return 'BookStack\\Bookshelf';
-    }
-
     /**
      * Get the books in this shelf.
      * Should not be used directly since does not take into account permissions.
@@ -31,6 +24,14 @@ class Bookshelf extends Entity
             ->orderBy('order', 'asc');
     }
 
+    /**
+     * Related books that are visible to the current user.
+     */
+    public function visibleBooks(): BelongsToMany
+    {
+        return $this->books()->visible();
+    }
+
     /**
      * Get the url for this bookshelf.
      * @param string|bool $path
@@ -68,13 +69,20 @@ class Bookshelf extends Entity
 
     /**
      * Get the cover image of the shelf
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
      */
-    public function cover()
+    public function cover(): BelongsTo
     {
         return $this->belongsTo(Image::class, 'image_id');
     }
 
+    /**
+     * Get the type of the image model that is used when storing a cover image.
+     */
+    public function coverImageTypeKey(): string
+    {
+        return 'cover_shelf';
+    }
+
     /**
      * Get an excerpt of this book's description to the specified length or less.
      * @param int $length
@@ -87,21 +95,26 @@ class Bookshelf extends Entity
     }
 
     /**
-     * Return a generalised, common raw query that can be 'unioned' across entities.
-     * @return string
+     * Check if this shelf contains the given book.
+     * @param Book $book
+     * @return bool
      */
-    public function entityRawQuery()
+    public function contains(Book $book): bool
     {
-        return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+        return $this->books()->where('id', '=', $book->id)->count() > 0;
     }
 
     /**
-     * Check if this shelf contains the given book.
+     * Add a book to the end of this shelf.
      * @param Book $book
-     * @return bool
      */
-    public function contains(Book $book)
+    public function appendBook(Book $book)
     {
-        return $this->books()->where('id', '=', $book->id)->count() > 0;
+        if ($this->contains($book)) {
+            return;
+        }
+
+        $maxOrder = $this->books()->max('order');
+        $this->books()->attach($book->id, ['order' => $maxOrder + 1]);
     }
 }
index 97ddbc2dc86764ed930c82d3c549f47b35c83fe1..43d63d026021dd07edcb1165841018ee213b13a3 100644 (file)
@@ -1,5 +1,6 @@
 <?php namespace BookStack\Entities;
 
+use BookStack\Entities\Managers\EntityContext;
 use Illuminate\View\View;
 
 class BreadcrumbsViewComposer
@@ -9,9 +10,9 @@ class BreadcrumbsViewComposer
 
     /**
      * BreadcrumbsViewComposer constructor.
-     * @param EntityContextManager $entityContextManager
+     * @param EntityContext $entityContextManager
      */
-    public function __construct(EntityContextManager $entityContextManager)
+    public function __construct(EntityContext $entityContextManager)
     {
         $this->entityContextManager = $entityContextManager;
     }
@@ -23,8 +24,9 @@ class BreadcrumbsViewComposer
     public function compose(View $view)
     {
         $crumbs = $view->getData()['crumbs'];
-        if (array_first($crumbs) instanceof Book) {
-            $shelf = $this->entityContextManager->getContextualShelfForBook(array_first($crumbs));
+        $firstCrumb = $crumbs[0] ?? null;
+        if ($firstCrumb instanceof Book) {
+            $shelf = $this->entityContextManager->getContextualShelfForBook($firstCrumb);
             if ($shelf) {
                 array_unshift($crumbs, $shelf);
                 $view->with('crumbs', $crumbs);
index b204f1903034663e755a6ca85bb38cf7f26683f3..848bc6448bd467b853ef25c8982c0aa8b5ad6e4c 100644 (file)
@@ -1,29 +1,18 @@
 <?php namespace BookStack\Entities;
 
-class Chapter extends Entity
+use Illuminate\Support\Collection;
+
+/**
+ * Class Chapter
+ * @property Collection<Page> $pages
+ * @package BookStack\Entities
+ */
+class Chapter extends BookChild
 {
     public $searchFactor = 1.3;
 
     protected $fillable = ['name', 'description', 'priority', 'book_id'];
 
-    /**
-     * Get the morph class for this model.
-     * @return string
-     */
-    public function getMorphClass()
-    {
-        return 'BookStack\\Chapter';
-    }
-
-    /**
-     * Get the book this chapter is within.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
-     */
-    public function book()
-    {
-        return $this->belongsTo(Book::class);
-    }
-
     /**
      * Get the pages that this chapter contains.
      * @param string $dir
@@ -63,20 +52,22 @@ class Chapter extends Entity
     }
 
     /**
-     * Return a generalised, common raw query that can be 'unioned' across entities.
-     * @return string
+     * Check if this chapter has any child pages.
+     * @return bool
      */
-    public function entityRawQuery()
+    public function hasChildren()
     {
-        return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+        return count($this->pages) > 0;
     }
 
     /**
-     * Check if this chapter has any child pages.
-     * @return bool
+     * Get the visible pages in this chapter.
      */
-    public function hasChildren()
+    public function getVisiblePages(): Collection
     {
-        return count($this->pages) > 0;
+        return $this->pages()->visible()
+        ->orderBy('draft', 'desc')
+        ->orderBy('priority', 'asc')
+        ->get();
     }
 }
index 482d217662ae20b54f62578d88367b8daacb1098..5013c39cfcf2bf309f1931594ebe702dd00dc44c 100644 (file)
@@ -6,8 +6,11 @@ use BookStack\Actions\Tag;
 use BookStack\Actions\View;
 use BookStack\Auth\Permissions\EntityPermission;
 use BookStack\Auth\Permissions\JointPermission;
+use BookStack\Facades\Permissions;
 use BookStack\Ownable;
 use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Database\Eloquent\Relations\MorphMany;
 
 /**
@@ -15,7 +18,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
  * The base class for book-like items such as pages, chapters & books.
  * This is not a database model in itself but extended.
  *
- * @property integer $id
+ * @property int $id
  * @property string $name
  * @property string $slug
  * @property Carbon $created_at
@@ -23,6 +26,11 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
  * @property int $created_by
  * @property int $updated_by
  * @property boolean $restricted
+ * @property Collection $tags
+ * @method static Entity|Builder visible()
+ * @method static Entity|Builder hasPermission(string $permission)
+ * @method static Builder withLastView()
+ * @method static Builder withViewCount()
  *
  * @package BookStack\Entities
  */
@@ -40,14 +48,45 @@ class Entity extends Ownable
     public $searchFactor = 1.0;
 
     /**
-     * Get the morph class for this model.
-     * Set here since, due to folder changes, the namespace used
-     * in the database no longer matches the class namespace.
-     * @return string
+     * Get the entities that are visible to the current user.
+     */
+    public function scopeVisible(Builder $query)
+    {
+        return $this->scopeHasPermission($query, 'view');
+    }
+
+    /**
+     * Scope the query to those entities that the current user has the given permission for.
+     */
+    public function scopeHasPermission(Builder $query, string $permission)
+    {
+        return Permissions::restrictEntityQuery($query, $permission);
+    }
+
+    /**
+     * Query scope to get the last view from the current user.
      */
-    public function getMorphClass()
+    public function scopeWithLastView(Builder $query)
     {
-        return 'BookStack\\Entity';
+        $viewedAtQuery = View::query()->select('updated_at')
+            ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
+            ->where('viewable_type', '=', $this->getMorphClass())
+            ->where('user_id', '=', user()->id)
+            ->take(1);
+
+        return $query->addSelect(['last_viewed_at' => $viewedAtQuery]);
+    }
+
+    /**
+     * Query scope to get the total view count of the entities.
+     */
+    public function scopeWithViewCount(Builder $query)
+    {
+        $viewCountQuery = View::query()->selectRaw('SUM(views) as view_count')
+            ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
+            ->where('viewable_type', '=', $this->getMorphClass())->take(1);
+
+        $query->addSelect(['view_count' => $viewCountQuery]);
     }
 
     /**
@@ -87,11 +126,12 @@ class Entity extends Ownable
 
     /**
      * Gets the activity objects for this entity.
-     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+     * @return MorphMany
      */
     public function activity()
     {
-        return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
+        return $this->morphMany(Activity::class, 'entity')
+            ->orderBy('created_at', 'desc');
     }
 
     /**
@@ -102,14 +142,9 @@ class Entity extends Ownable
         return $this->morphMany(View::class, 'viewable');
     }
 
-    public function viewCountQuery()
-    {
-        return $this->views()->selectRaw('viewable_id, sum(views) as view_count')->groupBy('viewable_id');
-    }
-
     /**
      * Get the Tag models that have been user assigned to this entity.
-     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+     * @return MorphMany
      */
     public function tags()
     {
@@ -129,7 +164,7 @@ class Entity extends Ownable
 
     /**
      * Get the related search terms.
-     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+     * @return MorphMany
      */
     public function searchTerms()
     {
@@ -158,7 +193,7 @@ class Entity extends Ownable
 
     /**
      * Get the entity jointPermissions this is connected to.
-     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+     * @return MorphMany
      */
     public function jointPermissions()
     {
@@ -238,21 +273,40 @@ class Entity extends Ownable
     }
 
     /**
-     * Return a generalised, common raw query that can be 'unioned' across entities.
+     * Get the url of this entity
+     * @param $path
      * @return string
      */
-    public function entityRawQuery()
+    public function getUrl($path = '/')
     {
-        return '';
+        return $path;
     }
 
     /**
-     * Get the url of this entity
-     * @param $path
-     * @return string
+     * Rebuild the permissions for this entity.
      */
-    public function getUrl($path = '/')
+    public function rebuildPermissions()
     {
-        return $path;
+        /** @noinspection PhpUnhandledExceptionInspection */
+        Permissions::buildJointPermissionsForEntity($this);
+    }
+
+    /**
+     * Index the current entity for search
+     */
+    public function indexForSearch()
+    {
+        $searchService = app()->make(SearchService::class);
+        $searchService->indexEntity($this);
+    }
+
+    /**
+     * Generate and set a new URL slug for this model.
+     */
+    public function refreshSlug(): string
+    {
+        $generator = new SlugGenerator($this);
+        $this->slug = $generator->generate();
+        return $this->slug;
     }
 }
index d0d4a7ad6c073a7737fdb0a03ef569fbef2487cb..6bf923b3112aa8e7387fd6eedeb601a601511dad 100644 (file)
@@ -39,11 +39,6 @@ class EntityProvider
 
     /**
      * EntityProvider constructor.
-     * @param Bookshelf $bookshelf
-     * @param Book $book
-     * @param Chapter $chapter
-     * @param Page $page
-     * @param PageRevision $pageRevision
      */
     public function __construct(
         Bookshelf $bookshelf,
@@ -62,9 +57,8 @@ class EntityProvider
     /**
      * Fetch all core entity types as an associated array
      * with their basic names as the keys.
-     * @return Entity[]
      */
-    public function all()
+    public function all(): array
     {
         return [
             'bookshelf' => $this->bookshelf,
@@ -76,10 +70,8 @@ class EntityProvider
 
     /**
      * Get an entity instance by it's basic name.
-     * @param string $type
-     * @return Entity
      */
-    public function get(string $type)
+    public function get(string $type): Entity
     {
         $type = strtolower($type);
         return $this->all()[$type];
@@ -87,15 +79,9 @@ class EntityProvider
 
     /**
      * Get the morph classes, as an array, for a single or multiple types.
-     * @param string|array $types
-     * @return array<string>
      */
-    public function getMorphClasses($types)
+    public function getMorphClasses(array $types): array
     {
-        if (is_string($types)) {
-            $types = [$types];
-        }
-
         $morphClasses = [];
         foreach ($types as $type) {
             $model = $this->get($type);
index 09635aa214f0b8e112583e2c747644030e5bd4b1..3ec867959a7d8b1046127e90534bff5050ca73f6 100644 (file)
@@ -1,35 +1,34 @@
 <?php namespace BookStack\Entities;
 
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\PageContent;
 use BookStack\Uploads\ImageService;
+use DomPDF;
+use Exception;
+use SnappyPDF;
+use Throwable;
 
 class ExportService
 {
 
-    protected $entityRepo;
     protected $imageService;
 
     /**
      * ExportService constructor.
-     * @param EntityRepo $entityRepo
-     * @param ImageService $imageService
      */
-    public function __construct(EntityRepo $entityRepo, ImageService $imageService)
+    public function __construct(ImageService $imageService)
     {
-        $this->entityRepo = $entityRepo;
         $this->imageService = $imageService;
     }
 
     /**
      * Convert a page to a self-contained HTML file.
      * Includes required CSS & image content. Images are base64 encoded into the HTML.
-     * @param \BookStack\Entities\Page $page
-     * @return mixed|string
-     * @throws \Throwable
+     * @throws Throwable
      */
     public function pageToContainedHtml(Page $page)
     {
-        $this->entityRepo->renderPage($page);
+        $page->html = (new PageContent($page))->render();
         $pageHtml = view('pages/export', [
             'page' => $page
         ])->render();
@@ -38,15 +37,13 @@ class ExportService
 
     /**
      * Convert a chapter to a self-contained HTML file.
-     * @param \BookStack\Entities\Chapter $chapter
-     * @return mixed|string
-     * @throws \Throwable
+     * @throws Throwable
      */
     public function chapterToContainedHtml(Chapter $chapter)
     {
-        $pages = $this->entityRepo->getChapterChildren($chapter);
+        $pages = $chapter->getVisiblePages();
         $pages->each(function ($page) {
-            $page->html = $this->entityRepo->renderPage($page);
+            $page->html = (new PageContent($page))->render();
         });
         $html = view('chapters/export', [
             'chapter' => $chapter,
@@ -57,13 +54,11 @@ class ExportService
 
     /**
      * Convert a book to a self-contained HTML file.
-     * @param Book $book
-     * @return mixed|string
-     * @throws \Throwable
+     * @throws Throwable
      */
     public function bookToContainedHtml(Book $book)
     {
-        $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+        $bookTree = (new BookContents($book))->getTree(false, true);
         $html = view('books/export', [
             'book' => $book,
             'bookChildren' => $bookTree
@@ -73,13 +68,11 @@ class ExportService
 
     /**
      * Convert a page to a PDF file.
-     * @param Page $page
-     * @return mixed|string
-     * @throws \Throwable
+     * @throws Throwable
      */
     public function pageToPdf(Page $page)
     {
-        $this->entityRepo->renderPage($page);
+        $page->html = (new PageContent($page))->render();
         $html = view('pages/pdf', [
             'page' => $page
         ])->render();
@@ -88,32 +81,30 @@ class ExportService
 
     /**
      * Convert a chapter to a PDF file.
-     * @param \BookStack\Entities\Chapter $chapter
-     * @return mixed|string
-     * @throws \Throwable
+     * @throws Throwable
      */
     public function chapterToPdf(Chapter $chapter)
     {
-        $pages = $this->entityRepo->getChapterChildren($chapter);
+        $pages = $chapter->getVisiblePages();
         $pages->each(function ($page) {
-            $page->html = $this->entityRepo->renderPage($page);
+            $page->html = (new PageContent($page))->render();
         });
+
         $html = view('chapters/export', [
             'chapter' => $chapter,
             'pages' => $pages
         ])->render();
+
         return $this->htmlToPdf($html);
     }
 
     /**
-     * Convert a book to a PDF file
-     * @param \BookStack\Entities\Book $book
-     * @return string
-     * @throws \Throwable
+     * Convert a book to a PDF file.
+     * @throws Throwable
      */
     public function bookToPdf(Book $book)
     {
-        $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+        $bookTree = (new BookContents($book))->getTree(false, true);
         $html = view('books/export', [
             'book' => $book,
             'bookChildren' => $bookTree
@@ -122,31 +113,27 @@ class ExportService
     }
 
     /**
-     * Convert normal webpage HTML to a PDF.
-     * @param $html
-     * @return string
-     * @throws \Exception
+     * Convert normal web-page HTML to a PDF.
+     * @throws Exception
      */
-    protected function htmlToPdf($html)
+    protected function htmlToPdf(string $html): string
     {
         $containedHtml = $this->containHtml($html);
         $useWKHTML = config('snappy.pdf.binary') !== false;
         if ($useWKHTML) {
-            $pdf = \SnappyPDF::loadHTML($containedHtml);
+            $pdf = SnappyPDF::loadHTML($containedHtml);
             $pdf->setOption('print-media-type', true);
         } else {
-            $pdf = \DomPDF::loadHTML($containedHtml);
+            $pdf = DomPDF::loadHTML($containedHtml);
         }
         return $pdf->output();
     }
 
     /**
      * Bundle of the contents of a html file to be self-contained.
-     * @param $htmlContent
-     * @return mixed|string
-     * @throws \Exception
+     * @throws Exception
      */
-    protected function containHtml($htmlContent)
+    protected function containHtml(string $htmlContent): string
     {
         $imageTagsOutput = [];
         preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
@@ -188,12 +175,10 @@ class ExportService
     /**
      * Converts the page contents into simple plain text.
      * This method filters any bad looking content to provide a nice final output.
-     * @param Page $page
-     * @return mixed
      */
-    public function pageToPlainText(Page $page)
+    public function pageToPlainText(Page $page): string
     {
-        $html = $this->entityRepo->renderPage($page);
+        $html = (new PageContent($page))->render();
         $text = strip_tags($html);
         // Replace multiple spaces with single spaces
         $text = preg_replace('/\ {2,}/', ' ', $text);
@@ -207,10 +192,8 @@ class ExportService
 
     /**
      * Convert a chapter into a plain text string.
-     * @param \BookStack\Entities\Chapter $chapter
-     * @return string
      */
-    public function chapterToPlainText(Chapter $chapter)
+    public function chapterToPlainText(Chapter $chapter): string
     {
         $text = $chapter->name . "\n\n";
         $text .= $chapter->description . "\n\n";
@@ -222,12 +205,10 @@ class ExportService
 
     /**
      * Convert a book into a plain text string.
-     * @param Book $book
-     * @return string
      */
-    public function bookToPlainText(Book $book)
+    public function bookToPlainText(Book $book): string
     {
-        $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+        $bookTree = (new BookContents($book))->getTree(false, true);
         $text = $book->name . "\n\n";
         foreach ($bookTree as $bookChild) {
             if ($bookChild->isA('chapter')) {
diff --git a/app/Entities/HasCoverImage.php b/app/Entities/HasCoverImage.php
new file mode 100644 (file)
index 0000000..31277f4
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+
+namespace BookStack\Entities;
+
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+interface HasCoverImage
+{
+
+    /**
+     * Get the cover image for this item.
+     */
+    public function cover(): BelongsTo;
+
+    /**
+     * Get the type of the image model that is used when storing a cover image.
+     */
+    public function coverImageTypeKey(): string;
+}
diff --git a/app/Entities/Managers/BookContents.php b/app/Entities/Managers/BookContents.php
new file mode 100644 (file)
index 0000000..8b8d02c
--- /dev/null
@@ -0,0 +1,204 @@
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\BookChild;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Entity;
+use BookStack\Entities\Page;
+use BookStack\Exceptions\SortOperationException;
+use Illuminate\Support\Collection;
+
+class BookContents
+{
+
+    /**
+     * @var Book
+     */
+    protected $book;
+
+    /**
+     * BookContents constructor.
+     * @param $book
+     */
+    public function __construct(Book $book)
+    {
+        $this->book = $book;
+    }
+
+    /**
+     * Get the current priority of the last item
+     * at the top-level of the book.
+     */
+    public function getLastPriority(): int
+    {
+        $maxPage = Page::visible()->where('book_id', '=', $this->book->id)
+            ->where('draft', '=', false)
+            ->where('chapter_id', '=', 0)->max('priority');
+        $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
+            ->max('priority');
+        return max($maxChapter, $maxPage, 1);
+    }
+
+    /**
+     * Get the contents as a sorted collection tree.
+     * TODO - Support $renderPages option
+     */
+    public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
+    {
+        $pages = $this->getPages($showDrafts);
+        $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
+        $all = collect()->concat($pages)->concat($chapters);
+        $chapterMap = $chapters->keyBy('id');
+        $lonePages = collect();
+
+        $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
+            $chapter = $chapterMap->get($chapter_id);
+            if ($chapter) {
+                $chapter->setAttribute('pages', collect($pages)->sortBy($this->bookChildSortFunc()));
+            } else {
+                $lonePages = $lonePages->concat($pages);
+            }
+        });
+
+        $all->each(function (Entity $entity) {
+            $entity->setRelation('book', $this->book);
+        });
+
+        return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
+    }
+
+    /**
+     * Function for providing a sorting score for an entity in relation to the
+     * other items within the book.
+     */
+    protected function bookChildSortFunc(): callable
+    {
+        return function (Entity $entity) {
+            if (isset($entity['draft']) && $entity['draft']) {
+                return -100;
+            }
+            return $entity['priority'] ?? 0;
+        };
+    }
+
+    /**
+     * Get the visible pages within this book.
+     */
+    protected function getPages(bool $showDrafts = false): Collection
+    {
+        $query = Page::visible()->where('book_id', '=', $this->book->id);
+
+        if (!$showDrafts) {
+            $query->where('draft', '=', false);
+        }
+
+        return $query->get();
+    }
+
+    /**
+     * Sort the books content using the given map.
+     * The map is a single-dimension collection of objects in the following format:
+     *   {
+     *     +"id": "294" (ID of item)
+     *     +"sort": 1 (Sort order index)
+     *     +"parentChapter": false (ID of parent chapter, as string, or false)
+     *     +"type": "page" (Entity type of item)
+     *     +"book": "1" (Id of book to place item in)
+     *   }
+     *
+     * Returns a list of books that were involved in the operation.
+     * @throws SortOperationException
+     */
+    public function sortUsingMap(Collection $sortMap): Collection
+    {
+        // Load models into map
+        $this->loadModelsIntoSortMap($sortMap);
+        $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
+
+        // Perform the sort
+        $sortMap->each(function ($mapItem) {
+            $this->applySortUpdates($mapItem);
+        });
+
+        // Update permissions and activity.
+        $booksInvolved->each(function (Book $book) {
+            $book->rebuildPermissions();
+        });
+
+        return $booksInvolved;
+    }
+
+    /**
+     * Using the given sort map item, detect changes for the related model
+     * and update it if required.
+     */
+    protected function applySortUpdates(\stdClass $sortMapItem)
+    {
+        /** @var BookChild $model */
+        $model = $sortMapItem->model;
+
+        $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
+        $bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
+        $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
+
+        if ($bookChanged) {
+            $model->changeBook($sortMapItem->book);
+        }
+
+        if ($chapterChanged) {
+            $model->chapter_id = intval($sortMapItem->parentChapter);
+            $model->save();
+        }
+
+        if ($priorityChanged) {
+            $model->priority = intval($sortMapItem->sort);
+            $model->save();
+        }
+    }
+
+    /**
+     * Load models from the database into the given sort map.
+     */
+    protected function loadModelsIntoSortMap(Collection $sortMap): void
+    {
+        $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
+            return  $sortMapItem->type . ':' . $sortMapItem->id;
+        });
+        $pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
+        $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
+
+        $pages = Page::visible()->whereIn('id', $pageIds)->get();
+        $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
+
+        foreach ($pages as $page) {
+            $sortItem = $keyMap->get('page:' . $page->id);
+            $sortItem->model = $page;
+        }
+
+        foreach ($chapters as $chapter) {
+            $sortItem = $keyMap->get('chapter:' . $chapter->id);
+            $sortItem->model = $chapter;
+        }
+    }
+
+    /**
+     * Get the books involved in a sort.
+     * The given sort map should have its models loaded first.
+     * @throws SortOperationException
+     */
+    protected function getBooksInvolvedInSort(Collection $sortMap): Collection
+    {
+        $bookIdsInvolved = collect([$this->book->id]);
+        $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
+        $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
+        $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
+
+        $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
+
+        if (count($books) !== count($bookIdsInvolved)) {
+            throw new SortOperationException("Could not find all books requested in sort operation");
+        }
+
+        return $books;
+    }
+}
similarity index 53%
rename from app/Entities/EntityContextManager.php
rename to app/Entities/Managers/EntityContext.php
index 20be0de2b01a5e34fc61a6d0b2752b6d8c2ae903..551cd1a100c142f26cea88d8e86aaa282c768fa0 100644 (file)
@@ -1,44 +1,38 @@
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Managers;
 
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
 use Illuminate\Session\Store;
 
-class EntityContextManager
+class EntityContext
 {
     protected $session;
-    protected $entityRepo;
 
     protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
 
     /**
      * EntityContextManager constructor.
-     * @param Store $session
-     * @param EntityRepo $entityRepo
      */
-    public function __construct(Store $session, EntityRepo $entityRepo)
+    public function __construct(Store $session)
     {
         $this->session = $session;
-        $this->entityRepo = $entityRepo;
     }
 
     /**
      * Get the current bookshelf context for the given book.
-     * @param Book $book
-     * @return Bookshelf|null
      */
-    public function getContextualShelfForBook(Book $book)
+    public function getContextualShelfForBook(Book $book): ?Bookshelf
     {
         $contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
-        if (is_int($contextBookshelfId)) {
 
-            /** @var Bookshelf $shelf */
-            $shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
-
-            if ($shelf && $shelf->contains($book)) {
-                return $shelf;
-            }
+        if (!is_int($contextBookshelfId)) {
+            return null;
         }
-        return null;
+
+        $shelf = Bookshelf::visible()->find($contextBookshelfId);
+        $shelfContainsBook = $shelf && $shelf->contains($book);
+
+        return $shelfContainsBook ? $shelf : null;
     }
 
     /**
diff --git a/app/Entities/Managers/PageContent.php b/app/Entities/Managers/PageContent.php
new file mode 100644 (file)
index 0000000..36bc244
--- /dev/null
@@ -0,0 +1,304 @@
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Page;
+use DOMDocument;
+use DOMElement;
+use DOMNodeList;
+use DOMXPath;
+
+class PageContent
+{
+
+    protected $page;
+
+    /**
+     * PageContent constructor.
+     */
+    public function __construct(Page $page)
+    {
+        $this->page = $page;
+    }
+
+    /**
+     * Update the content of the page with new provided HTML.
+     */
+    public function setNewHTML(string $html)
+    {
+        $this->page->html = $this->formatHtml($html);
+        $this->page->text = $this->toPlainText();
+    }
+
+    /**
+     * Formats a page's html to be tagged correctly within the system.
+     */
+    protected function formatHtml(string $htmlText): string
+    {
+        if ($htmlText == '') {
+            return $htmlText;
+        }
+
+        libxml_use_internal_errors(true);
+        $doc = new DOMDocument();
+        $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
+
+        $container = $doc->documentElement;
+        $body = $container->childNodes->item(0);
+        $childNodes = $body->childNodes;
+
+        // Set ids on top-level nodes
+        $idMap = [];
+        foreach ($childNodes as $index => $childNode) {
+            $this->setUniqueId($childNode, $idMap);
+        }
+
+        // Ensure no duplicate ids within child items
+        $xPath = new DOMXPath($doc);
+        $idElems = $xPath->query('//body//*//*[@id]');
+        foreach ($idElems as $domElem) {
+            $this->setUniqueId($domElem, $idMap);
+        }
+
+        // Generate inner html as a string
+        $html = '';
+        foreach ($childNodes as $childNode) {
+            $html .= $doc->saveHTML($childNode);
+        }
+
+        return $html;
+    }
+
+    /**
+     * Set a unique id on the given DOMElement.
+     * A map for existing ID's should be passed in to check for current existence.
+     * @param DOMElement $element
+     * @param array $idMap
+     */
+    protected function setUniqueId($element, array &$idMap)
+    {
+        if (get_class($element) !== 'DOMElement') {
+            return;
+        }
+
+        // Overwrite id if not a BookStack custom id
+        $existingId = $element->getAttribute('id');
+        if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
+            $idMap[$existingId] = true;
+            return;
+        }
+
+        // Create an unique id for the element
+        // Uses the content as a basis to ensure output is the same every time
+        // the same content is passed through.
+        $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
+        $newId = urlencode($contentId);
+        $loopIndex = 0;
+
+        while (isset($idMap[$newId])) {
+            $newId = urlencode($contentId . '-' . $loopIndex);
+            $loopIndex++;
+        }
+
+        $element->setAttribute('id', $newId);
+        $idMap[$newId] = true;
+    }
+
+    /**
+     * Get a plain-text visualisation of this page.
+     */
+    protected function toPlainText(): string
+    {
+        $html = $this->render(true);
+        return strip_tags($html);
+    }
+
+    /**
+     * Render the page for viewing
+     */
+    public function render(bool $blankIncludes = false) : string
+    {
+        $content = $this->page->html;
+
+        if (!config('app.allow_content_scripts')) {
+            $content = $this->escapeScripts($content);
+        }
+
+        if ($blankIncludes) {
+            $content = $this->blankPageIncludes($content);
+        } else {
+            $content = $this->parsePageIncludes($content);
+        }
+
+        return $content;
+    }
+
+    /**
+     * Parse the headers on the page to get a navigation menu
+     */
+    public function getNavigation(string $htmlContent): array
+    {
+        if (empty($htmlContent)) {
+            return [];
+        }
+
+        libxml_use_internal_errors(true);
+        $doc = new DOMDocument();
+        $doc->loadHTML(mb_convert_encoding($htmlContent, 'HTML-ENTITIES', 'UTF-8'));
+        $xPath = new DOMXPath($doc);
+        $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
+
+        return $headers ? $this->headerNodesToLevelList($headers) : [];
+    }
+
+    /**
+     * Convert a DOMNodeList into an array of readable header attributes
+     * with levels normalised to the lower header level.
+     */
+    protected function headerNodesToLevelList(DOMNodeList $nodeList): array
+    {
+        $tree = collect($nodeList)->map(function ($header) {
+            $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
+            $text = mb_substr($text, 0, 100);
+
+            return [
+                'nodeName' => strtolower($header->nodeName),
+                'level' => intval(str_replace('h', '', $header->nodeName)),
+                'link' => '#' . $header->getAttribute('id'),
+                'text' => $text,
+            ];
+        })->filter(function ($header) {
+            return mb_strlen($header['text']) > 0;
+        });
+
+        // Shift headers if only smaller headers have been used
+        $levelChange = ($tree->pluck('level')->min() - 1);
+        $tree = $tree->map(function ($header) use ($levelChange) {
+            $header['level'] -= ($levelChange);
+            return $header;
+        });
+
+        return $tree->toArray();
+    }
+
+    /**
+     * Remove any page include tags within the given HTML.
+     */
+    protected function blankPageIncludes(string $html) : string
+    {
+        return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
+    }
+
+    /**
+     * Parse any include tags "{{@<page_id>#section}}" to be part of the page.
+     */
+    protected function parsePageIncludes(string $html) : string
+    {
+        $matches = [];
+        preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
+
+        foreach ($matches[1] as $index => $includeId) {
+            $fullMatch = $matches[0][$index];
+            $splitInclude = explode('#', $includeId, 2);
+
+            // Get page id from reference
+            $pageId = intval($splitInclude[0]);
+            if (is_nan($pageId)) {
+                continue;
+            }
+
+            // Find page and skip this if page not found
+            $matchedPage = Page::visible()->find($pageId);
+            if ($matchedPage === null) {
+                $html = str_replace($fullMatch, '', $html);
+                continue;
+            }
+
+            // If we only have page id, just insert all page html and continue.
+            if (count($splitInclude) === 1) {
+                $html = str_replace($fullMatch, $matchedPage->html, $html);
+                continue;
+            }
+
+            // Create and load HTML into a document
+            $innerContent = $this->fetchSectionOfPage($matchedPage, $splitInclude[1]);
+            $html = str_replace($fullMatch, trim($innerContent), $html);
+        }
+
+        return $html;
+    }
+
+
+    /**
+     * Fetch the content from a specific section of the given page.
+     */
+    protected function fetchSectionOfPage(Page $page, string $sectionId): string
+    {
+        $topLevelTags = ['table', 'ul', 'ol'];
+        $doc = new DOMDocument();
+        libxml_use_internal_errors(true);
+        $doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
+
+        // Search included content for the id given and blank out if not exists.
+        $matchingElem = $doc->getElementById($sectionId);
+        if ($matchingElem === null) {
+            return '';
+        }
+
+        // Otherwise replace the content with the found content
+        // Checks if the top-level wrapper should be included by matching on tag types
+        $innerContent = '';
+        $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
+        if ($isTopLevel) {
+            $innerContent .= $doc->saveHTML($matchingElem);
+        } else {
+            foreach ($matchingElem->childNodes as $childNode) {
+                $innerContent .= $doc->saveHTML($childNode);
+            }
+        }
+        libxml_clear_errors();
+
+        return $innerContent;
+    }
+
+    /**
+     * Escape script tags within HTML content.
+     */
+    protected function escapeScripts(string $html) : string
+    {
+        if (empty($html)) {
+            return $html;
+        }
+
+        libxml_use_internal_errors(true);
+        $doc = new DOMDocument();
+        $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
+        $xPath = new DOMXPath($doc);
+
+        // Remove standard script tags
+        $scriptElems = $xPath->query('//script');
+        foreach ($scriptElems as $scriptElem) {
+            $scriptElem->parentNode->removeChild($scriptElem);
+        }
+
+        // Remove data or JavaScript iFrames
+        $badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
+        foreach ($badIframes as $badIframe) {
+            $badIframe->parentNode->removeChild($badIframe);
+        }
+
+        // Remove 'on*' attributes
+        $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
+        foreach ($onAttributes as $attr) {
+            /** @var \DOMAttr $attr*/
+            $attrName = $attr->nodeName;
+            $attr->parentNode->removeAttribute($attrName);
+        }
+
+        $html = '';
+        $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
+        foreach ($topElems as $child) {
+            $html .= $doc->saveHTML($child);
+        }
+
+        return $html;
+    }
+}
diff --git a/app/Entities/Managers/PageEditActivity.php b/app/Entities/Managers/PageEditActivity.php
new file mode 100644 (file)
index 0000000..cebbf87
--- /dev/null
@@ -0,0 +1,74 @@
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Page;
+use BookStack\Entities\PageRevision;
+use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Builder;
+
+class PageEditActivity
+{
+
+    protected $page;
+
+    /**
+     * PageEditActivity constructor.
+     */
+    public function __construct(Page $page)
+    {
+        $this->page = $page;
+    }
+
+    /**
+     * Check if there's active editing being performed on this page.
+     * @return bool
+     */
+    public function hasActiveEditing(): bool
+    {
+        return $this->activePageEditingQuery(60)->count() > 0;
+    }
+
+    /**
+     * Get a notification message concerning the editing activity on the page.
+     */
+    public function activeEditingMessage(): string
+    {
+        $pageDraftEdits = $this->activePageEditingQuery(60)->get();
+        $count = $pageDraftEdits->count();
+
+        $userMessage = $count > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $count]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
+        $timeMessage = trans('entities.pages_draft_edit_active.time_b', ['minCount'=> 60]);
+        return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
+    }
+
+    /**
+     * Get the message to show when the user will be editing one of their drafts.
+     * @param PageRevision $draft
+     * @return string
+     */
+    public function getEditingActiveDraftMessage(PageRevision $draft): string
+    {
+        $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
+        if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
+            return $message;
+        }
+        return $message . "\n" . trans('entities.pages_draft_edited_notification');
+    }
+
+    /**
+     * A query to check for active update drafts on a particular page
+     * within the last given many minutes.
+     */
+    protected function activePageEditingQuery(int $withinMinutes): Builder
+    {
+        $checkTime = Carbon::now()->subMinutes($withinMinutes);
+        $query = PageRevision::query()
+            ->where('type', '=', 'update_draft')
+            ->where('page_id', '=', $this->page->id)
+            ->where('updated_at', '>', $this->page->updated_at)
+            ->where('created_by', '!=', user()->id)
+            ->where('updated_at', '>=', $checkTime)
+            ->with('createdBy');
+
+        return $query;
+    }
+}
diff --git a/app/Entities/Managers/TrashCan.php b/app/Entities/Managers/TrashCan.php
new file mode 100644 (file)
index 0000000..1a32294
--- /dev/null
@@ -0,0 +1,109 @@
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Entity;
+use BookStack\Entities\HasCoverImage;
+use BookStack\Entities\Page;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Facades\Activity;
+use BookStack\Uploads\AttachmentService;
+use BookStack\Uploads\ImageService;
+use Exception;
+use Illuminate\Contracts\Container\BindingResolutionException;
+
+class TrashCan
+{
+
+    /**
+     * Remove a bookshelf from the system.
+     * @throws Exception
+     */
+    public function destroyShelf(Bookshelf $shelf)
+    {
+        $this->destroyCommonRelations($shelf);
+        $shelf->delete();
+    }
+
+    /**
+     * Remove a book from the system.
+     * @throws NotifyException
+     * @throws BindingResolutionException
+     */
+    public function destroyBook(Book $book)
+    {
+        foreach ($book->pages as $page) {
+            $this->destroyPage($page);
+        }
+
+        foreach ($book->chapters as $chapter) {
+            $this->destroyChapter($chapter);
+        }
+
+        $this->destroyCommonRelations($book);
+        $book->delete();
+    }
+
+    /**
+     * Remove a page from the system.
+     * @throws NotifyException
+     */
+    public function destroyPage(Page $page)
+    {
+        // Check if set as custom homepage & remove setting if not used or throw error if active
+        $customHome = setting('app-homepage', '0:');
+        if (intval($page->id) === intval(explode(':', $customHome)[0])) {
+            if (setting('app-homepage-type') === 'page') {
+                throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
+            }
+            setting()->remove('app-homepage');
+        }
+
+        $this->destroyCommonRelations($page);
+
+        // Delete Attached Files
+        $attachmentService = app(AttachmentService::class);
+        foreach ($page->attachments as $attachment) {
+            $attachmentService->deleteFile($attachment);
+        }
+
+        $page->delete();
+    }
+
+    /**
+     * Remove a chapter from the system.
+     * @throws Exception
+     */
+    public function destroyChapter(Chapter $chapter)
+    {
+        if (count($chapter->pages) > 0) {
+            foreach ($chapter->pages as $page) {
+                $page->chapter_id = 0;
+                $page->save();
+            }
+        }
+
+        $this->destroyCommonRelations($chapter);
+        $chapter->delete();
+    }
+
+    /**
+     * Update entity relations to remove or update outstanding connections.
+     */
+    protected function destroyCommonRelations(Entity $entity)
+    {
+        Activity::removeEntity($entity);
+        $entity->views()->delete();
+        $entity->permissions()->delete();
+        $entity->tags()->delete();
+        $entity->comments()->delete();
+        $entity->jointPermissions()->delete();
+        $entity->searchTerms()->delete();
+
+        if ($entity instanceof HasCoverImage && $entity->cover) {
+            $imageService = app()->make(ImageService::class);
+            $imageService->destroy($entity->cover);
+        }
+    }
+}
index c32417418b7c0a2f0cd2e61f8b4856af17c476c5..76dc628fbf3f59e0bbbcf98897e715b743638213 100644 (file)
@@ -1,8 +1,25 @@
 <?php namespace BookStack\Entities;
 
 use BookStack\Uploads\Attachment;
-
-class Page extends Entity
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Permissions;
+
+/**
+ * Class Page
+ * @property int $chapter_id
+ * @property string $html
+ * @property string $markdown
+ * @property string $text
+ * @property bool $template
+ * @property bool $draft
+ * @property int $revision_count
+ * @property Chapter $chapter
+ * @property Collection $attachments
+ */
+class Page extends BookChild
 {
     protected $fillable = ['name', 'html', 'priority', 'markdown'];
 
@@ -11,12 +28,12 @@ class Page extends Entity
     public $textField = 'text';
 
     /**
-     * Get the morph class for this model.
-     * @return string
+     * Get the entities that are visible to the current user.
      */
-    public function getMorphClass()
+    public function scopeVisible(Builder $query)
     {
-        return 'BookStack\\Page';
+        $query = Permissions::enforceDraftVisiblityOnQuery($query);
+        return parent::scopeVisible($query);
     }
 
     /**
@@ -30,27 +47,17 @@ class Page extends Entity
         return $array;
     }
 
-    /**
-     * Get the book this page sits in.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
-     */
-    public function book()
-    {
-        return $this->belongsTo(Book::class);
-    }
-
     /**
      * Get the parent item
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
      */
-    public function parent()
+    public function parent(): Entity
     {
-        return $this->chapter_id ? $this->chapter() : $this->book();
+        return $this->chapter_id ? $this->chapter : $this->book;
     }
 
     /**
      * Get the chapter that this page is in, If applicable.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+     * @return BelongsTo
      */
     public function chapter()
     {
@@ -72,12 +79,12 @@ class Page extends Entity
      */
     public function revisions()
     {
-        return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
+        return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
     }
 
     /**
      * Get the attachments assigned to this page.
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     * @return HasMany
      */
     public function attachments()
     {
@@ -95,27 +102,17 @@ class Page extends Entity
         $midText = $this->draft ? '/draft/' : '/page/';
         $idComponent = $this->draft ? $this->id : urlencode($this->slug);
 
+        $url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
         if ($path !== false) {
-            return url('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
+            $url .= '/' . trim($path, '/');
         }
 
-        return url('/books/' . urlencode($bookSlug) . $midText . $idComponent);
-    }
-
-    /**
-     * Return a generalised, common raw query that can be 'unioned' across entities.
-     * @param bool $withContent
-     * @return string
-     */
-    public function entityRawQuery($withContent = false)
-    {
-        $htmlQuery = $withContent ? 'html' : "'' as html";
-        return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
+        return url($url);
     }
 
     /**
      * Get the current revision for the page if existing
-     * @return \BookStack\Entities\PageRevision|null
+     * @return PageRevision|null
      */
     public function getCurrentRevision()
     {
index d30147bfc52b1789f2faf4ecf89d8e3b50345c09..13dc713ba43be37453ca525f06e91f143793476e 100644 (file)
@@ -2,7 +2,21 @@
 
 use BookStack\Auth\User;
 use BookStack\Model;
+use Carbon\Carbon;
 
+/**
+ * Class PageRevision
+ * @property int $page_id
+ * @property string $slug
+ * @property string $book_slug
+ * @property int $created_by
+ * @property Carbon $created_at
+ * @property string $type
+ * @property string $summary
+ * @property string $markdown
+ * @property string $html
+ * @property int $revision_number
+ */
 class PageRevision extends Model
 {
     protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
@@ -41,13 +55,18 @@ class PageRevision extends Model
 
     /**
      * Get the previous revision for the same page if existing
-     * @return \BookStack\PageRevision|null
+     * @return \BookStack\Entities\PageRevision|null
      */
     public function getPrevious()
     {
-        if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
-            return static::find($id);
+        $id = static::newQuery()->where('page_id', '=', $this->page_id)
+            ->where('id', '<', $this->id)
+            ->max('id');
+
+        if ($id) {
+            return static::query()->find($id);
         }
+
         return null;
     }
 
diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php
new file mode 100644 (file)
index 0000000..78ce505
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+
+namespace BookStack\Entities\Repos;
+
+use BookStack\Actions\TagRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Entity;
+use BookStack\Entities\HasCoverImage;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Uploads\ImageRepo;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Collection;
+
+class BaseRepo
+{
+
+    protected $tagRepo;
+    protected $imageRepo;
+
+
+    /**
+     * BaseRepo constructor.
+     * @param $tagRepo
+     */
+    public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
+    {
+        $this->tagRepo = $tagRepo;
+        $this->imageRepo = $imageRepo;
+    }
+
+    /**
+     * Create a new entity in the system
+     */
+    public function create(Entity $entity, array $input)
+    {
+        $entity->fill($input);
+        $entity->forceFill([
+            'created_by' => user()->id,
+            'updated_by' => user()->id,
+        ]);
+        $entity->refreshSlug();
+        $entity->save();
+
+        if (isset($input['tags'])) {
+            $this->tagRepo->saveTagsToEntity($entity, $input['tags']);
+        }
+
+        $entity->rebuildPermissions();
+        $entity->indexForSearch();
+    }
+
+    /**
+     * Update the given entity.
+     */
+    public function update(Entity $entity, array $input)
+    {
+        $entity->fill($input);
+        $entity->updated_by = user()->id;
+
+        if ($entity->isDirty('name')) {
+            $entity->refreshSlug();
+        }
+
+        $entity->save();
+
+        if (isset($input['tags'])) {
+            $this->tagRepo->saveTagsToEntity($entity, $input['tags']);
+        }
+
+        $entity->rebuildPermissions();
+        $entity->indexForSearch();
+    }
+
+    /**
+     * Update the given items' cover image, or clear it.
+     * @throws ImageUploadException
+     * @throws \Exception
+     */
+    public function updateCoverImage(HasCoverImage $entity, UploadedFile $coverImage = null, bool $removeImage = false)
+    {
+        if ($coverImage) {
+            $this->imageRepo->destroyImage($entity->cover);
+            $image = $this->imageRepo->saveNew($coverImage, 'cover_book', $entity->id, 512, 512, true);
+            $entity->cover()->associate($image);
+        }
+
+        if ($removeImage) {
+            $this->imageRepo->destroyImage($entity->cover);
+            $entity->image_id = 0;
+            $entity->save();
+        }
+    }
+
+    /**
+     * Update the permissions of an entity.
+     */
+    public function updatePermissions(Entity $entity, bool $restricted, Collection $permissions = null)
+    {
+        $entity->restricted = $restricted;
+        $entity->permissions()->delete();
+
+        if (!is_null($permissions)) {
+            $entityPermissionData = $permissions->flatMap(function ($restrictions, $roleId) {
+                return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
+                    return [
+                        'role_id' => $roleId,
+                        'action' => strtolower($action),
+                    ] ;
+                });
+            });
+
+            $entity->permissions()->createMany($entityPermissionData);
+        }
+
+        $entity->save();
+        $entity->rebuildPermissions();
+    }
+}
diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php
new file mode 100644 (file)
index 0000000..7fcc80f
--- /dev/null
@@ -0,0 +1,134 @@
+<?php namespace BookStack\Entities\Repos;
+
+use BookStack\Actions\TagRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\TrashCan;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Uploads\ImageRepo;
+use Exception;
+use Illuminate\Contracts\Container\BindingResolutionException;
+use Illuminate\Contracts\Pagination\LengthAwarePaginator;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Collection;
+
+class BookRepo
+{
+
+    protected $baseRepo;
+    protected $tagRepo;
+    protected $imageRepo;
+
+    /**
+     * BookRepo constructor.
+     * @param $tagRepo
+     */
+    public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo)
+    {
+        $this->baseRepo = $baseRepo;
+        $this->tagRepo = $tagRepo;
+        $this->imageRepo = $imageRepo;
+    }
+
+    /**
+     * Get all books in a paginated format.
+     */
+    public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
+    {
+        return Book::visible()->orderBy($sort, $order)->paginate($count);
+    }
+
+    /**
+     * Get the books that were most recently viewed by this user.
+     */
+    public function getRecentlyViewed(int $count = 20): Collection
+    {
+        return Book::visible()->withLastView()
+            ->having('last_viewed_at', '>', 0)
+            ->orderBy('last_viewed_at', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get the most popular books in the system.
+     */
+    public function getPopular(int $count = 20): Collection
+    {
+        return Book::visible()->withViewCount()
+            ->having('view_count', '>', 0)
+            ->orderBy('view_count', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get the most recently created books from the system.
+     */
+    public function getRecentlyCreated(int $count = 20): Collection
+    {
+        return Book::visible()->orderBy('created_at', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get a book by its slug.
+     */
+    public function getBySlug(string $slug): Book
+    {
+        $book = Book::visible()->where('slug', '=', $slug)->first();
+
+        if ($book === null) {
+            throw new NotFoundException(trans('errors.book_not_found'));
+        }
+
+        return $book;
+    }
+
+    /**
+     * Create a new book in the system
+     */
+    public function create(array $input): Book
+    {
+        $book = new Book();
+        $this->baseRepo->create($book, $input);
+        return $book;
+    }
+
+    /**
+     * Update the given book.
+     */
+    public function update(Book $book, array $input): Book
+    {
+        $this->baseRepo->update($book, $input);
+        return $book;
+    }
+
+    /**
+     * Update the given book's cover image, or clear it.
+     * @throws ImageUploadException
+     * @throws Exception
+     */
+    public function updateCoverImage(Book $book, UploadedFile $coverImage = null, bool $removeImage = false)
+    {
+        $this->baseRepo->updateCoverImage($book, $coverImage, $removeImage);
+    }
+
+    /**
+     * Update the permissions of a book.
+     */
+    public function updatePermissions(Book $book, bool $restricted, Collection $permissions = null)
+    {
+        $this->baseRepo->updatePermissions($book, $restricted, $permissions);
+    }
+
+    /**
+     * Remove a book from the system.
+     * @throws NotifyException
+     * @throws BindingResolutionException
+     */
+    public function destroy(Book $book)
+    {
+        $trashCan = new TrashCan();
+        $trashCan->destroyBook($book);
+    }
+}
diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php
new file mode 100644 (file)
index 0000000..ab4a518
--- /dev/null
@@ -0,0 +1,173 @@
+<?php namespace BookStack\Entities\Repos;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Managers\TrashCan;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotFoundException;
+use Exception;
+use Illuminate\Contracts\Pagination\LengthAwarePaginator;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Collection;
+
+class BookshelfRepo
+{
+    protected $baseRepo;
+
+    /**
+     * BookshelfRepo constructor.
+     * @param $baseRepo
+     */
+    public function __construct(BaseRepo $baseRepo)
+    {
+        $this->baseRepo = $baseRepo;
+    }
+
+    /**
+     * Get all bookshelves in a paginated format.
+     */
+    public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
+    {
+        return Bookshelf::visible()->with('visibleBooks')
+            ->orderBy($sort, $order)->paginate($count);
+    }
+
+    /**
+     * Get the bookshelves that were most recently viewed by this user.
+     */
+    public function getRecentlyViewed(int $count = 20): Collection
+    {
+        return Bookshelf::visible()->withLastView()
+            ->having('last_viewed_at', '>', 0)
+            ->orderBy('last_viewed_at', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get the most popular bookshelves in the system.
+     */
+    public function getPopular(int $count = 20): Collection
+    {
+        return Bookshelf::visible()->withViewCount()
+            ->having('view_count', '>', 0)
+            ->orderBy('view_count', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get the most recently created bookshelves from the system.
+     */
+    public function getRecentlyCreated(int $count = 20): Collection
+    {
+        return Bookshelf::visible()->orderBy('created_at', 'desc')
+            ->take($count)->get();
+    }
+
+    /**
+     * Get a shelf by its slug.
+     */
+    public function getBySlug(string $slug): Bookshelf
+    {
+        $shelf = Bookshelf::visible()->where('slug', '=', $slug)->first();
+
+        if ($shelf === null) {
+            throw new NotFoundException(trans('errors.bookshelf_not_found'));
+        }
+
+        return $shelf;
+    }
+
+    /**
+     * Create a new shelf in the system.
+     */
+    public function create(array $input, array $bookIds): Bookshelf
+    {
+        $shelf = new Bookshelf();
+        $this->baseRepo->create($shelf, $input);
+        $this->updateBooks($shelf, $bookIds);
+        return $shelf;
+    }
+
+    /**
+     * Create a new shelf in the system.
+     */
+    public function update(Bookshelf $shelf, array $input, array $bookIds): Bookshelf
+    {
+        $this->baseRepo->update($shelf, $input);
+        $this->updateBooks($shelf, $bookIds);
+        return $shelf;
+    }
+
+    /**
+     * Update which books are assigned to this shelf by
+     * syncing the given book ids.
+     * Function ensures the books are visible to the current user and existing.
+     */
+    protected function updateBooks(Bookshelf $shelf, array $bookIds)
+    {
+        $numericIDs = collect($bookIds)->map(function ($id) {
+            return intval($id);
+        });
+
+        $syncData = Book::visible()
+            ->whereIn('id', $bookIds)
+            ->get(['id'])->pluck('id')->mapWithKeys(function ($bookId) use ($numericIDs) {
+                return [$bookId => ['order' => $numericIDs->search($bookId)]];
+            });
+
+        $shelf->books()->sync($syncData);
+    }
+
+    /**
+     * Update the given shelf cover image, or clear it.
+     * @throws ImageUploadException
+     * @throws Exception
+     */
+    public function updateCoverImage(Bookshelf $shelf, UploadedFile $coverImage = null, bool $removeImage = false)
+    {
+        $this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
+    }
+
+    /**
+     * Update the permissions of a bookshelf.
+     */
+    public function updatePermissions(Bookshelf $shelf, bool $restricted, Collection $permissions = null)
+    {
+        $this->baseRepo->updatePermissions($shelf, $restricted, $permissions);
+    }
+
+    /**
+     * Copy down the permissions of the given shelf to all child books.
+     */
+    public function copyDownPermissions(Bookshelf $shelf): int
+    {
+        $shelfPermissions = $shelf->permissions()->get(['role_id', 'action'])->toArray();
+        $shelfBooks = $shelf->books()->get();
+        $updatedBookCount = 0;
+
+        /** @var Book $book */
+        foreach ($shelfBooks as $book) {
+            if (!userCan('restrictions-manage', $book)) {
+                continue;
+            }
+            $book->permissions()->delete();
+            $book->restricted = $shelf->restricted;
+            $book->permissions()->createMany($shelfPermissions);
+            $book->save();
+            $book->rebuildPermissions();
+            $updatedBookCount++;
+        }
+
+        return $updatedBookCount;
+    }
+
+    /**
+     * Remove a bookshelf from the system.
+     * @throws Exception
+     */
+    public function destroy(Bookshelf $shelf)
+    {
+        $trashCan = new TrashCan();
+        $trashCan->destroyShelf($shelf);
+    }
+}
diff --git a/app/Entities/Repos/ChapterRepo.php b/app/Entities/Repos/ChapterRepo.php
new file mode 100644 (file)
index 0000000..c6f3a2d
--- /dev/null
@@ -0,0 +1,108 @@
+<?php namespace BookStack\Entities\Repos;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\TrashCan;
+use BookStack\Exceptions\MoveOperationException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\NotifyException;
+use Exception;
+use Illuminate\Contracts\Container\BindingResolutionException;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Support\Collection;
+
+class ChapterRepo
+{
+
+    protected $baseRepo;
+
+    /**
+     * ChapterRepo constructor.
+     * @param $baseRepo
+     */
+    public function __construct(BaseRepo $baseRepo)
+    {
+        $this->baseRepo = $baseRepo;
+    }
+
+    /**
+     * Get a chapter via the slug.
+     * @throws NotFoundException
+     */
+    public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
+    {
+        $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->first();
+
+        if ($chapter === null) {
+            throw new NotFoundException(trans('errors.chapter_not_found'));
+        }
+
+        return $chapter;
+    }
+
+    /**
+     * Create a new chapter in the system.
+     */
+    public function create(array $input, Book $parentBook): Chapter
+    {
+        $chapter = new Chapter();
+        $chapter->book_id = $parentBook->id;
+        $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
+        $this->baseRepo->create($chapter, $input);
+        return $chapter;
+    }
+
+    /**
+     * Update the given chapter.
+     */
+    public function update(Chapter $chapter, array $input): Chapter
+    {
+        $this->baseRepo->update($chapter, $input);
+        return $chapter;
+    }
+
+    /**
+     * Update the permissions of a chapter.
+     */
+    public function updatePermissions(Chapter $chapter, bool $restricted, Collection $permissions = null)
+    {
+        $this->baseRepo->updatePermissions($chapter, $restricted, $permissions);
+    }
+
+    /**
+     * Remove a chapter from the system.
+     * @throws Exception
+     */
+    public function destroy(Chapter $chapter)
+    {
+        $trashCan = new TrashCan();
+        $trashCan->destroyChapter($chapter);
+    }
+
+    /**
+     * Move the given chapter into a new parent book.
+     * The $parentIdentifier must be a string of the following format:
+     * 'book:<id>' (book:5)
+     * @throws MoveOperationException
+     */
+    public function move(Chapter $chapter, string $parentIdentifier): Book
+    {
+        $stringExploded = explode(':', $parentIdentifier);
+        $entityType = $stringExploded[0];
+        $entityId = intval($stringExploded[1]);
+
+        if ($entityType !== 'book') {
+            throw new MoveOperationException('Chapters can only be moved into books');
+        }
+
+        $parent = Book::visible()->where('id', '=', $entityId)->first();
+        if ($parent === null) {
+            throw new MoveOperationException('Book to move chapter into not found');
+        }
+
+        $chapter->changeBook($parent->id);
+        $chapter->rebuildPermissions();
+        return $parent;
+    }
+}
diff --git a/app/Entities/Repos/EntityRepo.php b/app/Entities/Repos/EntityRepo.php
deleted file mode 100644 (file)
index 996873b..0000000
+++ /dev/null
@@ -1,924 +0,0 @@
-<?php namespace BookStack\Entities\Repos;
-
-use Activity;
-use BookStack\Actions\TagRepo;
-use BookStack\Actions\ViewService;
-use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Auth\User;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
-use BookStack\Entities\EntityProvider;
-use BookStack\Entities\Page;
-use BookStack\Entities\SearchService;
-use BookStack\Exceptions\NotFoundException;
-use BookStack\Exceptions\NotifyException;
-use BookStack\Uploads\AttachmentService;
-use DOMDocument;
-use DOMNode;
-use DOMXPath;
-use Illuminate\Contracts\Pagination\LengthAwarePaginator;
-use Illuminate\Database\Eloquent\Builder;
-use Illuminate\Http\Request;
-use Illuminate\Support\Collection;
-use Throwable;
-
-class EntityRepo
-{
-
-    /**
-     * @var EntityProvider
-     */
-    protected $entityProvider;
-
-    /**
-     * @var PermissionService
-     */
-    protected $permissionService;
-
-    /**
-     * @var ViewService
-     */
-    protected $viewService;
-
-    /**
-     * @var TagRepo
-     */
-    protected $tagRepo;
-
-    /**
-     * @var SearchService
-     */
-    protected $searchService;
-
-    /**
-     * EntityRepo constructor.
-     * @param EntityProvider $entityProvider
-     * @param ViewService $viewService
-     * @param PermissionService $permissionService
-     * @param TagRepo $tagRepo
-     * @param SearchService $searchService
-     */
-    public function __construct(
-        EntityProvider $entityProvider,
-        ViewService $viewService,
-        PermissionService $permissionService,
-        TagRepo $tagRepo,
-        SearchService $searchService
-    ) {
-        $this->entityProvider = $entityProvider;
-        $this->viewService = $viewService;
-        $this->permissionService = $permissionService;
-        $this->tagRepo = $tagRepo;
-        $this->searchService = $searchService;
-    }
-
-    /**
-     * Base query for searching entities via permission system
-     * @param string $type
-     * @param bool $allowDrafts
-     * @param string $permission
-     * @return \Illuminate\Database\Query\Builder
-     */
-    protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
-    {
-        $q = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type), $permission);
-        if (strtolower($type) === 'page' && !$allowDrafts) {
-            $q = $q->where('draft', '=', false);
-        }
-        return $q;
-    }
-
-    /**
-     * Check if an entity with the given id exists.
-     * @param $type
-     * @param $id
-     * @return bool
-     */
-    public function exists($type, $id)
-    {
-        return $this->entityQuery($type)->where('id', '=', $id)->exists();
-    }
-
-    /**
-     * Get an entity by ID
-     * @param string $type
-     * @param integer $id
-     * @param bool $allowDrafts
-     * @param bool $ignorePermissions
-     * @return Entity
-     */
-    public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
-    {
-        $query = $this->entityQuery($type, $allowDrafts);
-
-        if ($ignorePermissions) {
-            $query = $this->entityProvider->get($type)->newQuery();
-        }
-
-        return $query->find($id);
-    }
-
-    /**
-     * @param string $type
-     * @param []int $ids
-     * @param bool $allowDrafts
-     * @param bool $ignorePermissions
-     * @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
-     */
-    public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
-    {
-        $query = $this->entityQuery($type, $allowDrafts);
-
-        if ($ignorePermissions) {
-            $query = $this->entityProvider->get($type)->newQuery();
-        }
-
-        return $query->whereIn('id', $ids)->get();
-    }
-
-    /**
-     * Get an entity by its url slug.
-     * @param string $type
-     * @param string $slug
-     * @param string|bool $bookSlug
-     * @return Entity
-     * @throws NotFoundException
-     */
-    public function getBySlug($type, $slug, $bookSlug = false)
-    {
-        $q = $this->entityQuery($type)->where('slug', '=', $slug);
-
-        if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
-            $q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
-                $query->select('id')
-                    ->from($this->entityProvider->book->getTable())
-                    ->where('slug', '=', $bookSlug)->limit(1);
-            });
-        }
-        $entity = $q->first();
-        if ($entity === null) {
-            throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
-        }
-        return $entity;
-    }
-
-
-    /**
-     * Get all entities of a type with the given permission, limited by count unless count is false.
-     * @param string $type
-     * @param integer|bool $count
-     * @param string $permission
-     * @return Collection
-     */
-    public function getAll($type, $count = 20, $permission = 'view')
-    {
-        $q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
-        if ($count !== false) {
-            $q = $q->take($count);
-        }
-        return $q->get();
-    }
-
-    /**
-     * Get all entities in a paginated format
-     * @param $type
-     * @param int $count
-     * @param string $sort
-     * @param string $order
-     * @param null|callable $queryAddition
-     * @return LengthAwarePaginator
-     */
-    public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
-    {
-        $query = $this->entityQuery($type);
-        $query = $this->addSortToQuery($query, $sort, $order);
-        if ($queryAddition) {
-            $queryAddition($query);
-        }
-        return $query->paginate($count);
-    }
-
-    /**
-     * Add sorting operations to an entity query.
-     * @param Builder $query
-     * @param string $sort
-     * @param string $order
-     * @return Builder
-     */
-    protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc')
-    {
-        $order = ($order === 'asc') ? 'asc' : 'desc';
-        $propertySorts = ['name', 'created_at', 'updated_at'];
-
-        if (in_array($sort, $propertySorts)) {
-            return $query->orderBy($sort, $order);
-        }
-
-        return $query;
-    }
-
-    /**
-     * Get the most recently created entities of the given type.
-     * @param string $type
-     * @param int $count
-     * @param int $page
-     * @param bool|callable $additionalQuery
-     * @return Collection
-     */
-    public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
-    {
-        $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
-            ->orderBy('created_at', 'desc');
-        if (strtolower($type) === 'page') {
-            $query = $query->where('draft', '=', false);
-        }
-        if ($additionalQuery !== false && is_callable($additionalQuery)) {
-            $additionalQuery($query);
-        }
-        return $query->skip($page * $count)->take($count)->get();
-    }
-
-    /**
-     * Get the most recently updated entities of the given type.
-     * @param string $type
-     * @param int $count
-     * @param int $page
-     * @param bool|callable $additionalQuery
-     * @return Collection
-     */
-    public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
-    {
-        $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
-            ->orderBy('updated_at', 'desc');
-        if (strtolower($type) === 'page') {
-            $query = $query->where('draft', '=', false);
-        }
-        if ($additionalQuery !== false && is_callable($additionalQuery)) {
-            $additionalQuery($query);
-        }
-        return $query->skip($page * $count)->take($count)->get();
-    }
-
-    /**
-     * Get the most recently viewed entities.
-     * @param string|bool $type
-     * @param int $count
-     * @param int $page
-     * @return mixed
-     */
-    public function getRecentlyViewed($type, $count = 10, $page = 0)
-    {
-        $filter = is_bool($type) ? false : $this->entityProvider->get($type);
-        return $this->viewService->getUserRecentlyViewed($count, $page, $filter);
-    }
-
-    /**
-     * Get the latest pages added to the system with pagination.
-     * @param string $type
-     * @param int $count
-     * @return mixed
-     */
-    public function getRecentlyCreatedPaginated($type, $count = 20)
-    {
-        return $this->entityQuery($type)->orderBy('created_at', 'desc')->paginate($count);
-    }
-
-    /**
-     * Get the latest pages added to the system with pagination.
-     * @param string $type
-     * @param int $count
-     * @return mixed
-     */
-    public function getRecentlyUpdatedPaginated($type, $count = 20)
-    {
-        return $this->entityQuery($type)->orderBy('updated_at', 'desc')->paginate($count);
-    }
-
-    /**
-     * Get the most popular entities base on all views.
-     * @param string $type
-     * @param int $count
-     * @param int $page
-     * @return mixed
-     */
-    public function getPopular(string $type, int $count = 10, int $page = 0)
-    {
-        return $this->viewService->getPopular($count, $page, $type);
-    }
-
-    /**
-     * Get draft pages owned by the current user.
-     * @param int $count
-     * @param int $page
-     * @return Collection
-     */
-    public function getUserDraftPages($count = 20, $page = 0)
-    {
-        return $this->entityProvider->page->where('draft', '=', true)
-            ->where('created_by', '=', user()->id)
-            ->orderBy('updated_at', 'desc')
-            ->skip($count * $page)->take($count)->get();
-    }
-
-    /**
-     * Get the number of entities the given user has created.
-     * @param string $type
-     * @param User $user
-     * @return int
-     */
-    public function getUserTotalCreated(string $type, User $user)
-    {
-        return $this->entityProvider->get($type)
-            ->where('created_by', '=', $user->id)->count();
-    }
-
-    /**
-     * Get the child items for a chapter sorted by priority but
-     * with draft items floated to the top.
-     * @param Bookshelf $bookshelf
-     * @return \Illuminate\Database\Eloquent\Collection|static[]
-     */
-    public function getBookshelfChildren(Bookshelf $bookshelf)
-    {
-        return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
-    }
-
-    /**
-     * Get the direct children of a book.
-     * @param Book $book
-     * @return \Illuminate\Database\Eloquent\Collection
-     */
-    public function getBookDirectChildren(Book $book)
-    {
-        $pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
-        $chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
-        return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
-    }
-
-    /**
-     * Get all child objects of a book.
-     * Returns a sorted collection of Pages and Chapters.
-     * Loads the book slug onto child elements to prevent access database access for getting the slug.
-     * @param Book $book
-     * @param bool $filterDrafts
-     * @param bool $renderPages
-     * @return mixed
-     */
-    public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
-    {
-        $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
-        $entities = [];
-        $parents = [];
-        $tree = [];
-
-        foreach ($q as $index => $rawEntity) {
-            if ($rawEntity->entity_type ===  $this->entityProvider->page->getMorphClass()) {
-                $entities[$index] = $this->entityProvider->page->newFromBuilder($rawEntity);
-                if ($renderPages) {
-                    $entities[$index]->html = $rawEntity->html;
-                    $entities[$index]->html = $this->renderPage($entities[$index]);
-                };
-            } else if ($rawEntity->entity_type === $this->entityProvider->chapter->getMorphClass()) {
-                $entities[$index] = $this->entityProvider->chapter->newFromBuilder($rawEntity);
-                $key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
-                $parents[$key] = $entities[$index];
-                $parents[$key]->setAttribute('pages', collect());
-            }
-            if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
-                $tree[] = $entities[$index];
-            }
-            $entities[$index]->book = $book;
-        }
-
-        foreach ($entities as $entity) {
-            if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
-                continue;
-            }
-            $parentKey = $this->entityProvider->chapter->getMorphClass() . ':' . $entity->chapter_id;
-            if (!isset($parents[$parentKey])) {
-                $tree[] = $entity;
-                continue;
-            }
-            $chapter = $parents[$parentKey];
-            $chapter->pages->push($entity);
-        }
-
-        return collect($tree);
-    }
-
-    /**
-     * Get the child items for a chapter sorted by priority but
-     * with draft items floated to the top.
-     * @param Chapter $chapter
-     * @return \Illuminate\Database\Eloquent\Collection|static[]
-     */
-    public function getChapterChildren(Chapter $chapter)
-    {
-        return $this->permissionService->enforceEntityRestrictions('page', $chapter->pages())
-            ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
-    }
-
-
-    /**
-     * Get the next sequential priority for a new child element in the given book.
-     * @param Book $book
-     * @return int
-     */
-    public function getNewBookPriority(Book $book)
-    {
-        $lastElem = $this->getBookChildren($book)->pop();
-        return $lastElem ? $lastElem->priority + 1 : 0;
-    }
-
-    /**
-     * Get a new priority for a new page to be added to the given chapter.
-     * @param Chapter $chapter
-     * @return int
-     */
-    public function getNewChapterPriority(Chapter $chapter)
-    {
-        $lastPage = $chapter->pages('DESC')->first();
-        return $lastPage !== null ? $lastPage->priority + 1 : 0;
-    }
-
-    /**
-     * Find a suitable slug for an entity.
-     * @param string $type
-     * @param string $name
-     * @param bool|integer $currentId
-     * @param bool|integer $bookId Only pass if type is not a book
-     * @return string
-     */
-    public function findSuitableSlug($type, $name, $currentId = false, $bookId = false)
-    {
-        $slug = $this->nameToSlug($name);
-        while ($this->slugExists($type, $slug, $currentId, $bookId)) {
-            $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
-        }
-        return $slug;
-    }
-
-    /**
-     * Check if a slug already exists in the database.
-     * @param string $type
-     * @param string $slug
-     * @param bool|integer $currentId
-     * @param bool|integer $bookId
-     * @return bool
-     */
-    protected function slugExists($type, $slug, $currentId = false, $bookId = false)
-    {
-        $query = $this->entityProvider->get($type)->where('slug', '=', $slug);
-        if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
-            $query = $query->where('book_id', '=', $bookId);
-        }
-        if ($currentId) {
-            $query = $query->where('id', '!=', $currentId);
-        }
-        return $query->count() > 0;
-    }
-
-    /**
-     * Updates entity restrictions from a request
-     * @param Request $request
-     * @param Entity $entity
-     * @throws Throwable
-     */
-    public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
-    {
-        $entity->restricted = $request->get('restricted', '') === 'true';
-        $entity->permissions()->delete();
-
-        if ($request->filled('restrictions')) {
-            foreach ($request->get('restrictions') as $roleId => $restrictions) {
-                foreach ($restrictions as $action => $value) {
-                    $entity->permissions()->create([
-                        'role_id' => $roleId,
-                        'action'  => strtolower($action)
-                    ]);
-                }
-            }
-        }
-
-        $entity->save();
-        $this->permissionService->buildJointPermissionsForEntity($entity);
-    }
-
-
-
-    /**
-     * Create a new entity from request input.
-     * Used for books and chapters.
-     * @param string $type
-     * @param array $input
-     * @param bool|Book $book
-     * @return Entity
-     */
-    public function createFromInput($type, $input = [], $book = false)
-    {
-        $isChapter = strtolower($type) === 'chapter';
-        $entityModel = $this->entityProvider->get($type)->newInstance($input);
-        $entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
-        $entityModel->created_by = user()->id;
-        $entityModel->updated_by = user()->id;
-        $isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
-
-        if (isset($input['tags'])) {
-            $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
-        }
-
-        $this->permissionService->buildJointPermissionsForEntity($entityModel);
-        $this->searchService->indexEntity($entityModel);
-        return $entityModel;
-    }
-
-    /**
-     * Update entity details from request input.
-     * Used for books and chapters
-     * @param string $type
-     * @param Entity $entityModel
-     * @param array $input
-     * @return Entity
-     */
-    public function updateFromInput($type, Entity $entityModel, $input = [])
-    {
-        if ($entityModel->name !== $input['name']) {
-            $entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id);
-        }
-        $entityModel->fill($input);
-        $entityModel->updated_by = user()->id;
-        $entityModel->save();
-
-        if (isset($input['tags'])) {
-            $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
-        }
-
-        $this->permissionService->buildJointPermissionsForEntity($entityModel);
-        $this->searchService->indexEntity($entityModel);
-        return $entityModel;
-    }
-
-    /**
-     * Sync the books assigned to a shelf from a comma-separated list
-     * of book IDs.
-     * @param Bookshelf $shelf
-     * @param string $books
-     */
-    public function updateShelfBooks(Bookshelf $shelf, string $books)
-    {
-        $ids = explode(',', $books);
-
-        // Check books exist and match ordering
-        $bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
-        $syncData = [];
-        foreach ($ids as $index => $id) {
-            if ($bookIds->contains($id)) {
-                $syncData[$id] = ['order' => $index];
-            }
-        }
-
-        $shelf->books()->sync($syncData);
-    }
-
-    /**
-     * Append a Book to a BookShelf.
-     * @param Bookshelf $shelf
-     * @param Book $book
-     */
-    public function appendBookToShelf(Bookshelf $shelf, Book $book)
-    {
-        if ($shelf->contains($book)) {
-            return;
-        }
-
-        $maxOrder = $shelf->books()->max('order');
-        $shelf->books()->attach($book->id, ['order' => $maxOrder + 1]);
-    }
-
-    /**
-     * Change the book that an entity belongs to.
-     * @param string $type
-     * @param integer $newBookId
-     * @param Entity $entity
-     * @param bool $rebuildPermissions
-     * @return Entity
-     */
-    public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
-    {
-        $entity->book_id = $newBookId;
-        // Update related activity
-        foreach ($entity->activity as $activity) {
-            $activity->book_id = $newBookId;
-            $activity->save();
-        }
-        $entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId);
-        $entity->save();
-
-        // Update all child pages if a chapter
-        if (strtolower($type) === 'chapter') {
-            foreach ($entity->pages as $page) {
-                $this->changeBook('page', $newBookId, $page, false);
-            }
-        }
-
-        // Update permissions if applicable
-        if ($rebuildPermissions) {
-            $entity->load('book');
-            $this->permissionService->buildJointPermissionsForEntity($entity->book);
-        }
-
-        return $entity;
-    }
-
-    /**
-     * Alias method to update the book jointPermissions in the PermissionService.
-     * @param Book $book
-     */
-    public function buildJointPermissionsForBook(Book $book)
-    {
-        $this->permissionService->buildJointPermissionsForEntity($book);
-    }
-
-    /**
-     * Format a name as a url slug.
-     * @param $name
-     * @return string
-     */
-    protected function nameToSlug($name)
-    {
-        $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
-        $slug = preg_replace('/\s{2,}/', ' ', $slug);
-        $slug = str_replace(' ', '-', $slug);
-        if ($slug === "") {
-            $slug = substr(md5(rand(1, 500)), 0, 5);
-        }
-        return $slug;
-    }
-
-    /**
-     * Render the page for viewing
-     * @param Page $page
-     * @param bool $blankIncludes
-     * @return string
-     */
-    public function renderPage(Page $page, bool $blankIncludes = false) : string
-    {
-        $content = $page->html;
-
-        if (!config('app.allow_content_scripts')) {
-            $content = $this->escapeScripts($content);
-        }
-
-        if ($blankIncludes) {
-            $content = $this->blankPageIncludes($content);
-        } else {
-            $content = $this->parsePageIncludes($content);
-        }
-
-        return $content;
-    }
-
-    /**
-     * Remove any page include tags within the given HTML.
-     * @param string $html
-     * @return string
-     */
-    protected function blankPageIncludes(string $html) : string
-    {
-        return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
-    }
-
-    /**
-     * Parse any include tags "{{@<page_id>#section}}" to be part of the page.
-     * @param string $html
-     * @return mixed|string
-     */
-    protected function parsePageIncludes(string $html) : string
-    {
-        $matches = [];
-        preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
-
-        $topLevelTags = ['table', 'ul', 'ol'];
-        foreach ($matches[1] as $index => $includeId) {
-            $splitInclude = explode('#', $includeId, 2);
-            $pageId = intval($splitInclude[0]);
-            if (is_nan($pageId)) {
-                continue;
-            }
-
-            $matchedPage = $this->getById('page', $pageId);
-            if ($matchedPage === null) {
-                $html = str_replace($matches[0][$index], '', $html);
-                continue;
-            }
-
-            if (count($splitInclude) === 1) {
-                $html = str_replace($matches[0][$index], $matchedPage->html, $html);
-                continue;
-            }
-
-            $doc = new DOMDocument();
-            libxml_use_internal_errors(true);
-            $doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
-            $matchingElem = $doc->getElementById($splitInclude[1]);
-            if ($matchingElem === null) {
-                $html = str_replace($matches[0][$index], '', $html);
-                continue;
-            }
-            $innerContent = '';
-            $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
-            if ($isTopLevel) {
-                $innerContent .= $doc->saveHTML($matchingElem);
-            } else {
-                foreach ($matchingElem->childNodes as $childNode) {
-                    $innerContent .= $doc->saveHTML($childNode);
-                }
-            }
-            libxml_clear_errors();
-            $html = str_replace($matches[0][$index], trim($innerContent), $html);
-        }
-
-        return $html;
-    }
-
-    /**
-     * Escape script tags within HTML content.
-     * @param string $html
-     * @return string
-     */
-    protected function escapeScripts(string $html) : string
-    {
-        if ($html == '') {
-            return $html;
-        }
-
-        libxml_use_internal_errors(true);
-        $doc = new DOMDocument();
-        $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
-        $xPath = new DOMXPath($doc);
-
-        // Remove standard script tags
-        $scriptElems = $xPath->query('//script');
-        foreach ($scriptElems as $scriptElem) {
-            $scriptElem->parentNode->removeChild($scriptElem);
-        }
-
-        // Remove data or JavaScript iFrames
-        $badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
-        foreach ($badIframes as $badIframe) {
-            $badIframe->parentNode->removeChild($badIframe);
-        }
-
-        // Remove 'on*' attributes
-        $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
-        foreach ($onAttributes as $attr) {
-            /** @var \DOMAttr $attr*/
-            $attrName = $attr->nodeName;
-            $attr->parentNode->removeAttribute($attrName);
-        }
-
-        $html = '';
-        $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
-        foreach ($topElems as $child) {
-            $html .= $doc->saveHTML($child);
-        }
-
-        return $html;
-    }
-
-    /**
-     * Search for image usage within page content.
-     * @param $imageString
-     * @return mixed
-     */
-    public function searchForImage($imageString)
-    {
-        $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
-        foreach ($pages as $page) {
-            $page->url = $page->getUrl();
-            $page->html = '';
-            $page->text = '';
-        }
-        return count($pages) > 0 ? $pages : false;
-    }
-
-    /**
-     * Destroy a bookshelf instance
-     * @param Bookshelf $shelf
-     * @throws Throwable
-     */
-    public function destroyBookshelf(Bookshelf $shelf)
-    {
-        $this->destroyEntityCommonRelations($shelf);
-        $shelf->delete();
-    }
-
-    /**
-     * Destroy the provided book and all its child entities.
-     * @param Book $book
-     * @throws NotifyException
-     * @throws Throwable
-     */
-    public function destroyBook(Book $book)
-    {
-        foreach ($book->pages as $page) {
-            $this->destroyPage($page);
-        }
-        foreach ($book->chapters as $chapter) {
-            $this->destroyChapter($chapter);
-        }
-        $this->destroyEntityCommonRelations($book);
-        $book->delete();
-    }
-
-    /**
-     * Destroy a chapter and its relations.
-     * @param Chapter $chapter
-     * @throws Throwable
-     */
-    public function destroyChapter(Chapter $chapter)
-    {
-        if (count($chapter->pages) > 0) {
-            foreach ($chapter->pages as $page) {
-                $page->chapter_id = 0;
-                $page->save();
-            }
-        }
-        $this->destroyEntityCommonRelations($chapter);
-        $chapter->delete();
-    }
-
-    /**
-     * Destroy a given page along with its dependencies.
-     * @param Page $page
-     * @throws NotifyException
-     * @throws Throwable
-     */
-    public function destroyPage(Page $page)
-    {
-        // Check if set as custom homepage & remove setting if not used or throw error if active
-        $customHome = setting('app-homepage', '0:');
-        if (intval($page->id) === intval(explode(':', $customHome)[0])) {
-            if (setting('app-homepage-type') === 'page') {
-                throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
-            }
-            setting()->remove('app-homepage');
-        }
-
-        $this->destroyEntityCommonRelations($page);
-
-        // Delete Attached Files
-        $attachmentService = app(AttachmentService::class);
-        foreach ($page->attachments as $attachment) {
-            $attachmentService->deleteFile($attachment);
-        }
-
-        $page->delete();
-    }
-
-    /**
-     * Destroy or handle the common relations connected to an entity.
-     * @param Entity $entity
-     * @throws Throwable
-     */
-    protected function destroyEntityCommonRelations(Entity $entity)
-    {
-        Activity::removeEntity($entity);
-        $entity->views()->delete();
-        $entity->permissions()->delete();
-        $entity->tags()->delete();
-        $entity->comments()->delete();
-        $this->permissionService->deleteJointPermissionsForEntity($entity);
-        $this->searchService->deleteEntityTerms($entity);
-    }
-
-    /**
-     * Copy the permissions of a bookshelf to all child books.
-     * Returns the number of books that had permissions updated.
-     * @param Bookshelf $bookshelf
-     * @return int
-     * @throws Throwable
-     */
-    public function copyBookshelfPermissions(Bookshelf $bookshelf)
-    {
-        $shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
-        $shelfBooks = $bookshelf->books()->get();
-        $updatedBookCount = 0;
-
-        foreach ($shelfBooks as $book) {
-            if (!userCan('restrictions-manage', $book)) {
-                continue;
-            }
-            $book->permissions()->delete();
-            $book->restricted = $bookshelf->restricted;
-            $book->permissions()->createMany($shelfPermissions);
-            $book->save();
-            $this->permissionService->buildJointPermissionsForEntity($book);
-            $updatedBookCount++;
-        }
-
-        return $updatedBookCount;
-    }
-}
index ed142eb611b53f3d715722ded3bf12bdb2e4455f..0fc68f95345189d6767087f04db9ec5ace2bb673 100644 (file)
 use BookStack\Entities\Book;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Entity;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Managers\TrashCan;
 use BookStack\Entities\Page;
 use BookStack\Entities\PageRevision;
-use Carbon\Carbon;
-use DOMDocument;
-use DOMElement;
-use DOMXPath;
+use BookStack\Exceptions\MoveOperationException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Exceptions\PermissionsException;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Collection;
 
-class PageRepo extends EntityRepo
+class PageRepo
 {
 
+    protected $baseRepo;
+
     /**
-     * Get page by slug.
-     * @param string $pageSlug
-     * @param string $bookSlug
-     * @return Page
-     * @throws \BookStack\Exceptions\NotFoundException
+     * PageRepo constructor.
      */
-    public function getPageBySlug(string $pageSlug, string $bookSlug)
+    public function __construct(BaseRepo $baseRepo)
     {
-        return $this->getBySlug('page', $pageSlug, $bookSlug);
+        $this->baseRepo = $baseRepo;
     }
 
     /**
-     * Search through page revisions and retrieve the last page in the
-     * current book that has a slug equal to the one given.
-     * @param string $pageSlug
-     * @param string $bookSlug
-     * @return null|Page
+     * Get a page by ID.
+     * @throws NotFoundException
      */
-    public function getPageByOldSlug(string $pageSlug, string $bookSlug)
+    public function getById(int $id): Page
     {
-        $revision = $this->entityProvider->pageRevision->where('slug', '=', $pageSlug)
-            ->whereHas('page', function ($query) {
-                $this->permissionService->enforceEntityRestrictions('page', $query);
+        $page = Page::visible()->with(['book'])->find($id);
+
+        if (!$page) {
+            throw new NotFoundException(trans('errors.page_not_found'));
+        }
+
+        return $page;
+    }
+
+    /**
+     * Get a page its book and own slug.
+     * @throws NotFoundException
+     */
+    public function getBySlug(string $bookSlug, string $pageSlug): Page
+    {
+        $page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->first();
+
+        if (!$page) {
+            throw new NotFoundException(trans('errors.page_not_found'));
+        }
+
+        return $page;
+    }
+
+    /**
+     * Get a page by its old slug but checking the revisions table
+     * for the last revision that matched the given page and book slug.
+     */
+    public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
+    {
+        $revision = PageRevision::query()
+            ->whereHas('page', function (Builder $query) {
+                $query->visible();
             })
+            ->where('slug', '=', $pageSlug)
             ->where('type', '=', 'version')
             ->where('book_slug', '=', $bookSlug)
             ->orderBy('created_at', 'desc')
-            ->with('page')->first();
-        return $revision !== null ? $revision->page : null;
+            ->with('page')
+            ->first();
+        return $revision ? $revision->page : null;
     }
 
     /**
-     * Updates a page with any fillable data and saves it into the database.
-     * @param Page $page
-     * @param int $book_id
-     * @param array $input
-     * @return Page
-     * @throws \Exception
+     * Get pages that have been marked as a template.
      */
-    public function updatePage(Page $page, int $book_id, array $input)
+    public function getTemplates(int $count = 10, int $page = 1, string $search = ''): LengthAwarePaginator
     {
-        // Hold the old details to compare later
-        $oldHtml = $page->html;
-        $oldName = $page->name;
+        $query = Page::visible()
+            ->where('template', '=', true)
+            ->orderBy('name', 'asc')
+            ->skip(($page - 1) * $count)
+            ->take($count);
+
+        if ($search) {
+            $query->where('name', 'like', '%' . $search . '%');
+        }
 
-        // Prevent slug being updated if no name change
-        if ($page->name !== $input['name']) {
-            $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id);
+        $paginator = $query->paginate($count, ['*'], 'page', $page);
+        $paginator->withPath('/templates');
+
+        return $paginator;
+    }
+
+    /**
+     * Get a parent item via slugs.
+     */
+    public function getParentFromSlugs(string $bookSlug, string $chapterSlug = null): Entity
+    {
+        if ($chapterSlug !== null) {
+            return $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
         }
 
-        // Save page tags if present
-        if (isset($input['tags'])) {
-            $this->tagRepo->saveTagsToEntity($page, $input['tags']);
+        return Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
+    }
+
+    /**
+     * Get the draft copy of the given page for the current user.
+     */
+    public function getUserDraft(Page $page): ?PageRevision
+    {
+        $revision = $this->getUserDraftQuery($page)->first();
+        return $revision;
+    }
+
+    /**
+     * Get a new draft page belonging to the given parent entity.
+     */
+    public function getNewDraftPage(Entity $parent)
+    {
+        $page = (new Page())->forceFill([
+            'name' => trans('entities.pages_initial_name'),
+            'created_by' => user()->id,
+            'updated_by' => user()->id,
+            'draft' => true,
+        ]);
+
+        if ($parent instanceof Chapter) {
+            $page->chapter_id = $parent->id;
+            $page->book_id = $parent->book_id;
+        } else {
+            $page->book_id = $parent->id;
         }
 
+        $page->save();
+        $page->refresh()->rebuildPermissions();
+        return $page;
+    }
+
+    /**
+     * Publish a draft page to make it a live, non-draft page.
+     */
+    public function publishDraft(Page $draft, array $input): Page
+    {
+        $this->baseRepo->update($draft, $input);
+        if (isset($input['template']) && userCan('templates-manage')) {
+            $draft->template = ($input['template'] === 'true');
+        }
+
+        $pageContent = new PageContent($draft);
+        $pageContent->setNewHTML($input['html']);
+        $draft->draft = false;
+        $draft->revision_count = 1;
+        $draft->priority = $this->getNewPriority($draft);
+        $draft->refreshSlug();
+        $draft->save();
+
+        $this->savePageRevision($draft, trans('entities.pages_initial_revision'));
+        $draft->indexForSearch();
+        return $draft->refresh();
+    }
+
+    /**
+     * Update a page in the system.
+     */
+    public function update(Page $page, array $input): Page
+    {
+        // Hold the old details to compare later
+        $oldHtml = $page->html;
+        $oldName = $page->name;
+
         if (isset($input['template']) && userCan('templates-manage')) {
             $page->template = ($input['template'] === 'true');
         }
 
+        $this->baseRepo->update($page, $input);
+
         // Update with new details
-        $userId = user()->id;
         $page->fill($input);
-        $page->html = $this->formatHtml($input['html']);
-        $page->text = $this->pageToPlainText($page);
+        $pageContent = new PageContent($page);
+        $pageContent->setNewHTML($input['html']);
+        $page->revision_count++;
+
         if (setting('app-editor') !== 'markdown') {
             $page->markdown = '';
         }
-        $page->updated_by = $userId;
-        $page->revision_count++;
+
         $page->save();
 
         // Remove all update drafts for this user & page.
-        $this->userUpdatePageDraftsQuery($page, $userId)->delete();
+        $this->getUserDraftQuery($page)->delete();
 
         // Save a revision after updating
         $summary = $input['summary'] ?? null;
@@ -95,24 +203,20 @@ class PageRepo extends EntityRepo
             $this->savePageRevision($page, $summary);
         }
 
-        $this->searchService->indexEntity($page);
-
         return $page;
     }
 
     /**
      * Saves a page revision into the system.
-     * @param Page $page
-     * @param null|string $summary
-     * @return PageRevision
-     * @throws \Exception
      */
-    public function savePageRevision(Page $page, string $summary = null)
+    protected function savePageRevision(Page $page, string $summary = null)
     {
-        $revision = $this->entityProvider->pageRevision->newInstance($page->toArray());
+        $revision = new PageRevision($page->toArray());
+
         if (setting('app-editor') !== 'markdown') {
             $revision->markdown = '';
         }
+
         $revision->page_id = $page->id;
         $revision->slug = $page->slug;
         $revision->book_slug = $page->book->slug;
@@ -123,439 +227,233 @@ class PageRepo extends EntityRepo
         $revision->revision_number = $page->revision_count;
         $revision->save();
 
-        $revisionLimit = config('app.revision_limit');
-        if ($revisionLimit !== false) {
-            $revisionsToDelete = $this->entityProvider->pageRevision->where('page_id', '=', $page->id)
-                ->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']);
-            if ($revisionsToDelete->count() > 0) {
-                $this->entityProvider->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
-            }
-        }
-
+        $this->deleteOldRevisions($page);
         return $revision;
     }
 
     /**
-     * Formats a page's html to be tagged correctly within the system.
-     * @param string $htmlText
-     * @return string
+     * Save a page update draft.
      */
-    protected function formatHtml(string $htmlText)
+    public function updatePageDraft(Page $page, array $input)
     {
-        if ($htmlText == '') {
-            return $htmlText;
-        }
-
-        libxml_use_internal_errors(true);
-        $doc = new DOMDocument();
-        $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
-
-        $container = $doc->documentElement;
-        $body = $container->childNodes->item(0);
-        $childNodes = $body->childNodes;
-
-        // Set ids on top-level nodes
-        $idMap = [];
-        foreach ($childNodes as $index => $childNode) {
-            $this->setUniqueId($childNode, $idMap);
-        }
-
-        // Ensure no duplicate ids within child items
-        $xPath = new DOMXPath($doc);
-        $idElems = $xPath->query('//body//*//*[@id]');
-        foreach ($idElems as $domElem) {
-            $this->setUniqueId($domElem, $idMap);
+        // If the page itself is a draft simply update that
+        if ($page->draft) {
+            $page->fill($input);
+            if (isset($input['html'])) {
+                $content = new PageContent($page);
+                $content->setNewHTML($input['html']);
+            }
+            $page->save();
+            return $page;
         }
 
-        // Generate inner html as a string
-        $html = '';
-        foreach ($childNodes as $childNode) {
-            $html .= $doc->saveHTML($childNode);
+        // Otherwise save the data to a revision
+        $draft = $this->getPageRevisionToUpdate($page);
+        $draft->fill($input);
+        if (setting('app-editor') !== 'markdown') {
+            $draft->markdown = '';
         }
 
-        return $html;
+        $draft->save();
+        return $draft;
     }
 
     /**
-     * Set a unique id on the given DOMElement.
-     * A map for existing ID's should be passed in to check for current existence.
-     * @param DOMElement $element
-     * @param array $idMap
+     * Destroy a page from the system.
+     * @throws NotifyException
      */
-    protected function setUniqueId($element, array &$idMap)
+    public function destroy(Page $page)
     {
-        if (get_class($element) !== 'DOMElement') {
-            return;
-        }
-
-        // Overwrite id if not a BookStack custom id
-        $existingId = $element->getAttribute('id');
-        if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
-            $idMap[$existingId] = true;
-            return;
-        }
-
-        // Create an unique id for the element
-        // Uses the content as a basis to ensure output is the same every time
-        // the same content is passed through.
-        $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
-        $newId = urlencode($contentId);
-        $loopIndex = 0;
-
-        while (isset($idMap[$newId])) {
-            $newId = urlencode($contentId . '-' . $loopIndex);
-            $loopIndex++;
-        }
-
-        $element->setAttribute('id', $newId);
-        $idMap[$newId] = true;
+        $trashCan = new TrashCan();
+        $trashCan->destroyPage($page);
     }
 
     /**
-     * Get the plain text version of a page's content.
-     * @param \BookStack\Entities\Page $page
-     * @return string
+     * Restores a revision's content back into a page.
      */
-    protected function pageToPlainText(Page $page) : string
+    public function restoreRevision(Page $page, int $revisionId): Page
     {
-        $html = $this->renderPage($page, true);
-        return strip_tags($html);
-    }
+        $page->revision_count++;
+        $this->savePageRevision($page);
 
-    /**
-     * Get a new draft page instance.
-     * @param Book $book
-     * @param Chapter|null $chapter
-     * @return \BookStack\Entities\Page
-     * @throws \Throwable
-     */
-    public function getDraftPage(Book $book, Chapter $chapter = null)
-    {
-        $page = $this->entityProvider->page->newInstance();
-        $page->name = trans('entities.pages_initial_name');
-        $page->created_by = user()->id;
+        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+        $page->fill($revision->toArray());
+        $content = new PageContent($page);
+        $content->setNewHTML($page->html);
         $page->updated_by = user()->id;
-        $page->draft = true;
-
-        if ($chapter) {
-            $page->chapter_id = $chapter->id;
-        }
+        $page->refreshSlug();
+        $page->save();
 
-        $book->pages()->save($page);
-        $page = $this->entityProvider->page->find($page->id);
-        $this->permissionService->buildJointPermissionsForEntity($page);
+        $page->indexForSearch();
         return $page;
     }
 
     /**
-     * Save a page update draft.
-     * @param Page $page
-     * @param array $data
-     * @return PageRevision|Page
+     * Move the given page into a new parent book or chapter.
+     * The $parentIdentifier must be a string of the following format:
+     * 'book:<id>' (book:5)
+     * @throws MoveOperationException
+     * @throws PermissionsException
      */
-    public function updatePageDraft(Page $page, array $data = [])
+    public function move(Page $page, string $parentIdentifier): Book
     {
-        // If the page itself is a draft simply update that
-        if ($page->draft) {
-            $page->fill($data);
-            if (isset($data['html'])) {
-                $page->text = $this->pageToPlainText($page);
-            }
-            $page->save();
-            return $page;
+        $parent = $this->findParentByIdentifier($parentIdentifier);
+        if ($parent === null) {
+            throw new MoveOperationException('Book or chapter to move page into not found');
         }
 
-        // Otherwise save the data to a revision
-        $userId = user()->id;
-        $drafts = $this->userUpdatePageDraftsQuery($page, $userId)->get();
-
-        if ($drafts->count() > 0) {
-            $draft = $drafts->first();
-        } else {
-            $draft = $this->entityProvider->pageRevision->newInstance();
-            $draft->page_id = $page->id;
-            $draft->slug = $page->slug;
-            $draft->book_slug = $page->book->slug;
-            $draft->created_by = $userId;
-            $draft->type = 'update_draft';
+        if (!userCan('page-create', $parent)) {
+            throw new PermissionsException('User does not have permission to create a page within the new parent');
         }
 
-        $draft->fill($data);
-        if (setting('app-editor') !== 'markdown') {
-            $draft->markdown = '';
-        }
-
-        $draft->save();
-        return $draft;
+        $page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
+        $page->rebuildPermissions();
+        return $parent;
     }
 
     /**
-     * Publish a draft page to make it a normal page.
-     * Sets the slug and updates the content.
-     * @param Page $draftPage
-     * @param array $input
-     * @return Page
-     * @throws \Exception
+     * Copy an existing page in the system.
+     * Optionally providing a new parent via string identifier and a new name.
+     * @throws MoveOperationException
+     * @throws PermissionsException
      */
-    public function publishPageDraft(Page $draftPage, array $input)
+    public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
     {
-        $draftPage->fill($input);
-
-        // Save page tags if present
-        if (isset($input['tags'])) {
-            $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
+        $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
+        if ($parent === null) {
+            throw new MoveOperationException('Book or chapter to move page into not found');
         }
 
-        if (isset($input['template']) && userCan('templates-manage')) {
-            $draftPage->template = ($input['template'] === 'true');
+        if (!userCan('page-create', $parent)) {
+            throw new PermissionsException('User does not have permission to create a page within the new parent');
         }
 
-        $draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
-        $draftPage->html = $this->formatHtml($input['html']);
-        $draftPage->text = $this->pageToPlainText($draftPage);
-        $draftPage->draft = false;
-        $draftPage->revision_count = 1;
-
-        $draftPage->save();
-        $this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
-        $this->searchService->indexEntity($draftPage);
-        return $draftPage;
-    }
-
-    /**
-     * The base query for getting user update drafts.
-     * @param Page $page
-     * @param $userId
-     * @return mixed
-     */
-    protected function userUpdatePageDraftsQuery(Page $page, int $userId)
-    {
-        return $this->entityProvider->pageRevision->where('created_by', '=', $userId)
-            ->where('type', 'update_draft')
-            ->where('page_id', '=', $page->id)
-            ->orderBy('created_at', 'desc');
-    }
+        $copyPage = $this->getNewDraftPage($parent);
+        $pageData = $page->getAttributes();
 
-    /**
-     * Get the latest updated draft revision for a particular page and user.
-     * @param Page $page
-     * @param $userId
-     * @return PageRevision|null
-     */
-    public function getUserPageDraft(Page $page, int $userId)
-    {
-        return $this->userUpdatePageDraftsQuery($page, $userId)->first();
-    }
+        // Update name
+        if (!empty($newName)) {
+            $pageData['name'] = $newName;
+        }
 
-    /**
-     * Get the notification message that informs the user that they are editing a draft page.
-     * @param PageRevision $draft
-     * @return string
-     */
-    public function getUserPageDraftMessage(PageRevision $draft)
-    {
-        $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
-        if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
-            return $message;
+        // Copy tags from previous page if set
+        if ($page->tags) {
+            $pageData['tags'] = [];
+            foreach ($page->tags as $tag) {
+                $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
+            }
         }
-        return $message . "\n" . trans('entities.pages_draft_edited_notification');
+
+        return $this->publishDraft($copyPage, $pageData);
     }
 
     /**
-     * A query to check for active update drafts on a particular page.
-     * @param Page $page
-     * @param int $minRange
-     * @return mixed
+     * Find a page parent entity via a identifier string in the format:
+     * {type}:{id}
+     * Example: (book:5)
+     * @throws MoveOperationException
      */
-    protected function activePageEditingQuery(Page $page, int $minRange = null)
+    protected function findParentByIdentifier(string $identifier): ?Entity
     {
-        $query = $this->entityProvider->pageRevision->where('type', '=', 'update_draft')
-            ->where('page_id', '=', $page->id)
-            ->where('updated_at', '>', $page->updated_at)
-            ->where('created_by', '!=', user()->id)
-            ->with('createdBy');
+        $stringExploded = explode(':', $identifier);
+        $entityType = $stringExploded[0];
+        $entityId = intval($stringExploded[1]);
 
-        if ($minRange !== null) {
-            $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
+        if ($entityType !== 'book' && $entityType !== 'chapter') {
+            throw new MoveOperationException('Pages can only be in books or chapters');
         }
 
-        return $query;
+        $parentClass = $entityType === 'book' ? Book::class : Chapter::class;
+        return $parentClass::visible()->where('id', '=', $entityId)->first();
     }
 
     /**
-     * Check if a page is being actively editing.
-     * Checks for edits since last page updated.
-     * Passing in a minuted range will check for edits
-     * within the last x minutes.
-     * @param Page $page
-     * @param int $minRange
-     * @return bool
+     * Update the permissions of a page.
      */
-    public function isPageEditingActive(Page $page, int $minRange = null)
+    public function updatePermissions(Page $page, bool $restricted, Collection $permissions = null)
     {
-        $draftSearch = $this->activePageEditingQuery($page, $minRange);
-        return $draftSearch->count() > 0;
+        $this->baseRepo->updatePermissions($page, $restricted, $permissions);
     }
 
     /**
-     * Get a notification message concerning the editing activity on a particular page.
-     * @param Page $page
-     * @param int $minRange
-     * @return string
+     * Change the page's parent to the given entity.
      */
-    public function getPageEditingActiveMessage(Page $page, int $minRange = null)
+    protected function changeParent(Page $page, Entity $parent)
     {
-        $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
+        $book = ($parent instanceof Book) ? $parent : $parent->book;
+        $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0;
+        $page->save();
 
-        $userMessage = $pageDraftEdits->count() > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
-        $timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]);
-        return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
+        if ($page->book->id !== $book->id) {
+            $page->changeBook($book->id);
+        }
+
+        $page->load('book');
+        $book->rebuildPermissions();
     }
 
     /**
-     * Parse the headers on the page to get a navigation menu
-     * @param string $pageContent
-     * @return array
+     * Get a page revision to update for the given page.
+     * Checks for an existing revisions before providing a fresh one.
      */
-    public function getPageNav(string $pageContent)
+    protected function getPageRevisionToUpdate(Page $page): PageRevision
     {
-        if ($pageContent == '') {
-            return [];
-        }
-        libxml_use_internal_errors(true);
-        $doc = new DOMDocument();
-        $doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
-        $xPath = new DOMXPath($doc);
-        $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
-
-        if (is_null($headers)) {
-            return [];
+        $drafts = $this->getUserDraftQuery($page)->get();
+        if ($drafts->count() > 0) {
+            return $drafts->first();
         }
 
-        $tree = collect($headers)->map(function($header) {
-            $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
-            $text = mb_substr($text, 0, 100);
-
-            return [
-                'nodeName' => strtolower($header->nodeName),
-                'level' => intval(str_replace('h', '', $header->nodeName)),
-                'link' => '#' . $header->getAttribute('id'),
-                'text' => $text,
-            ];
-        })->filter(function($header) {
-            return mb_strlen($header['text']) > 0;
-        });
-
-        // Shift headers if only smaller headers have been used
-        $levelChange = ($tree->pluck('level')->min() - 1);
-        $tree = $tree->map(function ($header) use ($levelChange) {
-            $header['level'] -= ($levelChange);
-            return $header;
-        });
-
-        return $tree->toArray();
+        $draft = new PageRevision();
+        $draft->page_id = $page->id;
+        $draft->slug = $page->slug;
+        $draft->book_slug = $page->book->slug;
+        $draft->created_by = user()->id;
+        $draft->type = 'update_draft';
+        return $draft;
     }
 
     /**
-     * Restores a revision's content back into a page.
-     * @param Page $page
-     * @param Book $book
-     * @param  int $revisionId
-     * @return Page
-     * @throws \Exception
+     * Delete old revisions, for the given page, from the system.
      */
-    public function restorePageRevision(Page $page, Book $book, int $revisionId)
+    protected function deleteOldRevisions(Page $page)
     {
-        $page->revision_count++;
-        $this->savePageRevision($page);
-        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
-        $page->fill($revision->toArray());
-        $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
-        $page->text = $this->pageToPlainText($page);
-        $page->updated_by = user()->id;
-        $page->save();
-        $this->searchService->indexEntity($page);
-        return $page;
-    }
+        $revisionLimit = config('app.revision_limit');
+        if ($revisionLimit === false) {
+            return;
+        }
 
-    /**
-     * Change the page's parent to the given entity.
-     * @param Page $page
-     * @param Entity $parent
-     * @throws \Throwable
-     */
-    public function changePageParent(Page $page, Entity $parent)
-    {
-        $book = $parent->isA('book') ? $parent : $parent->book;
-        $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
-        $page->save();
-        if ($page->book->id !== $book->id) {
-            $page = $this->changeBook('page', $book->id, $page);
+        $revisionsToDelete = PageRevision::query()
+            ->where('page_id', '=', $page->id)
+            ->orderBy('created_at', 'desc')
+            ->skip(intval($revisionLimit))
+            ->take(10)
+            ->get(['id']);
+        if ($revisionsToDelete->count() > 0) {
+            PageRevision::query()->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
         }
-        $page->load('book');
-        $this->permissionService->buildJointPermissionsForEntity($book);
     }
 
     /**
-     * Create a copy of a page in a new location with a new name.
-     * @param \BookStack\Entities\Page $page
-     * @param \BookStack\Entities\Entity $newParent
-     * @param string $newName
-     * @return \BookStack\Entities\Page
-     * @throws \Throwable
+     * Get a new priority for a page
      */
-    public function copyPage(Page $page, Entity $newParent, string $newName = '')
+    protected function getNewPriority(Page $page): int
     {
-        $newBook = $newParent->isA('book') ? $newParent : $newParent->book;
-        $newChapter = $newParent->isA('chapter') ? $newParent : null;
-        $copyPage = $this->getDraftPage($newBook, $newChapter);
-        $pageData = $page->getAttributes();
-
-        // Update name
-        if (!empty($newName)) {
-            $pageData['name'] = $newName;
-        }
-
-        // Copy tags from previous page if set
-        if ($page->tags) {
-            $pageData['tags'] = [];
-            foreach ($page->tags as $tag) {
-                $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
-            }
+        if ($page->parent() instanceof Chapter) {
+            $lastPage = $page->parent()->pages('desc')->first();
+            return $lastPage ? $lastPage->priority + 1 : 0;
         }
 
-        // Set priority
-        if ($newParent->isA('chapter')) {
-            $pageData['priority'] = $this->getNewChapterPriority($newParent);
-        } else {
-            $pageData['priority'] = $this->getNewBookPriority($newParent);
-        }
-
-        return $this->publishPageDraft($copyPage, $pageData);
+        return (new BookContents($page->book))->getLastPriority() + 1;
     }
 
     /**
-     * Get pages that have been marked as templates.
-     * @param int $count
-     * @param int $page
-     * @param string $search
-     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
+     * Get the query to find the user's draft copies of the given page.
      */
-    public function getPageTemplates(int $count = 10, int $page = 1,  string $search = '')
+    protected function getUserDraftQuery(Page $page)
     {
-        $query = $this->entityQuery('page')
-            ->where('template', '=', true)
-            ->orderBy('name', 'asc')
-            ->skip( ($page - 1) * $count)
-            ->take($count);
-
-        if ($search) {
-            $query->where('name', 'like', '%' . $search . '%');
-        }
-
-        $paginator = $query->paginate($count, ['*'], 'page', $page);
-        $paginator->withPath('/templates');
-
-        return $paginator;
+        return PageRevision::query()->where('created_by', '=', user()->id)
+            ->where('type', 'update_draft')
+            ->where('page_id', '=', $page->id)
+            ->orderBy('created_at', 'desc');
     }
 }
index 9e7cfdd0cdf1c8de852ee88a5464b662580e6f5b..ee9b87786a57a1e059ed050621f0c694427b17cb 100644 (file)
@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
 use Illuminate\Database\Query\Builder;
 use Illuminate\Database\Query\JoinClause;
 use Illuminate\Support\Collection;
+use Illuminate\Support\Str;
 
 class SearchService
 {
@@ -210,7 +211,7 @@ class SearchService
 
         // Handle filters
         foreach ($terms['filters'] as $filterTerm => $filterValue) {
-            $functionName = camel_case('filter_' . $filterTerm);
+            $functionName = Str::camel('filter_' . $filterTerm);
             if (method_exists($this, $functionName)) {
                 $this->$functionName($entitySelect, $entity, $filterValue);
             }
@@ -514,7 +515,7 @@ class SearchService
 
     protected function filterSortBy(EloquentBuilder $query, Entity $model, $input)
     {
-        $functionName = camel_case('sort_by_' . $input);
+        $functionName = Str::camel('sort_by_' . $input);
         if (method_exists($this, $functionName)) {
             $this->$functionName($query, $model);
         }
diff --git a/app/Entities/SlugGenerator.php b/app/Entities/SlugGenerator.php
new file mode 100644 (file)
index 0000000..459a526
--- /dev/null
@@ -0,0 +1,62 @@
+<?php namespace BookStack\Entities;
+
+class SlugGenerator
+{
+
+    protected $entity;
+
+    /**
+     * SlugGenerator constructor.
+     * @param $entity
+     */
+    public function __construct(Entity $entity)
+    {
+        $this->entity = $entity;
+    }
+
+    /**
+     * Generate a fresh slug for the given entity.
+     * The slug will generated so it does not conflict within the same parent item.
+     */
+    public function generate(): string
+    {
+        $slug = $this->formatNameAsSlug($this->entity->name);
+        while ($this->slugInUse($slug)) {
+            $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
+        }
+        return $slug;
+    }
+
+    /**
+     * Format a name as a url slug.
+     */
+    protected function formatNameAsSlug(string $name): string
+    {
+        $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
+        $slug = preg_replace('/\s{2,}/', ' ', $slug);
+        $slug = str_replace(' ', '-', $slug);
+        if ($slug === "") {
+            $slug = substr(md5(rand(1, 500)), 0, 5);
+        }
+        return $slug;
+    }
+
+    /**
+     * Check if a slug is already in-use for this
+     * type of model within the same parent.
+     */
+    protected function slugInUse(string $slug): bool
+    {
+        $query = $this->entity->newQuery()->where('slug', '=', $slug);
+
+        if ($this->entity instanceof BookChild) {
+            $query->where('book_id', '=', $this->entity->book_id);
+        }
+
+        if ($this->entity->id) {
+            $query->where('id', '!=', $this->entity->id);
+        }
+
+        return $query->count() > 0;
+    }
+}
diff --git a/app/Exceptions/MoveOperationException.php b/app/Exceptions/MoveOperationException.php
new file mode 100644 (file)
index 0000000..c237dfa
--- /dev/null
@@ -0,0 +1,8 @@
+<?php namespace BookStack\Exceptions;
+
+use Exception;
+
+class MoveOperationException extends Exception
+{
+
+}
diff --git a/app/Exceptions/SortOperationException.php b/app/Exceptions/SortOperationException.php
new file mode 100644 (file)
index 0000000..8f91217
--- /dev/null
@@ -0,0 +1,8 @@
+<?php namespace BookStack\Exceptions;
+
+use Exception;
+
+class SortOperationException extends Exception
+{
+
+}
index 203e08c8573100786f00728cc65d6c4b7c74b0a0..e19707457a33e47bfee00c944b667bdd9af60a5c 100644 (file)
@@ -1,6 +1,7 @@
 <?php namespace BookStack\Exceptions;
 
-class UserTokenExpiredException extends \Exception {
+class UserTokenExpiredException extends \Exception
+{
 
     public $userId;
 
@@ -14,6 +15,4 @@ class UserTokenExpiredException extends \Exception {
         $this->userId = $userId;
         parent::__construct($message);
     }
-
-
-}
\ No newline at end of file
+}
index 08c1fd830d167a5b875deba746e7ec2a2fa7d77d..3ed53f72ab42e706db04e51cc9a23b48f45e11c6 100644 (file)
@@ -1,3 +1,6 @@
 <?php namespace BookStack\Exceptions;
 
-class UserTokenNotFoundException extends \Exception {}
\ No newline at end of file
+class UserTokenNotFoundException extends \Exception
+{
+
+}
diff --git a/app/Facades/Permissions.php b/app/Facades/Permissions.php
new file mode 100644 (file)
index 0000000..c552d7c
--- /dev/null
@@ -0,0 +1,16 @@
+<?php namespace BookStack\Facades;
+
+use Illuminate\Support\Facades\Facade;
+
+class Permissions extends Facade
+{
+    /**
+     * Get the registered name of the component.
+     *
+     * @return string
+     */
+    protected static function getFacadeAccessor()
+    {
+        return 'permissions';
+    }
+}
index 0289f8e1d8f09e5bdf1bde68c7437c2e5a4746be..8f5da49ed83c10c5979b9e4b8eaa74ac324e14b9 100644 (file)
@@ -1,37 +1,37 @@
 <?php namespace BookStack\Http\Controllers;
 
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Repos\PageRepo;
 use BookStack\Exceptions\FileUploadException;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Uploads\Attachment;
 use BookStack\Uploads\AttachmentService;
+use Exception;
+use Illuminate\Contracts\Filesystem\FileNotFoundException;
 use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
 
 class AttachmentController extends Controller
 {
     protected $attachmentService;
     protected $attachment;
-    protected $entityRepo;
+    protected $pageRepo;
 
     /**
      * AttachmentController constructor.
-     * @param \BookStack\Uploads\AttachmentService $attachmentService
-     * @param Attachment $attachment
-     * @param EntityRepo $entityRepo
      */
-    public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo)
+    public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
     {
         $this->attachmentService = $attachmentService;
         $this->attachment = $attachment;
-        $this->entityRepo = $entityRepo;
+        $this->pageRepo = $pageRepo;
         parent::__construct();
     }
 
 
     /**
      * Endpoint at which attachments are uploaded to.
-     * @param Request $request
-     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
+     * @throws ValidationException
+     * @throws NotFoundException
      */
     public function upload(Request $request)
     {
@@ -41,7 +41,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->entityRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
 
         $this->checkPermission('attachment-create-all');
         $this->checkOwnablePermission('page-update', $page);
@@ -59,11 +59,10 @@ class AttachmentController extends Controller
 
     /**
      * Update an uploaded attachment.
-     * @param int $attachmentId
-     * @param Request $request
-     * @return mixed
+     * @throws ValidationException
+     * @throws NotFoundException
      */
-    public function uploadUpdate($attachmentId, Request $request)
+    public function uploadUpdate(Request $request, $attachmentId)
     {
         $this->validate($request, [
             'uploaded_to' => 'required|integer|exists:pages,id',
@@ -71,7 +70,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->entityRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $attachment = $this->attachment->findOrFail($attachmentId);
 
         $this->checkOwnablePermission('page-update', $page);
@@ -94,11 +93,10 @@ class AttachmentController extends Controller
 
     /**
      * Update the details of an existing file.
-     * @param $attachmentId
-     * @param Request $request
-     * @return Attachment|mixed
+     * @throws ValidationException
+     * @throws NotFoundException
      */
-    public function update($attachmentId, Request $request)
+    public function update(Request $request, $attachmentId)
     {
         $this->validate($request, [
             'uploaded_to' => 'required|integer|exists:pages,id',
@@ -107,7 +105,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->entityRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $attachment = $this->attachment->findOrFail($attachmentId);
 
         $this->checkOwnablePermission('page-update', $page);
@@ -123,8 +121,8 @@ class AttachmentController extends Controller
 
     /**
      * Attach a link to a page.
-     * @param Request $request
-     * @return mixed
+     * @throws ValidationException
+     * @throws NotFoundException
      */
     public function attachLink(Request $request)
     {
@@ -135,7 +133,7 @@ class AttachmentController extends Controller
         ]);
 
         $pageId = $request->get('uploaded_to');
-        $page = $this->entityRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
 
         $this->checkPermission('attachment-create-all');
         $this->checkOwnablePermission('page-update', $page);
@@ -149,29 +147,26 @@ class AttachmentController extends Controller
 
     /**
      * Get the attachments for a specific page.
-     * @param $pageId
-     * @return mixed
      */
-    public function listForPage($pageId)
+    public function listForPage(int $pageId)
     {
-        $page = $this->entityRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $this->checkOwnablePermission('page-view', $page);
         return response()->json($page->attachments);
     }
 
     /**
      * Update the attachment sorting.
-     * @param $pageId
-     * @param Request $request
-     * @return mixed
+     * @throws ValidationException
+     * @throws NotFoundException
      */
-    public function sortForPage($pageId, Request $request)
+    public function sortForPage(Request $request, int $pageId)
     {
         $this->validate($request, [
             'files' => 'required|array',
             'files.*.id' => 'required|integer',
         ]);
-        $page = $this->entityRepo->getById('page', $pageId);
+        $page = $this->pageRepo->getById($pageId);
         $this->checkOwnablePermission('page-update', $page);
 
         $attachments = $request->get('files');
@@ -181,16 +176,15 @@ class AttachmentController extends Controller
 
     /**
      * Get an attachment from storage.
-     * @param $attachmentId
-     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
-     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+     * @throws FileNotFoundException
      * @throws NotFoundException
      */
-    public function get($attachmentId)
+    public function get(int $attachmentId)
     {
         $attachment = $this->attachment->findOrFail($attachmentId);
-        $page = $this->entityRepo->getById('page', $attachment->uploaded_to);
-        if ($page === null) {
+        try {
+            $page = $this->pageRepo->getById($attachment->uploaded_to);
+        } catch (NotFoundException $exception) {
             throw new NotFoundException(trans('errors.attachment_not_found'));
         }
 
@@ -208,9 +202,9 @@ class AttachmentController extends Controller
      * Delete a specific attachment in the system.
      * @param $attachmentId
      * @return mixed
-     * @throws \Exception
+     * @throws Exception
      */
-    public function delete($attachmentId)
+    public function delete(int $attachmentId)
     {
         $attachment = $this->attachment->findOrFail($attachmentId);
         $this->checkOwnablePermission('attachment-delete', $attachment);
index 3e240b94efa845385320d0b48fc17b8a5b2edfaf..099558eb77fdce133b307aaac327277bc02510f8 100644 (file)
@@ -64,16 +64,15 @@ class ConfirmEmailController extends Controller
         try {
             $userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
         } catch (Exception $exception) {
-
             if ($exception instanceof UserTokenNotFoundException) {
-                session()->flash('error', trans('errors.email_confirmation_invalid'));
+                $this->showErrorNotification(trans('errors.email_confirmation_invalid'));
                 return redirect('/register');
             }
 
             if ($exception instanceof UserTokenExpiredException) {
                 $user = $this->userRepo->getById($exception->userId);
                 $this->emailConfirmationService->sendConfirmation($user);
-                session()->flash('error', trans('errors.email_confirmation_expired'));
+                $this->showErrorNotification(trans('errors.email_confirmation_expired'));
                 return redirect('/register/confirm');
             }
 
@@ -85,7 +84,7 @@ class ConfirmEmailController extends Controller
         $user->save();
 
         auth()->login($user);
-        session()->flash('success', trans('auth.email_confirm_success'));
+        $this->showSuccessNotification(trans('auth.email_confirm_success'));
         $this->emailConfirmationService->deleteByUser($user);
 
         return redirect('/');
@@ -107,12 +106,11 @@ class ConfirmEmailController extends Controller
         try {
             $this->emailConfirmationService->sendConfirmation($user);
         } catch (Exception $e) {
-            session()->flash('error', trans('auth.email_confirm_send_error'));
+            $this->showErrorNotification(trans('auth.email_confirm_send_error'));
             return redirect('/register/confirm');
         }
 
-        session()->flash('success', trans('auth.email_confirm_resent'));
+        $this->showSuccessNotification(trans('auth.email_confirm_resent'));
         return redirect('/register/confirm');
     }
-
 }
index a0cbae9c611bffeb421e64788ebca7651ef2f65e..a3c0433a56f9e4af9d574ae0446edac02dbd5c1f 100644 (file)
@@ -53,7 +53,7 @@ class ForgotPasswordController extends Controller
 
         if ($response === Password::RESET_LINK_SENT) {
             $message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
-            session()->flash('success', $message);
+            $this->showSuccessNotification($message);
             return back()->with('status', trans($response));
         }
 
index c411f2363210999c6b44e578a19024b734e85fba..304d3bed2e69999b381e0a8e1b4cb6a22790f492 100644 (file)
@@ -18,6 +18,8 @@ use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
 use Illuminate\Routing\Redirector;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Str;
 use Laravel\Socialite\Contracts\User as SocialUser;
 use Validator;
 
@@ -77,7 +79,7 @@ class RegisterController extends Controller
         return Validator::make($data, [
             'name' => 'required|min:2|max:255',
             'email' => 'required|email|max:255|unique:users',
-            'password' => 'required|min:6',
+            'password' => 'required|min:8',
         ]);
     }
 
@@ -129,7 +131,7 @@ class RegisterController extends Controller
         return User::create([
             'name' => $data['name'],
             'email' => $data['email'],
-            'password' => bcrypt($data['password']),
+            'password' => Hash::make($data['password']),
         ]);
     }
 
@@ -164,14 +166,14 @@ class RegisterController extends Controller
             try {
                 $this->emailConfirmationService->sendConfirmation($newUser);
             } catch (Exception $e) {
-                session()->flash('error', trans('auth.email_confirm_send_error'));
+                $this->showErrorNotification(trans('auth.email_confirm_send_error'));
             }
 
             return redirect('/register/confirm');
         }
 
         auth()->login($newUser);
-        session()->flash('success', trans('auth.register_success'));
+        $this->showSuccessNotification(trans('auth.register_success'));
         return redirect($this->redirectPath());
     }
 
@@ -191,14 +193,14 @@ class RegisterController extends Controller
 
     /**
      * The callback for social login services.
-     * @param $socialDriver
      * @param Request $request
+     * @param string $socialDriver
      * @return RedirectResponse|Redirector
      * @throws SocialSignInException
      * @throws UserRegistrationException
      * @throws SocialDriverNotConfigured
      */
-    public function socialCallback($socialDriver, Request $request)
+    public function socialCallback(Request $request, string $socialDriver)
     {
         if (!session()->has('social-callback')) {
             throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
@@ -261,7 +263,7 @@ class RegisterController extends Controller
         $userData = [
             'name' => $socialUser->getName(),
             'email' => $socialUser->getEmail(),
-            'password' => str_random(30)
+            'password' => Str::random(30)
         ];
         return $this->registerUser($userData, $socialAccount, $emailVerified);
     }
index 56f1cf026f0727af67bc51d785c012bd6b74daa7..4d98eca597c3fffcece65fc79209bae646a49ab9 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Http\Controllers\Auth;
 
 use BookStack\Http\Controllers\Controller;
 use Illuminate\Foundation\Auth\ResetsPasswords;
+use Illuminate\Http\Request;
 
 class ResetPasswordController extends Controller
 {
@@ -36,13 +37,14 @@ class ResetPasswordController extends Controller
     /**
      * Get the response for a successful password reset.
      *
-     * @param  string  $response
+     * @param Request $request
+     * @param string $response
      * @return \Illuminate\Http\Response
      */
-    protected function sendResetResponse($response)
+    protected function sendResetResponse(Request $request, $response)
     {
         $message = trans('auth.reset_password_success');
-        session()->flash('success', $message);
+        $this->showSuccessNotification($message);
         return redirect($this->redirectPath())
             ->with('status', trans($response));
     }
index 5d9373f452c183eab30d5c871a93797b0165e07c..c361b0a9b43687101e577db4f3e46f55b86ba397 100644 (file)
@@ -54,15 +54,15 @@ class UserInviteController extends Controller
 
     /**
      * Sets the password for an invited user and then grants them access.
-     * @param string $token
      * @param Request $request
+     * @param string $token
      * @return RedirectResponse|Redirector
      * @throws Exception
      */
-    public function setPassword(string $token, Request $request)
+    public function setPassword(Request $request, string $token)
     {
         $this->validate($request, [
-            'password' => 'required|min:6'
+            'password' => 'required|min:8'
         ]);
 
         try {
@@ -77,7 +77,7 @@ class UserInviteController extends Controller
         $user->save();
 
         auth()->login($user);
-        session()->flash('success', trans('auth.user_invite_success', ['appName' => setting('app-name')]));
+        $this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')]));
         $this->inviteService->deleteByUser($user);
 
         return redirect('/');
@@ -96,11 +96,10 @@ class UserInviteController extends Controller
         }
 
         if ($exception instanceof UserTokenExpiredException) {
-            session()->flash('error', trans('errors.invite_token_expired'));
+            $this->showErrorNotification(trans('errors.invite_token_expired'));
             return redirect('/password/email');
         }
 
         throw $exception;
     }
-
 }
index a990eed9a950e4ffb45365bae858a35b279e1d43..e7d788d91dde01b78fdd158035fe07fd6cd5f54e 100644 (file)
@@ -1,67 +1,46 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Book;
-use BookStack\Entities\EntityContextManager;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
-use BookStack\Uploads\ImageRepo;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Managers\EntityContext;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotifyException;
 use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
+use Throwable;
 use Views;
 
 class BookController extends Controller
 {
 
-    protected $entityRepo;
-    protected $userRepo;
-    protected $exportService;
+    protected $bookRepo;
     protected $entityContextManager;
-    protected $imageRepo;
 
     /**
      * BookController constructor.
-     * @param EntityRepo $entityRepo
-     * @param UserRepo $userRepo
-     * @param ExportService $exportService
-     * @param EntityContextManager $entityContextManager
-     * @param ImageRepo $imageRepo
      */
-    public function __construct(
-        EntityRepo $entityRepo,
-        UserRepo $userRepo,
-        ExportService $exportService,
-        EntityContextManager $entityContextManager,
-        ImageRepo $imageRepo
-    ) {
-        $this->entityRepo = $entityRepo;
-        $this->userRepo = $userRepo;
-        $this->exportService = $exportService;
+    public function __construct(EntityContext $entityContextManager, BookRepo $bookRepo)
+    {
+        $this->bookRepo = $bookRepo;
         $this->entityContextManager = $entityContextManager;
-        $this->imageRepo = $imageRepo;
         parent::__construct();
     }
 
     /**
      * Display a listing of the book.
-     * @return Response
      */
     public function index()
     {
-        $view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
-        $sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
-        $order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
-        $sortOptions = [
-            'name' => trans('common.sort_name'),
-            'created_at' => trans('common.sort_created_at'),
-            'updated_at' => trans('common.sort_updated_at'),
-        ];
-
-        $books = $this->entityRepo->getAllPaginated('book', 18, $sort, $order);
-        $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
-        $popular = $this->entityRepo->getPopular('book', 4, 0);
-        $new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
+        $view = setting()->getForCurrentUser('books_view_type', config('app.views.books'));
+        $sort = setting()->getForCurrentUser('books_sort', 'name');
+        $order = setting()->getForCurrentUser('books_sort_order', 'asc');
+
+        $books = $this->bookRepo->getAllPaginated(18, $sort, $order);
+        $recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
+        $popular = $this->bookRepo->getPopular(4);
+        $new = $this->bookRepo->getRecentlyCreated(4);
 
         $this->entityContextManager->clearShelfContext();
 
@@ -74,25 +53,22 @@ class BookController extends Controller
             'view' => $view,
             'sort' => $sort,
             'order' => $order,
-            'sortOptions' => $sortOptions,
         ]);
     }
 
     /**
      * Show the form for creating a new book.
-     * @param string $shelfSlug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
      */
     public function create(string $shelfSlug = null)
     {
+        $this->checkPermission('book-create-all');
+
         $bookshelf = null;
         if ($shelfSlug !== null) {
-            $bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
+            $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
             $this->checkOwnablePermission('bookshelf-update', $bookshelf);
         }
 
-        $this->checkPermission('book-create-all');
         $this->setPageTitle(trans('entities.books_create'));
         return view('books.create', [
             'bookshelf' => $bookshelf
@@ -101,12 +77,8 @@ class BookController extends Controller
 
     /**
      * Store a newly created book in storage.
-     *
-     * @param Request $request
-     * @param string $shelfSlug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \BookStack\Exceptions\ImageUploadException
+     * @throws ImageUploadException
+     * @throws ValidationException
      */
     public function store(Request $request, string $shelfSlug = null)
     {
@@ -114,21 +86,21 @@ class BookController extends Controller
         $this->validate($request, [
             'name' => 'required|string|max:255',
             'description' => 'string|max:1000',
-            'image' => $this->imageRepo->getImageValidationRules(),
+            'image' => $this->getImageValidationRules(),
         ]);
 
         $bookshelf = null;
         if ($shelfSlug !== null) {
-            $bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
+            $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
             $this->checkOwnablePermission('bookshelf-update', $bookshelf);
         }
 
-        $book = $this->entityRepo->createFromInput('book', $request->all());
-        $this->bookUpdateActions($book, $request);
+        $book = $this->bookRepo->create($request->all());
+        $this->bookRepo->updateCoverImage($book, $request->file('image', null));
         Activity::add($book, 'book_create', $book->id);
 
         if ($bookshelf) {
-            $this->entityRepo->appendBookToShelf($bookshelf, $book);
+            $bookshelf->appendBook($book);
             Activity::add($bookshelf, 'bookshelf_update');
         }
 
@@ -137,17 +109,11 @@ class BookController extends Controller
 
     /**
      * Display the specified book.
-     * @param $slug
-     * @param Request $request
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
      */
-    public function show($slug, Request $request)
+    public function show(Request $request, string $slug)
     {
-        $book = $this->entityRepo->getBySlug('book', $slug);
-        $this->checkOwnablePermission('book-view', $book);
-
-        $bookChildren = $this->entityRepo->getBookChildren($book);
+        $book = $this->bookRepo->getBySlug($slug);
+        $bookChildren = (new BookContents($book))->getTree(true);
 
         Views::add($book);
         if ($request->has('shelf')) {
@@ -165,12 +131,10 @@ class BookController extends Controller
 
     /**
      * Show the form for editing the specified book.
-     * @param $slug
-     * @return Response
      */
-    public function edit($slug)
+    public function edit(string $slug)
     {
-        $book = $this->entityRepo->getBySlug('book', $slug);
+        $book = $this->bookRepo->getBySlug($slug);
         $this->checkOwnablePermission('book-update', $book);
         $this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
         return view('books.edit', ['book' => $book, 'current' => $book]);
@@ -178,254 +142,83 @@ class BookController extends Controller
 
     /**
      * Update the specified book in storage.
-     * @param Request $request
-     * @param          $slug
-     * @return Response
-     * @throws \BookStack\Exceptions\ImageUploadException
-     * @throws \BookStack\Exceptions\NotFoundException
+     * @throws ImageUploadException
+     * @throws ValidationException
+     * @throws Throwable
      */
     public function update(Request $request, string $slug)
     {
-        $book = $this->entityRepo->getBySlug('book', $slug);
+        $book = $this->bookRepo->getBySlug($slug);
         $this->checkOwnablePermission('book-update', $book);
         $this->validate($request, [
             'name' => 'required|string|max:255',
             'description' => 'string|max:1000',
-            'image' => $this->imageRepo->getImageValidationRules(),
+            'image' => $this->getImageValidationRules(),
         ]);
 
-         $book = $this->entityRepo->updateFromInput('book', $book, $request->all());
-         $this->bookUpdateActions($book, $request);
+        $book = $this->bookRepo->update($book, $request->all());
+        $resetCover = $request->has('image_reset');
+        $this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
 
-         Activity::add($book, 'book_update', $book->id);
+        Activity::add($book, 'book_update', $book->id);
 
-         return redirect($book->getUrl());
+        return redirect($book->getUrl());
     }
 
     /**
-     * Shows the page to confirm deletion
-     * @param $bookSlug
-     * @return \Illuminate\View\View
+     * Shows the page to confirm deletion.
      */
-    public function showDelete($bookSlug)
+    public function showDelete(string $bookSlug)
     {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
-        $this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
+        $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
         return view('books.delete', ['book' => $book, 'current' => $book]);
     }
 
     /**
-     * Shows the view which allows pages to be re-ordered and sorted.
-     * @param string $bookSlug
-     * @return \Illuminate\View\View
-     * @throws \BookStack\Exceptions\NotFoundException
-     */
-    public function sort($bookSlug)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $this->checkOwnablePermission('book-update', $book);
-
-        $bookChildren = $this->entityRepo->getBookChildren($book, true);
-
-        $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
-        return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
-    }
-
-    /**
-     * Shows the sort box for a single book.
-     * Used via AJAX when loading in extra books to a sort.
-     * @param $bookSlug
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     * Remove the specified book from the system.
+     * @throws Throwable
+     * @throws NotifyException
      */
-    public function getSortItem($bookSlug)
+    public function destroy(string $bookSlug)
     {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $bookChildren = $this->entityRepo->getBookChildren($book);
-        return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
-    }
-
-    /**
-     * Saves an array of sort mapping to pages and chapters.
-     * @param  string $bookSlug
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     */
-    public function saveSort($bookSlug, Request $request)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $this->checkOwnablePermission('book-update', $book);
-
-        // Return if no map sent
-        if (!$request->filled('sort-tree')) {
-            return redirect($book->getUrl());
-        }
-
-        // Sort pages and chapters
-        $sortMap = collect(json_decode($request->get('sort-tree')));
-        $bookIdsInvolved = collect([$book->id]);
-
-        // Load models into map
-        $sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
-            $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
-            $mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
-            // Store source and target books
-            $bookIdsInvolved->push(intval($mapItem->model->book_id));
-            $bookIdsInvolved->push(intval($mapItem->book));
-        });
-
-        // Get the books involved in the sort
-        $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
-        $booksInvolved = $this->entityRepo->getManyById('book', $bookIdsInvolved, false, true);
-        // Throw permission error if invalid ids or inaccessible books given.
-        if (count($bookIdsInvolved) !== count($booksInvolved)) {
-            $this->showPermissionError();
-        }
-        // Check permissions of involved books
-        $booksInvolved->each(function (Book $book) {
-             $this->checkOwnablePermission('book-update', $book);
-        });
-
-        // Perform the sort
-        $sortMap->each(function ($mapItem) {
-            $model = $mapItem->model;
-
-            $priorityChanged = intval($model->priority) !== intval($mapItem->sort);
-            $bookChanged = intval($model->book_id) !== intval($mapItem->book);
-            $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
-
-            if ($bookChanged) {
-                $this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
-            }
-            if ($chapterChanged) {
-                $model->chapter_id = intval($mapItem->parentChapter);
-                $model->save();
-            }
-            if ($priorityChanged) {
-                $model->priority = intval($mapItem->sort);
-                $model->save();
-            }
-        });
-
-        // Rebuild permissions and add activity for involved books.
-        $booksInvolved->each(function (Book $book) {
-            $this->entityRepo->buildJointPermissionsForBook($book);
-            Activity::add($book, 'book_sort', $book->id);
-        });
-
-        return redirect($book->getUrl());
-    }
-
-    /**
-     * Remove the specified book from storage.
-     * @param $bookSlug
-     * @return Response
-     */
-    public function destroy($bookSlug)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('book-delete', $book);
-        Activity::addMessage('book_delete', 0, $book->name);
 
-        if ($book->cover) {
-            $this->imageRepo->destroyImage($book->cover);
-        }
-        $this->entityRepo->destroyBook($book);
+        Activity::addMessage('book_delete', $book->name);
+        $this->bookRepo->destroy($book);
 
         return redirect('/books');
     }
 
     /**
-     * Show the Restrictions view.
-     * @param $bookSlug
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     * Show the permissions view.
      */
-    public function showPermissions($bookSlug)
+    public function showPermissions(string $bookSlug)
     {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $book);
-        $roles = $this->userRepo->getRestrictableRoles();
+
         return view('books.permissions', [
             'book' => $book,
-            'roles' => $roles
         ]);
     }
 
     /**
      * Set the restrictions for this book.
-     * @param $bookSlug
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \Throwable
+     * @throws Throwable
      */
-    public function permissions($bookSlug, Request $request)
+    public function permissions(Request $request, string $bookSlug)
     {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = $this->bookRepo->getBySlug($bookSlug);
         $this->checkOwnablePermission('restrictions-manage', $book);
-        $this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
-        session()->flash('success', trans('entities.books_permissions_updated'));
-        return redirect($book->getUrl());
-    }
-
-    /**
-     * Export a book as a PDF file.
-     * @param string $bookSlug
-     * @return mixed
-     */
-    public function exportPdf($bookSlug)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $pdfContent = $this->exportService->bookToPdf($book);
-        return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
-    }
-
-    /**
-     * Export a book as a contained HTML file.
-     * @param string $bookSlug
-     * @return mixed
-     */
-    public function exportHtml($bookSlug)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $htmlContent = $this->exportService->bookToContainedHtml($book);
-        return $this->downloadResponse($htmlContent, $bookSlug . '.html');
-    }
-
-    /**
-     * Export a book as a plain text file.
-     * @param $bookSlug
-     * @return mixed
-     */
-    public function exportPlainText($bookSlug)
-    {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
-        $textContent = $this->exportService->bookToPlainText($book);
-        return $this->downloadResponse($textContent, $bookSlug . '.txt');
-    }
 
-    /**
-     * Common actions to run on book update.
-     * Handles updating the cover image.
-     * @param Book $book
-     * @param Request $request
-     * @throws \BookStack\Exceptions\ImageUploadException
-     */
-    protected function bookUpdateActions(Book $book, Request $request)
-    {
-        // Update the cover image if in request
-        if ($request->has('image')) {
-            $this->imageRepo->destroyImage($book->cover);
-            $newImage = $request->file('image');
-            $image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
-            $book->image_id = $image->id;
-            $book->save();
-        }
+        $restricted = $request->get('restricted') === 'true';
+        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+        $this->bookRepo->updatePermissions($book, $restricted, $permissions);
 
-        if ($request->has('image_reset')) {
-            $this->imageRepo->destroyImage($book->cover);
-            $book->image_id = 0;
-            $book->save();
-        }
+        $this->showSuccessNotification(trans('entities.books_permissions_updated'));
+        return redirect($book->getUrl());
     }
 }
diff --git a/app/Http/Controllers/BookExportController.php b/app/Http/Controllers/BookExportController.php
new file mode 100644 (file)
index 0000000..cfa3d6a
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Repos\BookRepo;
+use Throwable;
+
+class BookExportController extends Controller
+{
+
+    protected $bookRepo;
+    protected $exportService;
+
+    /**
+     * BookExportController constructor.
+     */
+    public function __construct(BookRepo $bookRepo, ExportService $exportService)
+    {
+        $this->bookRepo = $bookRepo;
+        $this->exportService = $exportService;
+        parent::__construct();
+    }
+
+    /**
+     * Export a book as a PDF file.
+     * @throws Throwable
+     */
+    public function pdf(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $pdfContent = $this->exportService->bookToPdf($book);
+        return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
+    }
+
+    /**
+     * Export a book as a contained HTML file.
+     * @throws Throwable
+     */
+    public function html(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $htmlContent = $this->exportService->bookToContainedHtml($book);
+        return $this->downloadResponse($htmlContent, $bookSlug . '.html');
+    }
+
+    /**
+     * Export a book as a plain text file.
+     */
+    public function plainText(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $textContent = $this->exportService->bookToPlainText($book);
+        return $this->downloadResponse($textContent, $bookSlug . '.txt');
+    }
+}
diff --git a/app/Http/Controllers/BookSortController.php b/app/Http/Controllers/BookSortController.php
new file mode 100644 (file)
index 0000000..f5fb6f2
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Exceptions\SortOperationException;
+use BookStack\Facades\Activity;
+use Illuminate\Http\Request;
+
+class BookSortController extends Controller
+{
+
+    protected $bookRepo;
+
+    /**
+     * BookSortController constructor.
+     * @param $bookRepo
+     */
+    public function __construct(BookRepo $bookRepo)
+    {
+        $this->bookRepo = $bookRepo;
+        parent::__construct();
+    }
+
+    /**
+     * Shows the view which allows pages to be re-ordered and sorted.
+     */
+    public function show(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('book-update', $book);
+
+        $bookChildren = (new BookContents($book))->getTree(false);
+
+        $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
+        return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
+    }
+
+    /**
+     * Shows the sort box for a single book.
+     * Used via AJAX when loading in extra books to a sort.
+     */
+    public function showItem(string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $bookChildren = (new BookContents($book))->getTree();
+        return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
+    }
+
+    /**
+     * Sorts a book using a given mapping array.
+     */
+    public function update(Request $request, string $bookSlug)
+    {
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('book-update', $book);
+
+        // Return if no map sent
+        if (!$request->filled('sort-tree')) {
+            return redirect($book->getUrl());
+        }
+
+        $sortMap = collect(json_decode($request->get('sort-tree')));
+        $bookContents = new BookContents($book);
+        $booksInvolved = collect();
+
+        try {
+            $booksInvolved = $bookContents->sortUsingMap($sortMap);
+        } catch (SortOperationException $exception) {
+            $this->showPermissionError();
+        }
+
+        // Rebuild permissions and add activity for involved books.
+        $booksInvolved->each(function (Book $book) {
+            Activity::add($book, 'book_sort', $book->id);
+        });
+
+        return redirect($book->getUrl());
+    }
+}
index bcf2e12df88a67628bdded31f2db1162ce120098..57e67dc00e618b53fec9b16eec134242422dd8af 100644 (file)
@@ -1,34 +1,30 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\EntityContextManager;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\EntityContext;
+use BookStack\Entities\Repos\BookshelfRepo;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotFoundException;
 use BookStack\Uploads\ImageRepo;
+use Exception;
 use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
 use Views;
 
 class BookshelfController extends Controller
 {
 
-    protected $entityRepo;
-    protected $userRepo;
+    protected $bookshelfRepo;
     protected $entityContextManager;
     protected $imageRepo;
 
     /**
      * BookController constructor.
-     * @param EntityRepo $entityRepo
-     * @param UserRepo $userRepo
-     * @param EntityContextManager $entityContextManager
-     * @param ImageRepo $imageRepo
      */
-    public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo)
+    public function __construct(BookshelfRepo $bookshelfRepo, EntityContext $entityContextManager, ImageRepo $imageRepo)
     {
-        $this->entityRepo = $entityRepo;
-        $this->userRepo = $userRepo;
+        $this->bookshelfRepo = $bookshelfRepo;
         $this->entityContextManager = $entityContextManager;
         $this->imageRepo = $imageRepo;
         parent::__construct();
@@ -36,27 +32,22 @@ class BookshelfController extends Controller
 
     /**
      * Display a listing of the book.
-     * @return Response
      */
     public function index()
     {
-        $view = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
-        $sort = setting()->getUser($this->currentUser, 'bookshelves_sort', 'name');
-        $order = setting()->getUser($this->currentUser, 'bookshelves_sort_order', 'asc');
+        $view = setting()->getForCurrentUser('bookshelves_view_type', config('app.views.bookshelves', 'grid'));
+        $sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
+        $order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
         $sortOptions = [
             'name' => trans('common.sort_name'),
             'created_at' => trans('common.sort_created_at'),
             'updated_at' => trans('common.sort_updated_at'),
         ];
 
-        $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order);
-        foreach ($shelves as $shelf) {
-            $shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
-        }
-
-        $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
-        $popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
-        $new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
+        $shelves = $this->bookshelfRepo->getAllPaginated(18, $sort, $order);
+        $recents = $this->isSignedIn() ? $this->bookshelfRepo->getRecentlyViewed(4) : false;
+        $popular = $this->bookshelfRepo->getPopular(4);
+        $new = $this->bookshelfRepo->getRecentlyCreated(4);
 
         $this->entityContextManager->clearShelfContext();
         $this->setPageTitle(trans('entities.shelves'));
@@ -74,21 +65,19 @@ class BookshelfController extends Controller
 
     /**
      * Show the form for creating a new bookshelf.
-     * @return Response
      */
     public function create()
     {
         $this->checkPermission('bookshelf-create-all');
-        $books = $this->entityRepo->getAll('book', false, 'update');
+        $books = Book::hasPermission('update')->get();
         $this->setPageTitle(trans('entities.shelves_create'));
         return view('shelves.create', ['books' => $books]);
     }
 
     /**
      * Store a newly created bookshelf in storage.
-     * @param Request $request
-     * @return Response
-     * @throws \BookStack\Exceptions\ImageUploadException
+     * @throws ValidationException
+     * @throws ImageUploadException
      */
     public function store(Request $request)
     {
@@ -96,80 +85,63 @@ class BookshelfController extends Controller
         $this->validate($request, [
             'name' => 'required|string|max:255',
             'description' => 'string|max:1000',
-            'image' => $this->imageRepo->getImageValidationRules(),
+            'image' => $this->getImageValidationRules(),
         ]);
 
-        $shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
-        $this->shelfUpdateActions($shelf, $request);
+        $bookIds = explode(',', $request->get('books', ''));
+        $shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
+        $this->bookshelfRepo->updateCoverImage($shelf);
 
         Activity::add($shelf, 'bookshelf_create');
         return redirect($shelf->getUrl());
     }
 
-
     /**
-     * Display the specified bookshelf.
-     * @param String $slug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
+     * Display the bookshelf of the given slug.
+     * @throws NotFoundException
      */
     public function show(string $slug)
     {
-        /** @var Bookshelf $shelf */
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('book-view', $shelf);
 
-        $books = $this->entityRepo->getBookshelfChildren($shelf);
         Views::add($shelf);
         $this->entityContextManager->setShelfContext($shelf->id);
 
         $this->setPageTitle($shelf->getShortName());
-
         return view('shelves.show', [
             'shelf' => $shelf,
-            'books' => $books,
             'activity' => Activity::entityActivity($shelf, 20, 1)
         ]);
     }
 
     /**
      * Show the form for editing the specified bookshelf.
-     * @param $slug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
      */
     public function edit(string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-update', $shelf);
 
-        $shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
-        $shelfBookIds = $shelfBooks->pluck('id');
-        $books = $this->entityRepo->getAll('book', false, 'update');
-        $books = $books->filter(function ($book) use ($shelfBookIds) {
-             return !$shelfBookIds->contains($book->id);
-        });
+        $shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
+        $books = Book::hasPermission('update')->whereNotIn('id', $shelfBookIds)->get();
 
         $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
         return view('shelves.edit', [
             'shelf' => $shelf,
             'books' => $books,
-            'shelfBooks' => $shelfBooks,
         ]);
     }
 
-
     /**
      * Update the specified bookshelf in storage.
-     * @param Request $request
-     * @param string $slug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \BookStack\Exceptions\ImageUploadException
+     * @throws ValidationException
+     * @throws ImageUploadException
+     * @throws NotFoundException
      */
     public function update(Request $request, string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-update', $shelf);
         $this->validate($request, [
             'name' => 'required|string|max:255',
@@ -177,24 +149,22 @@ class BookshelfController extends Controller
             'image' => $this->imageRepo->getImageValidationRules(),
         ]);
 
-         $shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
-         $this->shelfUpdateActions($shelf, $request);
 
-         Activity::add($shelf, 'bookshelf_update');
+        $bookIds = explode(',', $request->get('books', ''));
+        $shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
+        $resetCover = $request->has('image_reset');
+        $this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
+        Activity::add($shelf, 'bookshelf_update');
 
-         return redirect($shelf->getUrl());
+        return redirect($shelf->getUrl());
     }
 
-
     /**
      * Shows the page to confirm deletion
-     * @param $slug
-     * @return \Illuminate\View\View
-     * @throws \BookStack\Exceptions\NotFoundException
      */
     public function showDelete(string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-delete', $shelf);
 
         $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
@@ -203,101 +173,58 @@ class BookshelfController extends Controller
 
     /**
      * Remove the specified bookshelf from storage.
-     * @param string $slug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \Throwable
+     * @throws Exception
      */
     public function destroy(string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-delete', $shelf);
-        Activity::addMessage('bookshelf_delete', 0, $shelf->name);
 
-        if ($shelf->cover) {
-            $this->imageRepo->destroyImage($shelf->cover);
-        }
-        $this->entityRepo->destroyBookshelf($shelf);
+        Activity::addMessage('bookshelf_delete', $shelf->name);
+        $this->bookshelfRepo->destroy($shelf);
 
         return redirect('/shelves');
     }
 
     /**
      * Show the permissions view.
-     * @param string $slug
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
-     * @throws \BookStack\Exceptions\NotFoundException
      */
     public function showPermissions(string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('restrictions-manage', $shelf);
 
-        $roles = $this->userRepo->getRestrictableRoles();
         return view('shelves.permissions', [
             'shelf' => $shelf,
-            'roles' => $roles
         ]);
     }
 
     /**
      * Set the permissions for this bookshelf.
-     * @param string $slug
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \Throwable
      */
-    public function permissions(string $slug, Request $request)
+    public function permissions(Request $request, string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('restrictions-manage', $shelf);
 
-        $this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
-        session()->flash('success', trans('entities.shelves_permissions_updated'));
+        $restricted = $request->get('restricted') === 'true';
+        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+        $this->bookshelfRepo->updatePermissions($shelf, $restricted, $permissions);
+
+        $this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
         return redirect($shelf->getUrl());
     }
 
     /**
      * Copy the permissions of a bookshelf to the child books.
-     * @param string $slug
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     * @throws \BookStack\Exceptions\NotFoundException
      */
     public function copyPermissions(string $slug)
     {
-        $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+        $shelf = $this->bookshelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('restrictions-manage', $shelf);
 
-        $updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
-        session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
+        $updateCount = $this->bookshelfRepo->copyDownPermissions($shelf);
+        $this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
         return redirect($shelf->getUrl());
     }
-
-    /**
-     * Common actions to run on bookshelf update.
-     * @param Bookshelf $shelf
-     * @param Request $request
-     * @throws \BookStack\Exceptions\ImageUploadException
-     */
-    protected function shelfUpdateActions(Bookshelf $shelf, Request $request)
-    {
-        // Update the books that the shelf references
-        $this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
-
-        // Update the cover image if in request
-        if ($request->has('image')) {
-            $newImage = $request->file('image');
-            $this->imageRepo->destroyImage($shelf->cover);
-            $image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
-            $shelf->image_id = $image->id;
-            $shelf->save();
-        }
-
-        if ($request->has('image_reset')) {
-            $this->imageRepo->destroyImage($shelf->cover);
-            $shelf->image_id = 0;
-            $shelf->save();
-        }
-    }
 }
index c19e45694850f7418ca8f5c98c5ae15a024847f9..1355979107eb0181d272e3610511688d5772b7b7 100644 (file)
@@ -1,83 +1,74 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Exceptions\MoveOperationException;
+use BookStack\Exceptions\NotFoundException;
 use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
+use Throwable;
 use Views;
 
 class ChapterController extends Controller
 {
 
-    protected $userRepo;
-    protected $entityRepo;
-    protected $exportService;
+    protected $chapterRepo;
 
     /**
      * ChapterController constructor.
-     * @param EntityRepo $entityRepo
-     * @param UserRepo $userRepo
-     * @param \BookStack\Entities\ExportService $exportService
      */
-    public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
+    public function __construct(ChapterRepo $chapterRepo)
     {
-        $this->entityRepo = $entityRepo;
-        $this->userRepo = $userRepo;
-        $this->exportService = $exportService;
+        $this->chapterRepo = $chapterRepo;
         parent::__construct();
     }
 
     /**
      * Show the form for creating a new chapter.
-     * @param $bookSlug
-     * @return Response
      */
-    public function create($bookSlug)
+    public function create(string $bookSlug)
     {
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
         $this->checkOwnablePermission('chapter-create', $book);
+
         $this->setPageTitle(trans('entities.chapters_create'));
         return view('chapters.create', ['book' => $book, 'current' => $book]);
     }
 
     /**
      * Store a newly created chapter in storage.
-     * @param          $bookSlug
-     * @param  Request $request
-     * @return Response
+     * @throws ValidationException
      */
-    public function store($bookSlug, Request $request)
+    public function store(Request $request, string $bookSlug)
     {
         $this->validate($request, [
             'name' => 'required|string|max:255'
         ]);
 
-        $book = $this->entityRepo->getBySlug('book', $bookSlug);
+        $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
         $this->checkOwnablePermission('chapter-create', $book);
 
-        $input = $request->all();
-        $input['priority'] = $this->entityRepo->getNewBookPriority($book);
-        $chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
+        $chapter = $this->chapterRepo->create($request->all(), $book);
         Activity::add($chapter, 'chapter_create', $book->id);
+
         return redirect($chapter->getUrl());
     }
 
     /**
      * Display the specified chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return Response
      */
-    public function show($bookSlug, $chapterSlug)
+    public function show(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-view', $chapter);
-        $sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
+
+        $sidebarTree = (new BookContents($chapter->book))->getTree();
+        $pages = $chapter->getVisiblePages();
         Views::add($chapter);
+
         $this->setPageTitle($chapter->getShortName());
-        $pages = $this->entityRepo->getChapterChildren($chapter);
         return view('chapters.show', [
             'book' => $chapter->book,
             'chapter' => $chapter,
@@ -89,79 +80,71 @@ class ChapterController extends Controller
 
     /**
      * Show the form for editing the specified chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return Response
      */
-    public function edit($bookSlug, $chapterSlug)
+    public function edit(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
+
         $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
         return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
     }
 
     /**
      * Update the specified chapter in storage.
-     * @param  Request $request
-     * @param          $bookSlug
-     * @param          $chapterSlug
-     * @return Response
-     * @throws \BookStack\Exceptions\NotFoundException
+     * @throws NotFoundException
      */
-    public function update(Request $request, $bookSlug, $chapterSlug)
+    public function update(Request $request, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
 
-        $this->entityRepo->updateFromInput('chapter', $chapter, $request->all());
+        $this->chapterRepo->update($chapter, $request->all());
         Activity::add($chapter, 'chapter_update', $chapter->book->id);
+
         return redirect($chapter->getUrl());
     }
 
     /**
      * Shows the page to confirm deletion of this chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return \Illuminate\View\View
+     * @throws NotFoundException
      */
-    public function showDelete($bookSlug, $chapterSlug)
+    public function showDelete(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
+
         $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
         return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
     }
 
     /**
      * Remove the specified chapter from storage.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return Response
+     * @throws NotFoundException
+     * @throws Throwable
      */
-    public function destroy($bookSlug, $chapterSlug)
+    public function destroy(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-        $book = $chapter->book;
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-delete', $chapter);
-        Activity::addMessage('chapter_delete', $book->id, $chapter->name);
-        $this->entityRepo->destroyChapter($chapter);
-        return redirect($book->getUrl());
+
+        Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
+        $this->chapterRepo->destroy($chapter);
+
+        return redirect($chapter->book->getUrl());
     }
 
     /**
      * Show the page for moving a chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return mixed
-     * @throws \BookStack\Exceptions\NotFoundException
+     * @throws NotFoundException
      */
-    public function showMove($bookSlug, $chapterSlug)
+    public function showMove(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
         $this->checkOwnablePermission('chapter-update', $chapter);
         $this->checkOwnablePermission('chapter-delete', $chapter);
+
         return view('chapters.move', [
             'chapter' => $chapter,
             'book' => $chapter->book
@@ -170,15 +153,11 @@ class ChapterController extends Controller
 
     /**
      * Perform the move action for a chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @param Request $request
-     * @return mixed
-     * @throws \BookStack\Exceptions\NotFoundException
+     * @throws NotFoundException
      */
-    public function move($bookSlug, $chapterSlug, Request $request)
+    public function move(Request $request, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
         $this->checkOwnablePermission('chapter-delete', $chapter);
 
@@ -187,100 +166,47 @@ class ChapterController extends Controller
             return redirect($chapter->getUrl());
         }
 
-        $stringExploded = explode(':', $entitySelection);
-        $entityType = $stringExploded[0];
-        $entityId = intval($stringExploded[1]);
-
-        $parent = false;
-
-        if ($entityType == 'book') {
-            $parent = $this->entityRepo->getById('book', $entityId);
-        }
-
-        if ($parent === false || $parent === null) {
-            session()->flash('error', trans('errors.selected_book_not_found'));
+        try {
+            $newBook = $this->chapterRepo->move($chapter, $entitySelection);
+        } catch (MoveOperationException $exception) {
+            $this->showErrorNotification(trans('errors.selected_book_not_found'));
             return redirect()->back();
         }
 
-        $this->entityRepo->changeBook('chapter', $parent->id, $chapter, true);
-        Activity::add($chapter, 'chapter_move', $chapter->book->id);
-        session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name]));
+        Activity::add($chapter, 'chapter_move', $newBook->id);
 
+        $this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
         return redirect($chapter->getUrl());
     }
 
     /**
      * Show the Restrictions view.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
-     * @throws \BookStack\Exceptions\NotFoundException
+     * @throws NotFoundException
      */
-    public function showPermissions($bookSlug, $chapterSlug)
+    public function showPermissions(string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('restrictions-manage', $chapter);
-        $roles = $this->userRepo->getRestrictableRoles();
+
         return view('chapters.permissions', [
             'chapter' => $chapter,
-            'roles' => $roles
         ]);
     }
 
     /**
      * Set the restrictions for this chapter.
-     * @param $bookSlug
-     * @param $chapterSlug
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     * @throws \BookStack\Exceptions\NotFoundException
-     * @throws \Throwable
+     * @throws NotFoundException
      */
-    public function permissions($bookSlug, $chapterSlug, Request $request)
+    public function permissions(Request $request, string $bookSlug, string $chapterSlug)
     {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('restrictions-manage', $chapter);
-        $this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
-        session()->flash('success', trans('entities.chapters_permissions_success'));
-        return redirect($chapter->getUrl());
-    }
 
-    /**
-     * Exports a chapter to pdf .
-     * @param string $bookSlug
-     * @param string $chapterSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportPdf($bookSlug, $chapterSlug)
-    {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-        $pdfContent = $this->exportService->chapterToPdf($chapter);
-        return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
-    }
+        $restricted = $request->get('restricted') === 'true';
+        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+        $this->chapterRepo->updatePermissions($chapter, $restricted, $permissions);
 
-    /**
-     * Export a chapter to a self-contained HTML file.
-     * @param string $bookSlug
-     * @param string $chapterSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportHtml($bookSlug, $chapterSlug)
-    {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-        $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
-        return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
-    }
-
-    /**
-     * Export a chapter to a simple plaintext .txt file.
-     * @param string $bookSlug
-     * @param string $chapterSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportPlainText($bookSlug, $chapterSlug)
-    {
-        $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-        $chapterText = $this->exportService->chapterToPlainText($chapter);
-        return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
+        $this->showSuccessNotification(trans('entities.chapters_permissions_success'));
+        return redirect($chapter->getUrl());
     }
 }
diff --git a/app/Http/Controllers/ChapterExportController.php b/app/Http/Controllers/ChapterExportController.php
new file mode 100644 (file)
index 0000000..0c86f85
--- /dev/null
@@ -0,0 +1,58 @@
+<?php namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Exceptions\NotFoundException;
+use Throwable;
+
+class ChapterExportController extends Controller
+{
+
+    protected $chapterRepo;
+    protected $exportService;
+
+    /**
+     * ChapterExportController constructor.
+     */
+    public function __construct(ChapterRepo $chapterRepo, ExportService $exportService)
+    {
+        $this->chapterRepo = $chapterRepo;
+        $this->exportService = $exportService;
+        parent::__construct();
+    }
+
+    /**
+     * Exports a chapter to pdf.
+     * @throws NotFoundException
+     * @throws Throwable
+     */
+    public function pdf(string $bookSlug, string $chapterSlug)
+    {
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $pdfContent = $this->exportService->chapterToPdf($chapter);
+        return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
+    }
+
+    /**
+     * Export a chapter to a self-contained HTML file.
+     * @throws NotFoundException
+     * @throws Throwable
+     */
+    public function html(string $bookSlug, string $chapterSlug)
+    {
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
+        return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
+    }
+
+    /**
+     * Export a chapter to a simple plaintext .txt file.
+     * @throws NotFoundException
+     */
+    public function plainText(string $bookSlug, string $chapterSlug)
+    {
+        $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+        $chapterText = $this->exportService->chapterToPlainText($chapter);
+        return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
+    }
+}
index 860b507623fb6b5d09cd6a2a1f1a4aea5021fc35..068358d72d10af92de45327e0b207093c9c501fd 100644 (file)
@@ -2,44 +2,36 @@
 
 use Activity;
 use BookStack\Actions\CommentRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use Illuminate\Database\Eloquent\ModelNotFoundException;
+use BookStack\Entities\Page;
 use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
 
 class CommentController extends Controller
 {
-    protected $entityRepo;
     protected $commentRepo;
 
     /**
      * CommentController constructor.
-     * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
-     * @param \BookStack\Actions\CommentRepo $commentRepo
      */
-    public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
+    public function __construct(CommentRepo $commentRepo)
     {
-        $this->entityRepo = $entityRepo;
         $this->commentRepo = $commentRepo;
         parent::__construct();
     }
 
     /**
      * Save a new comment for a Page
-     * @param Request $request
-     * @param integer $pageId
-     * @param null|integer $commentId
-     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
+     * @throws ValidationException
      */
-    public function savePageComment(Request $request, $pageId, $commentId = null)
+    public function savePageComment(Request $request, int $pageId, int $commentId = null)
     {
         $this->validate($request, [
             'text' => 'required|string',
             'html' => 'required|string',
         ]);
 
-        try {
-            $page = $this->entityRepo->getById('page', $pageId, true);
-        } catch (ModelNotFoundException $e) {
+        $page = Page::visible()->find($pageId);
+        if ($page === null) {
             return response('Not found', 404);
         }
 
@@ -59,11 +51,9 @@ class CommentController extends Controller
 
     /**
      * Update an existing comment.
-     * @param Request $request
-     * @param integer $commentId
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     * @throws ValidationException
      */
-    public function update(Request $request, $commentId)
+    public function update(Request $request, int $commentId)
     {
         $this->validate($request, [
             'text' => 'required|string',
@@ -80,13 +70,12 @@ class CommentController extends Controller
 
     /**
      * Delete a comment from the system.
-     * @param integer $id
-     * @return \Illuminate\Http\JsonResponse
      */
-    public function destroy($id)
+    public function destroy(int $id)
     {
         $comment = $this->commentRepo->getById($id);
         $this->checkOwnablePermission('comment-delete', $comment);
+
         $this->commentRepo->delete($comment);
         return response()->json(['message' => trans('entities.comment_deleted')]);
     }
index 5bc62c601a73bcf63913a2bac3bd63c25dbab2e1..b9576f2febd7923293b8eacb105bab6130f118c1 100644 (file)
@@ -2,7 +2,6 @@
 
 namespace BookStack\Http\Controllers;
 
-use BookStack\Auth\User;
 use BookStack\Ownable;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Foundation\Validation\ValidatesRequests;
@@ -14,42 +13,27 @@ abstract class Controller extends BaseController
 {
     use DispatchesJobs, ValidatesRequests;
 
-    /**
-     * @var User static
-     */
-    protected $currentUser;
-    /**
-     * @var bool
-     */
-    protected $signedIn;
-
     /**
      * Controller constructor.
      */
     public function __construct()
     {
-        $this->middleware(function ($request, $next) {
-
-            // Get a user instance for the current user
-            $user = user();
-
-            // Share variables with controllers
-            $this->currentUser = $user;
-            $this->signedIn = auth()->check();
-
-            // Share variables with views
-            view()->share('signedIn', $this->signedIn);
-            view()->share('currentUser', $user);
+        //
+    }
 
-            return $next($request);
-        });
+    /**
+     * Check if the current user is signed in.
+     */
+    protected function isSignedIn(): bool
+    {
+        return auth()->check();
     }
 
     /**
      * Stops the application and shows a permission error if
      * the application is in demo mode.
      */
-    protected function preventAccessForDemoUsers()
+    protected function preventAccessInDemoMode()
     {
         if (config('app.env') === 'demo') {
             $this->showPermissionError();
@@ -75,7 +59,7 @@ abstract class Controller extends BaseController
             $response = response()->json(['error' => trans('errors.permissionJson')], 403);
         } else {
             $response = redirect('/');
-            session()->flash('error', trans('errors.permission'));
+            $this->showErrorNotification(trans('errors.permission'));
         }
 
         throw new HttpResponseException($response);
@@ -133,7 +117,7 @@ abstract class Controller extends BaseController
     protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
     {
         return $this->checkPermissionOr($permissionName, function () use ($userId) {
-            return $userId === $this->currentUser->id;
+            return $userId === user()->id;
         });
     }
 
@@ -145,7 +129,7 @@ abstract class Controller extends BaseController
      */
     protected function jsonError($messageText = "", $statusCode = 500)
     {
-        return response()->json(['message' => $messageText], $statusCode);
+        return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
     }
 
     /**
@@ -178,4 +162,39 @@ abstract class Controller extends BaseController
             'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
         ]);
     }
+
+    /**
+     * Show a positive, successful notification to the user on next view load.
+     * @param string $message
+     */
+    protected function showSuccessNotification(string $message)
+    {
+        session()->flash('success', $message);
+    }
+
+    /**
+     * Show a warning notification to the user on next view load.
+     * @param string $message
+     */
+    protected function showWarningNotification(string $message)
+    {
+        session()->flash('warning', $message);
+    }
+
+    /**
+     * Show an error notification to the user on next view load.
+     * @param string $message
+     */
+    protected function showErrorNotification(string $message)
+    {
+        session()->flash('error', $message);
+    }
+
+    /**
+     * Get the validation rules for image files.
+     */
+    protected function getImageValidationRules(): string
+    {
+        return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
+    }
 }
index d2c75f95622fee3d8278629dd75b3390268dcb38..260952fd16eb8e18962bc962ac3db1acc3f13445 100644 (file)
@@ -1,23 +1,16 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Page;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Repos\BookshelfRepo;
 use Illuminate\Http\Response;
 use Views;
 
 class HomeController extends Controller
 {
-    protected $entityRepo;
-
-    /**
-     * HomeController constructor.
-     * @param EntityRepo $entityRepo
-     */
-    public function __construct(EntityRepo $entityRepo)
-    {
-        $this->entityRepo = $entityRepo;
-        parent::__construct();
-    }
 
     /**
      * Display the homepage.
@@ -26,10 +19,20 @@ class HomeController extends Controller
     public function index()
     {
         $activity = Activity::latest(10);
-        $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
+        $draftPages = [];
+
+        if ($this->isSignedIn()) {
+            $draftPages = Page::visible()->where('draft', '=', true)
+                ->where('created_by', '=', user()->id)
+                ->orderBy('updated_at', 'desc')->take(6)->get();
+        }
+
         $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
-        $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
-        $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
+        $recents = $this->isSignedIn() ?
+              Views::getUserRecentlyViewed(12*$recentFactor, 0)
+            : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
+        $recentlyUpdatedPages = Page::visible()->where('draft', false)
+            ->orderBy('updated_at', 'desc')->take(12)->get();
 
         $homepageOptions = ['default', 'books', 'bookshelves', 'page'];
         $homepageOption = setting('app-homepage-type', 'default');
@@ -47,9 +50,9 @@ class HomeController extends Controller
         // Add required list ordering & sorting for books & shelves views.
         if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
             $key = $homepageOption;
-            $view = setting()->getUser($this->currentUser, $key . '_view_type', config('app.views.' . $key));
-            $sort = setting()->getUser($this->currentUser, $key . '_sort', 'name');
-            $order = setting()->getUser($this->currentUser, $key . '_sort_order', 'asc');
+            $view = setting()->getForCurrentUser($key . '_view_type', config('app.views.' . $key));
+            $sort = setting()->getForCurrentUser($key . '_sort', 'name');
+            $order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
 
             $sortOptions = [
                 'name' => trans('common.sort_name'),
@@ -66,16 +69,18 @@ class HomeController extends Controller
         }
 
         if ($homepageOption === 'bookshelves') {
-            $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
+            $shelfRepo = app(BookshelfRepo::class);
+            $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
             foreach ($shelves as $shelf) {
-                $shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
+                $shelf->books = $shelf->visibleBooks;
             }
             $data = array_merge($commonData, ['shelves' => $shelves]);
             return view('common.home-shelves', $data);
         }
 
         if ($homepageOption === 'books') {
-            $books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
+            $bookRepo = app(BookRepo::class);
+            $books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']);
             $data = array_merge($commonData, ['books' => $books]);
             return view('common.home-book', $data);
         }
@@ -83,8 +88,9 @@ class HomeController extends Controller
         if ($homepageOption === 'page') {
             $homepageSetting = setting('app-homepage', '0:');
             $id = intval(explode(':', $homepageSetting)[0]);
-            $customHomepage = $this->entityRepo->getById('page', $id, false, true);
-            $this->entityRepo->renderPage($customHomepage, true);
+            $customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id);
+            $pageContent = new PageContent($customHomepage);
+            $customHomepage->html = $pageContent->render(true);
             return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
         }
 
index 024003f87d13c4a40611b7942ce83d3cbdcf9a43..9c67704dd339bd0b21d3471b751206625386869b 100644 (file)
@@ -1,6 +1,6 @@
 <?php namespace BookStack\Http\Controllers\Images;
 
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Page;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Http\Controllers\Controller;
 use BookStack\Repos\PageRepo;
@@ -47,13 +47,13 @@ class ImageController extends Controller
 
     /**
      * Update image details
-     * @param integer $id
      * @param Request $request
+     * @param integer $id
      * @return \Illuminate\Http\JsonResponse
      * @throws ImageUploadException
      * @throws \Exception
      */
-    public function update($id, Request $request)
+    public function update(Request $request, $id)
     {
         $this->validate($request, [
             'name' => 'required|min:2|string'
@@ -69,16 +69,21 @@ class ImageController extends Controller
 
     /**
      * Show the usage of an image on pages.
-     * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
-     * @param $id
-     * @return \Illuminate\Http\JsonResponse
      */
-    public function usage(EntityRepo $entityRepo, $id)
+    public function usage(int $id)
     {
         $image = $this->imageRepo->getById($id);
         $this->checkImagePermission($image);
-        $pageSearch = $entityRepo->searchForImage($image->url);
-        return response()->json($pageSearch);
+
+        $pages = Page::visible()->where('html', 'like', '%' . $image->url . '%')->get(['id', 'name', 'slug', 'book_id']);
+        foreach ($pages as $page) {
+            $page->url = $page->getUrl();
+            $page->html = '';
+            $page->text = '';
+        }
+        $result = count($pages) > 0 ? $pages : false;
+
+        return response()->json($result);
     }
 
     /**
index ad1e3266597f2183aa0cd72a4b668d052e7eb2a8..630f888ed3e9dc0d16da96b622a03278b527cfb9 100644 (file)
@@ -1,61 +1,46 @@
 <?php namespace BookStack\Http\Controllers;
 
 use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Managers\PageEditActivity;
+use BookStack\Entities\Page;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Exceptions\NotFoundException;
-use GatherContent\Htmldiff\Htmldiff;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Exceptions\PermissionsException;
+use Exception;
 use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
+use Throwable;
 use Views;
 
 class PageController extends Controller
 {
 
     protected $pageRepo;
-    protected $exportService;
-    protected $userRepo;
 
     /**
      * PageController constructor.
-     * @param \BookStack\Entities\Repos\PageRepo $pageRepo
-     * @param \BookStack\Entities\ExportService $exportService
-     * @param UserRepo $userRepo
      */
-    public function __construct(PageRepo $pageRepo, ExportService $exportService, UserRepo $userRepo)
+    public function __construct(PageRepo $pageRepo)
     {
         $this->pageRepo = $pageRepo;
-        $this->exportService = $exportService;
-        $this->userRepo = $userRepo;
         parent::__construct();
     }
 
     /**
      * Show the form for creating a new page.
-     * @param string $bookSlug
-     * @param string $chapterSlug
-     * @return Response
-     * @internal param bool $pageSlug
-     * @throws NotFoundException
+     * @throws Throwable
      */
-    public function create($bookSlug, $chapterSlug = null)
+    public function create(string $bookSlug, string $chapterSlug = null)
     {
-        if ($chapterSlug !== null) {
-            $chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-            $book = $chapter->book;
-        } else {
-            $chapter = null;
-            $book = $this->pageRepo->getBySlug('book', $bookSlug);
-        }
-
-        $parent = $chapter ? $chapter : $book;
+        $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('page-create', $parent);
 
         // Redirect to draft edit screen if signed in
-        if ($this->signedIn) {
-            $draft = $this->pageRepo->getDraftPage($book, $chapter);
+        if ($this->isSignedIn()) {
+            $draft = $this->pageRepo->getNewDraftPage($parent);
             return redirect($draft->getUrl());
         }
 
@@ -66,51 +51,38 @@ class PageController extends Controller
 
     /**
      * Create a new page as a guest user.
-     * @param Request $request
-     * @param string $bookSlug
-     * @param string|null $chapterSlug
-     * @return mixed
-     * @throws NotFoundException
+     * @throws ValidationException
      */
-    public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
+    public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
     {
         $this->validate($request, [
             'name' => 'required|string|max:255'
         ]);
 
-        if ($chapterSlug !== null) {
-            $chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
-            $book = $chapter->book;
-        } else {
-            $chapter = null;
-            $book = $this->pageRepo->getBySlug('book', $bookSlug);
-        }
-
-        $parent = $chapter ? $chapter : $book;
+        $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
         $this->checkOwnablePermission('page-create', $parent);
 
-        $page = $this->pageRepo->getDraftPage($book, $chapter);
-        $this->pageRepo->publishPageDraft($page, [
+        $page = $this->pageRepo->getNewDraftPage($parent);
+        $this->pageRepo->publishDraft($page, [
             'name' => $request->get('name'),
             'html' => ''
         ]);
+
         return redirect($page->getUrl('/edit'));
     }
 
     /**
      * Show form to continue editing a draft page.
-     * @param string $bookSlug
-     * @param int $pageId
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     * @throws NotFoundException
      */
-    public function editDraft($bookSlug, $pageId)
+    public function editDraft(string $bookSlug, int $pageId)
     {
-        $draft = $this->pageRepo->getById('page', $pageId, true);
-        $this->checkOwnablePermission('page-create', $draft->parent);
+        $draft = $this->pageRepo->getById($pageId);
+        $this->checkOwnablePermission('page-create', $draft->parent());
         $this->setPageTitle(trans('entities.pages_edit_draft'));
 
-        $draftsEnabled = $this->signedIn;
-        $templates = $this->pageRepo->getPageTemplates(10);
+        $draftsEnabled = $this->isSignedIn();
+        $templates = $this->pageRepo->getTemplates(10);
 
         return view('pages.edit', [
             'page' => $draft,
@@ -123,63 +95,50 @@ class PageController extends Controller
 
     /**
      * Store a new page by changing a draft into a page.
-     * @param  Request $request
-     * @param  string $bookSlug
-     * @param  int $pageId
-     * @return Response
+     * @throws NotFoundException
+     * @throws ValidationException
      */
-    public function store(Request $request, $bookSlug, $pageId)
+    public function store(Request $request, string $bookSlug, int $pageId)
     {
         $this->validate($request, [
             'name' => 'required|string|max:255'
         ]);
+        $draftPage = $this->pageRepo->getById($pageId);
+        $this->checkOwnablePermission('page-create', $draftPage->parent());
 
-        $input = $request->all();
-        $draftPage = $this->pageRepo->getById('page', $pageId, true);
-        $book = $draftPage->book;
-
-        $parent = $draftPage->parent;
-        $this->checkOwnablePermission('page-create', $parent);
-
-        if ($parent->isA('chapter')) {
-            $input['priority'] = $this->pageRepo->getNewChapterPriority($parent);
-        } else {
-            $input['priority'] = $this->pageRepo->getNewBookPriority($parent);
-        }
-
-        $page = $this->pageRepo->publishPageDraft($draftPage, $input);
+        $page = $this->pageRepo->publishDraft($draftPage, $request->all());
+        Activity::add($page, 'page_create', $draftPage->book->id);
 
-        Activity::add($page, 'page_create', $book->id);
         return redirect($page->getUrl());
     }
 
     /**
      * Display the specified page.
      * If the page is not found via the slug the revisions are searched for a match.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return Response
      * @throws NotFoundException
      */
-    public function show($bookSlug, $pageSlug)
+    public function show(string $bookSlug, string $pageSlug)
     {
         try {
-            $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+            $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         } catch (NotFoundException $e) {
-            $page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug);
+            $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
+
             if ($page === null) {
                 throw $e;
             }
+
             return redirect($page->getUrl());
         }
 
         $this->checkOwnablePermission('page-view', $page);
 
-        $page->html = $this->pageRepo->renderPage($page);
-        $sidebarTree = $this->pageRepo->getBookChildren($page->book);
-        $pageNav = $this->pageRepo->getPageNav($page->html);
+        $pageContent = (new PageContent($page));
+        $page->html = $pageContent->render();
+        $sidebarTree = (new BookContents($page->book))->getTree();
+        $pageNav = $pageContent->getNavigation($page->html);
 
-        // check if the comment's are enabled
+        // Check if page comments are enabled
         $commentsEnabled = !setting('app-disable-comments');
         if ($commentsEnabled) {
             $page->load(['comments.createdBy']);
@@ -188,7 +147,8 @@ class PageController extends Controller
         Views::add($page);
         $this->setPageTitle($page->getShortName());
         return view('pages.show', [
-            'page' => $page,'book' => $page->book,
+            'page' => $page,
+            'book' => $page->book,
             'current' => $page,
             'sidebarTree' => $sidebarTree,
             'commentsEnabled' => $commentsEnabled,
@@ -198,52 +158,47 @@ class PageController extends Controller
 
     /**
      * Get page from an ajax request.
-     * @param int $pageId
-     * @return \Illuminate\Http\JsonResponse
+     * @throws NotFoundException
      */
-    public function getPageAjax($pageId)
+    public function getPageAjax(int $pageId)
     {
-        $page = $this->pageRepo->getById('page', $pageId);
+        $page = $this->pageRepo->getById($pageId);
         return response()->json($page);
     }
 
     /**
      * Show the form for editing the specified page.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return Response
      * @throws NotFoundException
      */
-    public function edit($bookSlug, $pageSlug)
+    public function edit(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
-        $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
+
         $page->isDraft = false;
+        $editActivity = new PageEditActivity($page);
 
         // Check for active editing
         $warnings = [];
-        if ($this->pageRepo->isPageEditingActive($page, 60)) {
-            $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
+        if ($editActivity->hasActiveEditing()) {
+            $warnings[] = $editActivity->activeEditingMessage();
         }
 
         // Check for a current draft version for this user
-        $userPageDraft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
-        if ($userPageDraft !== null) {
-            $page->name = $userPageDraft->name;
-            $page->html = $userPageDraft->html;
-            $page->markdown = $userPageDraft->markdown;
+        $userDraft = $this->pageRepo->getUserDraft($page);
+        if ($userDraft !== null) {
+            $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
             $page->isDraft = true;
-            $warnings [] = $this->pageRepo->getUserPageDraftMessage($userPageDraft);
+            $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
         }
 
         if (count($warnings) > 0) {
-            session()->flash('warning', implode("\n", $warnings));
+            $this->showWarningNotification(implode("\n", $warnings));
         }
 
-        $draftsEnabled = $this->signedIn;
-        $templates = $this->pageRepo->getPageTemplates(10);
-
+        $templates = $this->pageRepo->getTemplates(10);
+        $draftsEnabled = $this->isSignedIn();
+        $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
         return view('pages.edit', [
             'page' => $page,
             'book' => $page->book,
@@ -255,39 +210,34 @@ class PageController extends Controller
 
     /**
      * Update the specified page in storage.
-     * @param  Request $request
-     * @param  string $bookSlug
-     * @param  string $pageSlug
-     * @return Response
+     * @throws ValidationException
+     * @throws NotFoundException
      */
-    public function update(Request $request, $bookSlug, $pageSlug)
+    public function update(Request $request, string $bookSlug, string $pageSlug)
     {
         $this->validate($request, [
             'name' => 'required|string|max:255'
         ]);
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
-        $this->pageRepo->updatePage($page, $page->book->id, $request->all());
+
+        $this->pageRepo->update($page, $request->all());
         Activity::add($page, 'page_update', $page->book->id);
+
         return redirect($page->getUrl());
     }
 
     /**
      * Save a draft update as a revision.
-     * @param Request $request
-     * @param int $pageId
-     * @return \Illuminate\Http\JsonResponse
+     * @throws NotFoundException
      */
-    public function saveDraft(Request $request, $pageId)
+    public function saveDraft(Request $request, int $pageId)
     {
-        $page = $this->pageRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $this->checkOwnablePermission('page-update', $page);
 
-        if (!$this->signedIn) {
-            return response()->json([
-                'status' => 'error',
-                'message' => trans('errors.guests_cannot_save_drafts'),
-            ], 500);
+        if (!$this->isSignedIn()) {
+            return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
         }
 
         $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
@@ -301,253 +251,98 @@ class PageController extends Controller
     }
 
     /**
-     * Redirect from a special link url which
-     * uses the page id rather than the name.
-     * @param int $pageId
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     * Redirect from a special link url which uses the page id rather than the name.
+     * @throws NotFoundException
      */
-    public function redirectFromLink($pageId)
+    public function redirectFromLink(int $pageId)
     {
-        $page = $this->pageRepo->getById('page', $pageId);
+        $page = $this->pageRepo->getById($pageId);
         return redirect($page->getUrl());
     }
 
     /**
      * Show the deletion page for the specified page.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\View\View
+     * @throws NotFoundException
      */
-    public function showDelete($bookSlug, $pageSlug)
+    public function showDelete(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-delete', $page);
         $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
-        return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
+        return view('pages.delete', [
+            'book' => $page->book,
+            'page' => $page,
+            'current' => $page
+        ]);
     }
 
-
     /**
      * Show the deletion page for the specified page.
-     * @param string $bookSlug
-     * @param int $pageId
-     * @return \Illuminate\View\View
      * @throws NotFoundException
      */
-    public function showDeleteDraft($bookSlug, $pageId)
+    public function showDeleteDraft(string $bookSlug, int $pageId)
     {
-        $page = $this->pageRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $this->checkOwnablePermission('page-update', $page);
         $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
-        return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
+        return view('pages.delete', [
+            'book' => $page->book,
+            'page' => $page,
+            'current' => $page
+        ]);
     }
 
     /**
      * Remove the specified page from storage.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return Response
-     * @internal param int $id
+     * @throws NotFoundException
+     * @throws Throwable
+     * @throws NotifyException
      */
-    public function destroy($bookSlug, $pageSlug)
+    public function destroy(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $book = $page->book;
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-delete', $page);
-        $this->pageRepo->destroyPage($page);
 
-        Activity::addMessage('page_delete', $book->id, $page->name);
-        session()->flash('success', trans('entities.pages_delete_success'));
+        $book = $page->book;
+        $this->pageRepo->destroy($page);
+        Activity::addMessage('page_delete', $page->name, $book->id);
+
+        $this->showSuccessNotification(trans('entities.pages_delete_success'));
         return redirect($book->getUrl());
     }
 
     /**
      * Remove the specified draft page from storage.
-     * @param string $bookSlug
-     * @param int $pageId
-     * @return Response
      * @throws NotFoundException
+     * @throws NotifyException
+     * @throws Throwable
      */
-    public function destroyDraft($bookSlug, $pageId)
+    public function destroyDraft(string $bookSlug, int $pageId)
     {
-        $page = $this->pageRepo->getById('page', $pageId, true);
+        $page = $this->pageRepo->getById($pageId);
         $book = $page->book;
+        $chapter = $page->chapter;
         $this->checkOwnablePermission('page-update', $page);
-        session()->flash('success', trans('entities.pages_delete_draft_success'));
-        $this->pageRepo->destroyPage($page);
-        return redirect($book->getUrl());
-    }
-
-    /**
-     * Shows the last revisions for this page.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\View\View
-     * @throws NotFoundException
-     */
-    public function showRevisions($bookSlug, $pageSlug)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
-        return view('pages.revisions', ['page' => $page, 'current' => $page]);
-    }
 
-    /**
-     * Shows a preview of a single revision
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param int $revisionId
-     * @return \Illuminate\View\View
-     */
-    public function showRevision($bookSlug, $pageSlug, $revisionId)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
-        if ($revision === null) {
-            abort(404);
-        }
+        $this->pageRepo->destroy($page);
 
-        $page->fill($revision->toArray());
-        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
+        $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
 
-        return view('pages.revision', [
-            'page' => $page,
-            'book' => $page->book,
-            'diff' => null,
-            'revision' => $revision
-        ]);
-    }
-
-    /**
-     * Shows the changes of a single revision
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param int $revisionId
-     * @return \Illuminate\View\View
-     */
-    public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
-        if ($revision === null) {
-            abort(404);
+        if ($chapter && userCan('view', $chapter)) {
+            return redirect($chapter->getUrl());
         }
-
-        $prev = $revision->getPrevious();
-        $prevContent = ($prev === null) ? '' : $prev->html;
-        $diff = (new Htmldiff)->diff($prevContent, $revision->html);
-
-        $page->fill($revision->toArray());
-        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
-
-        return view('pages.revision', [
-            'page' => $page,
-            'book' => $page->book,
-            'diff' => $diff,
-            'revision' => $revision
-        ]);
-    }
-
-    /**
-     * Restores a page using the content of the specified revision.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param int $revisionId
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     */
-    public function restoreRevision($bookSlug, $pageSlug, $revisionId)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $this->checkOwnablePermission('page-update', $page);
-        $page = $this->pageRepo->restorePageRevision($page, $page->book, $revisionId);
-        Activity::add($page, 'page_restore', $page->book->id);
-        return redirect($page->getUrl());
-    }
-
-
-    /**
-     * Deletes a revision using the id of the specified revision.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param int $revId
-     * @throws NotFoundException
-     * @throws BadRequestException
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     */
-    public function destroyRevision($bookSlug, $pageSlug, $revId)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $this->checkOwnablePermission('page-delete', $page);
-
-        $revision = $page->revisions()->where('id', '=', $revId)->first();
-        if ($revision === null) {
-            throw new NotFoundException("Revision #{$revId} not found");
-        }
-
-        // Get the current revision for the page
-        $currentRevision = $page->getCurrentRevision();
-
-        // Check if its the latest revision, cannot delete latest revision.
-        if (intval($currentRevision->id) === intval($revId)) {
-            session()->flash('error', trans('entities.revision_cannot_delete_latest'));
-            return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
-        }
-
-        $revision->delete();
-        session()->flash('success', trans('entities.revision_delete_success'));
-        return redirect($page->getUrl('/revisions'));
-    }
-
-    /**
-     * Exports a page to a PDF.
-     * https://github.com/barryvdh/laravel-dompdf
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportPdf($bookSlug, $pageSlug)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $page->html = $this->pageRepo->renderPage($page);
-        $pdfContent = $this->exportService->pageToPdf($page);
-        return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
-    }
-
-    /**
-     * Export a page to a self-contained HTML file.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportHtml($bookSlug, $pageSlug)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $page->html = $this->pageRepo->renderPage($page);
-        $containedHtml = $this->exportService->pageToContainedHtml($page);
-        return $this->downloadResponse($containedHtml, $pageSlug . '.html');
-    }
-
-    /**
-     * Export a page to a simple plaintext .txt file.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\Http\Response
-     */
-    public function exportPlainText($bookSlug, $pageSlug)
-    {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $pageText = $this->exportService->pageToPlainText($page);
-        return $this->downloadResponse($pageText, $pageSlug . '.txt');
+        return redirect($book->getUrl());
     }
 
     /**
-     * Show a listing of recently created pages
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     * Show a listing of recently created pages.
      */
     public function showRecentlyUpdated()
     {
-        // TODO - Still exist?
-        $pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(url('/pages/recently-updated'));
+        $pages = Page::visible()->orderBy('updated_at', 'desc')
+            ->paginate(20)
+            ->setPath(url('/pages/recently-updated'));
+
         return view('pages.detailed-listing', [
             'title' => trans('entities.recently_updated_pages'),
             'pages' => $pages
@@ -556,14 +351,11 @@ class PageController extends Controller
 
     /**
      * Show the view to choose a new parent to move a page into.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return mixed
      * @throws NotFoundException
      */
-    public function showMove($bookSlug, $pageSlug)
+    public function showMove(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
         $this->checkOwnablePermission('page-delete', $page);
         return view('pages.move', [
@@ -573,16 +365,13 @@ class PageController extends Controller
     }
 
     /**
-     * Does the action of moving the location of a page
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param Request $request
-     * @return mixed
+     * Does the action of moving the location of a page.
      * @throws NotFoundException
+     * @throws Throwable
      */
-    public function move($bookSlug, $pageSlug, Request $request)
+    public function move(Request $request, string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-update', $page);
         $this->checkOwnablePermission('page-delete', $page);
 
@@ -591,37 +380,29 @@ class PageController extends Controller
             return redirect($page->getUrl());
         }
 
-        $stringExploded = explode(':', $entitySelection);
-        $entityType = $stringExploded[0];
-        $entityId = intval($stringExploded[1]);
-
-
         try {
-            $parent = $this->pageRepo->getById($entityType, $entityId);
-        } catch (\Exception $e) {
-            session()->flash(trans('entities.selected_book_chapter_not_found'));
+            $parent = $this->pageRepo->move($page, $entitySelection);
+        } catch (Exception $exception) {
+            if ($exception instanceof  PermissionsException) {
+                $this->showPermissionError();
+            }
+
+            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
             return redirect()->back();
         }
 
-        $this->checkOwnablePermission('page-create', $parent);
-
-        $this->pageRepo->changePageParent($page, $parent);
         Activity::add($page, 'page_move', $page->book->id);
-        session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
-
+        $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
         return redirect($page->getUrl());
     }
 
     /**
      * Show the view to copy a page.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return mixed
      * @throws NotFoundException
      */
-    public function showCopy($bookSlug, $pageSlug)
+    public function showCopy(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-view', $page);
         session()->flashInput(['name' => $page->name]);
         return view('pages.copy', [
@@ -630,78 +411,65 @@ class PageController extends Controller
         ]);
     }
 
+
     /**
      * Create a copy of a page within the requested target destination.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param Request $request
-     * @return mixed
      * @throws NotFoundException
+     * @throws Throwable
      */
-    public function copy($bookSlug, $pageSlug, Request $request)
+    public function copy(Request $request, string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('page-view', $page);
 
-        $entitySelection = $request->get('entity_selection', null);
-        if ($entitySelection === null || $entitySelection === '') {
-            $parent = $page->chapter ? $page->chapter : $page->book;
-        } else {
-            $stringExploded = explode(':', $entitySelection);
-            $entityType = $stringExploded[0];
-            $entityId = intval($stringExploded[1]);
-
-            try {
-                $parent = $this->pageRepo->getById($entityType, $entityId);
-            } catch (\Exception $e) {
-                session()->flash(trans('entities.selected_book_chapter_not_found'));
-                return redirect()->back();
-            }
-        }
+        $entitySelection = $request->get('entity_selection', null) ?? null;
+        $newName = $request->get('name', null);
 
-        $this->checkOwnablePermission('page-create', $parent);
+        try {
+            $pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
+        } catch (Exception $exception) {
+            if ($exception instanceof  PermissionsException) {
+                $this->showPermissionError();
+            }
 
-        $pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', ''));
+            $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
+            return redirect()->back();
+        }
 
         Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
-        session()->flash('success', trans('entities.pages_copy_success'));
 
+        $this->showSuccessNotification(trans('entities.pages_copy_success'));
         return redirect($pageCopy->getUrl());
     }
 
     /**
      * Show the Permissions view.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
      * @throws NotFoundException
      */
-    public function showPermissions($bookSlug, $pageSlug)
+    public function showPermissions(string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('restrictions-manage', $page);
-        $roles = $this->userRepo->getRestrictableRoles();
         return view('pages.permissions', [
             'page'  => $page,
-            'roles' => $roles
         ]);
     }
 
     /**
      * Set the permissions for this page.
-     * @param string $bookSlug
-     * @param string $pageSlug
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
      * @throws NotFoundException
-     * @throws \Throwable
+     * @throws Throwable
      */
-    public function permissions($bookSlug, $pageSlug, Request $request)
+    public function permissions(Request $request, string $bookSlug, string $pageSlug)
     {
-        $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $this->checkOwnablePermission('restrictions-manage', $page);
-        $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
-        session()->flash('success', trans('entities.pages_permissions_success'));
+
+        $restricted = $request->get('restricted') === 'true';
+        $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+        $this->pageRepo->updatePermissions($page, $restricted, $permissions);
+
+        $this->showSuccessNotification(trans('entities.pages_permissions_success'));
         return redirect($page->getUrl());
     }
 }
diff --git a/app/Http/Controllers/PageExportController.php b/app/Http/Controllers/PageExportController.php
new file mode 100644 (file)
index 0000000..3b02ea2
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Exceptions\NotFoundException;
+use Throwable;
+
+class PageExportController extends Controller
+{
+
+    protected $pageRepo;
+    protected $exportService;
+
+    /**
+     * PageExportController constructor.
+     * @param PageRepo $pageRepo
+     * @param ExportService $exportService
+     */
+    public function __construct(PageRepo $pageRepo, ExportService $exportService)
+    {
+        $this->pageRepo = $pageRepo;
+        $this->exportService = $exportService;
+        parent::__construct();
+    }
+
+    /**
+     * Exports a page to a PDF.
+     * https://github.com/barryvdh/laravel-dompdf
+     * @throws NotFoundException
+     * @throws Throwable
+     */
+    public function pdf(string $bookSlug, string $pageSlug)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page->html = (new PageContent($page))->render();
+        $pdfContent = $this->exportService->pageToPdf($page);
+        return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
+    }
+
+    /**
+     * Export a page to a self-contained HTML file.
+     * @throws NotFoundException
+     * @throws Throwable
+     */
+    public function html(string $bookSlug, string $pageSlug)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $page->html = (new PageContent($page))->render();
+        $containedHtml = $this->exportService->pageToContainedHtml($page);
+        return $this->downloadResponse($containedHtml, $pageSlug . '.html');
+    }
+
+    /**
+     * Export a page to a simple plaintext .txt file.
+     * @throws NotFoundException
+     */
+    public function plainText(string $bookSlug, string $pageSlug)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $pageText = $this->exportService->pageToPlainText($page);
+        return $this->downloadResponse($pageText, $pageSlug . '.txt');
+    }
+}
diff --git a/app/Http/Controllers/PageRevisionController.php b/app/Http/Controllers/PageRevisionController.php
new file mode 100644 (file)
index 0000000..3c65b50
--- /dev/null
@@ -0,0 +1,128 @@
+<?php namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Facades\Activity;
+use GatherContent\Htmldiff\Htmldiff;
+
+class PageRevisionController extends Controller
+{
+
+    protected $pageRepo;
+
+    /**
+     * PageRevisionController constructor.
+     */
+    public function __construct(PageRepo $pageRepo)
+    {
+        $this->pageRepo = $pageRepo;
+        parent::__construct();
+    }
+
+    /**
+     * Shows the last revisions for this page.
+     * @throws NotFoundException
+     */
+    public function index(string $bookSlug, string $pageSlug)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
+        return view('pages.revisions', [
+            'page' => $page,
+            'current' => $page
+        ]);
+    }
+
+    /**
+     * Shows a preview of a single revision.
+     * @throws NotFoundException
+     */
+    public function show(string $bookSlug, string $pageSlug, int $revisionId)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+        if ($revision === null) {
+            throw new NotFoundException();
+        }
+
+        $page->fill($revision->toArray());
+
+        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
+        return view('pages.revision', [
+            'page' => $page,
+            'book' => $page->book,
+            'diff' => null,
+            'revision' => $revision
+        ]);
+    }
+
+    /**
+     * Shows the changes of a single revision.
+     * @throws NotFoundException
+     */
+    public function changes(string $bookSlug, string $pageSlug, int $revisionId)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+        if ($revision === null) {
+            throw new NotFoundException();
+        }
+
+        $prev = $revision->getPrevious();
+        $prevContent = $prev->html ?? '';
+        $diff = (new Htmldiff)->diff($prevContent, $revision->html);
+
+        $page->fill($revision->toArray());
+        $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
+
+        return view('pages.revision', [
+            'page' => $page,
+            'book' => $page->book,
+            'diff' => $diff,
+            'revision' => $revision
+        ]);
+    }
+
+    /**
+     * Restores a page using the content of the specified revision.
+     * @throws NotFoundException
+     */
+    public function restore(string $bookSlug, string $pageSlug, int $revisionId)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $this->checkOwnablePermission('page-update', $page);
+
+        $page = $this->pageRepo->restoreRevision($page, $revisionId);
+
+        Activity::add($page, 'page_restore', $page->book->id);
+        return redirect($page->getUrl());
+    }
+
+    /**
+     * Deletes a revision using the id of the specified revision.
+     * @throws NotFoundException
+     */
+    public function destroy(string $bookSlug, string $pageSlug, int $revId)
+    {
+        $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $this->checkOwnablePermission('page-delete', $page);
+
+        $revision = $page->revisions()->where('id', '=', $revId)->first();
+        if ($revision === null) {
+            throw new NotFoundException("Revision #{$revId} not found");
+        }
+
+        // Get the current revision for the page
+        $currentRevision = $page->getCurrentRevision();
+
+        // Check if its the latest revision, cannot delete latest revision.
+        if (intval($currentRevision->id) === intval($revId)) {
+            $this->showErrorNotification(trans('entities.revision_cannot_delete_latest'));
+            return redirect($page->getUrl('/revisions'));
+        }
+
+        $revision->delete();
+        $this->showSuccessNotification(trans('entities.revision_delete_success'));
+        return redirect($page->getUrl('/revisions'));
+    }
+}
index 05943351a06f1b47a7ba36e6052eabd215e73094..eaa1a8ae26ae18f28473c6796062dfb5065dba97 100644 (file)
@@ -11,8 +11,7 @@ class PageTemplateController extends Controller
     protected $pageRepo;
 
     /**
-     * PageTemplateController constructor.
-     * @param $pageRepo
+     * PageTemplateController constructor
      */
     public function __construct(PageRepo $pageRepo)
     {
@@ -22,14 +21,12 @@ class PageTemplateController extends Controller
 
     /**
      * Fetch a list of templates from the system.
-     * @param Request $request
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
      */
     public function list(Request $request)
     {
         $page = $request->get('page', 1);
         $search = $request->get('search', '');
-        $templates = $this->pageRepo->getPageTemplates(10, $page, $search);
+        $templates = $this->pageRepo->getTemplates(10, $page, $search);
 
         if ($search) {
             $templates->appends(['search' => $search]);
@@ -42,13 +39,11 @@ class PageTemplateController extends Controller
 
     /**
      * Get the content of a template.
-     * @param $templateId
-     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
      * @throws NotFoundException
      */
-    public function get($templateId)
+    public function get(int $templateId)
     {
-        $page = $this->pageRepo->getById('page', $templateId);
+        $page = $this->pageRepo->getById($templateId);
 
         if (!$page->template) {
             throw new NotFoundException();
@@ -59,5 +54,4 @@ class PageTemplateController extends Controller
             'markdown' => $page->markdown,
         ]);
     }
-
 }
index 9893d59935ff6d284d98d5e335e2b452089de15e..148ae5cd65a3049e421ea2b331f3bfa34f31ed4d 100644 (file)
@@ -53,7 +53,7 @@ class PermissionController extends Controller
         ]);
 
         $this->permissionsRepo->saveNewRole($request->all());
-        session()->flash('success', trans('settings.role_create_success'));
+        $this->showSuccessNotification(trans('settings.role_create_success'));
         return redirect('/settings/roles');
     }
 
@@ -75,12 +75,13 @@ class PermissionController extends Controller
 
     /**
      * Updates a user role.
-     * @param $id
      * @param Request $request
+     * @param $id
      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
      * @throws PermissionsException
+     * @throws \Illuminate\Validation\ValidationException
      */
-    public function updateRole($id, Request $request)
+    public function updateRole(Request $request, $id)
     {
         $this->checkPermission('user-roles-manage');
         $this->validate($request, [
@@ -89,7 +90,7 @@ class PermissionController extends Controller
         ]);
 
         $this->permissionsRepo->updateRole($id, $request->all());
-        session()->flash('success', trans('settings.role_update_success'));
+        $this->showSuccessNotification(trans('settings.role_update_success'));
         return redirect('/settings/roles');
     }
 
@@ -112,22 +113,22 @@ class PermissionController extends Controller
     /**
      * Delete a role from the system,
      * Migrate from a previous role if set.
-     * @param $id
      * @param Request $request
+     * @param $id
      * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
      */
-    public function deleteRole($id, Request $request)
+    public function deleteRole(Request $request, $id)
     {
         $this->checkPermission('user-roles-manage');
 
         try {
             $this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
         } catch (PermissionsException $e) {
-            session()->flash('error', $e->getMessage());
+            $this->showErrorNotification($e->getMessage());
             return redirect()->back();
         }
 
-        session()->flash('success', trans('settings.role_delete_success'));
+        $this->showSuccessNotification(trans('settings.role_delete_success'));
         return redirect('/settings/roles');
     }
 }
index 1691ee9b0bf937f338436305baa8b9beea3bb24f..a5cd7ad6b82224f094173f1d76b2d4a864b48174 100644 (file)
@@ -1,35 +1,27 @@
 <?php namespace BookStack\Http\Controllers;
 
 use BookStack\Actions\ViewService;
-use BookStack\Entities\EntityContextManager;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Entity;
+use BookStack\Entities\Managers\EntityContext;
 use BookStack\Entities\SearchService;
-use BookStack\Exceptions\NotFoundException;
-use Illuminate\Contracts\View\Factory;
 use Illuminate\Http\Request;
-use Illuminate\View\View;
 
 class SearchController extends Controller
 {
-    protected $entityRepo;
     protected $viewService;
     protected $searchService;
     protected $entityContextManager;
 
     /**
      * SearchController constructor.
-     * @param EntityRepo $entityRepo
-     * @param ViewService $viewService
-     * @param SearchService $searchService
-     * @param EntityContextManager $entityContextManager
      */
     public function __construct(
-        EntityRepo $entityRepo,
         ViewService $viewService,
         SearchService $searchService,
-        EntityContextManager $entityContextManager
+        EntityContext $entityContextManager
     ) {
-        $this->entityRepo = $entityRepo;
         $this->viewService = $viewService;
         $this->searchService = $searchService;
         $this->entityContextManager = $entityContextManager;
@@ -38,9 +30,6 @@ class SearchController extends Controller
 
     /**
      * Searches all entities.
-     * @param Request $request
-     * @return View
-     * @internal param string $searchTerm
      */
     public function search(Request $request)
     {
@@ -64,12 +53,8 @@ class SearchController extends Controller
 
     /**
      * Searches all entities within a book.
-     * @param Request $request
-     * @param integer $bookId
-     * @return View
-     * @internal param string $searchTerm
      */
-    public function searchBook(Request $request, $bookId)
+    public function searchBook(Request $request, int $bookId)
     {
         $term = $request->get('term', '');
         $results = $this->searchService->searchBook($bookId, $term);
@@ -78,12 +63,8 @@ class SearchController extends Controller
 
     /**
      * Searches all entities within a chapter.
-     * @param Request $request
-     * @param integer $chapterId
-     * @return View
-     * @internal param string $searchTerm
      */
-    public function searchChapter(Request $request, $chapterId)
+    public function searchChapter(Request $request, int $chapterId)
     {
         $term = $request->get('term', '');
         $results = $this->searchService->searchChapter($chapterId, $term);
@@ -93,8 +74,6 @@ class SearchController extends Controller
     /**
      * Search for a list of entities and return a partial HTML response of matching entities.
      * Returns the most popular entities if no search is provided.
-     * @param Request $request
-     * @return mixed
      */
     public function searchEntitiesAjax(Request $request)
     {
@@ -115,15 +94,13 @@ class SearchController extends Controller
 
     /**
      * Search siblings items in the system.
-     * @param Request $request
-     * @return Factory|View|mixed
      */
     public function searchSiblings(Request $request)
     {
         $type = $request->get('entity_type', null);
         $id = $request->get('entity_id', null);
 
-        $entity = $this->entityRepo->getById($type, $id);
+        $entity = Entity::getEntityInstance($type)->newQuery()->visible()->find($id);
         if (!$entity) {
             return $this->jsonError(trans('errors.entity_not_found'), 404);
         }
@@ -132,12 +109,12 @@ class SearchController extends Controller
 
         // Page in chapter
         if ($entity->isA('page') && $entity->chapter) {
-            $entities = $this->entityRepo->getChapterChildren($entity->chapter);
+            $entities = $entity->chapter->visiblePages();
         }
 
         // Page in book or chapter
         if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
-            $entities = $this->entityRepo->getBookDirectChildren($entity->book);
+            $entities = $entity->book->getDirectChildren();
         }
 
         // Book
@@ -145,15 +122,15 @@ class SearchController extends Controller
         if ($entity->isA('book')) {
             $contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
             if ($contextShelf) {
-                $entities = $this->entityRepo->getBookshelfChildren($contextShelf);
+                $entities = $contextShelf->visibleBooks()->get();
             } else {
-                $entities = $this->entityRepo->getAll('book');
+                $entities = Book::visible()->get();
             }
         }
 
         // Shelve
         if ($entity->isA('bookshelf')) {
-            $entities = $this->entityRepo->getAll('bookshelf');
+            $entities = Bookshelf::visible()->get();
         }
 
         return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
index 650833c7f33bd7a9a5d5e6e97a4446688c22b775..1146f22c749ec8df442dfe6a278b3922a3a1fdea 100644 (file)
@@ -47,7 +47,7 @@ class SettingController extends Controller
      */
     public function update(Request $request)
     {
-        $this->preventAccessForDemoUsers();
+        $this->preventAccessInDemoMode();
         $this->checkPermission('settings-manage');
         $this->validate($request, [
             'app_logo' => $this->imageRepo->getImageValidationRules(),
@@ -76,7 +76,7 @@ class SettingController extends Controller
             setting()->remove('app-logo');
         }
 
-        session()->flash('success', trans('settings.settings_save_success'));
+        $this->showSuccessNotification(trans('settings.settings_save_success'));
         return redirect('/settings');
     }
 
@@ -111,14 +111,14 @@ class SettingController extends Controller
         $imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
         $deleteCount = count($imagesToDelete);
         if ($deleteCount === 0) {
-            session()->flash('warning', trans('settings.maint_image_cleanup_nothing_found'));
+            $this->showWarningNotification(trans('settings.maint_image_cleanup_nothing_found'));
             return redirect('/settings/maintenance')->withInput();
         }
 
         if ($dryRun) {
             session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
         } else {
-            session()->flash('success', trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
+            $this->showSuccessNotification(trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
         }
 
         return redirect('/settings/maintenance#image-cleanup')->withInput();
index c9d2560ba0fff4cf9cb4360579ad71c3afd1c591..b55398d2f6f644cb35cec75383df75be9dd40b71 100644 (file)
@@ -8,6 +8,7 @@ use BookStack\Exceptions\UserUpdateException;
 use BookStack\Uploads\ImageRepo;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
+use Illuminate\Support\Str;
 
 class UserController extends Controller
 {
@@ -92,7 +93,7 @@ class UserController extends Controller
         $user = $this->user->fill($request->all());
 
         if ($authMethod === 'standard') {
-            $user->password = bcrypt($request->get('password', str_random(32)));
+            $user->password = bcrypt($request->get('password', Str::random(32)));
         } elseif ($authMethod === 'ldap') {
             $user->external_auth_id = $request->get('external_auth_id');
         }
@@ -143,7 +144,7 @@ class UserController extends Controller
      */
     public function update(Request $request, $id)
     {
-        $this->preventAccessForDemoUsers();
+        $this->preventAccessInDemoMode();
         $this->checkPermissionOrCurrentUser('users-manage', $id);
 
         $this->validate($request, [
@@ -176,7 +177,7 @@ class UserController extends Controller
         }
 
         // External auth id updates
-        if ($this->currentUser->can('users-manage') && $request->filled('external_auth_id')) {
+        if (user()->can('users-manage') && $request->filled('external_auth_id')) {
             $user->external_auth_id = $request->get('external_auth_id');
         }
 
@@ -201,7 +202,7 @@ class UserController extends Controller
         }
 
         $user->save();
-        session()->flash('success', trans('settings.users_edit_success'));
+        $this->showSuccessNotification(trans('settings.users_edit_success'));
 
         $redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
         return redirect($redirectUrl);
@@ -229,23 +230,23 @@ class UserController extends Controller
      */
     public function destroy($id)
     {
-        $this->preventAccessForDemoUsers();
+        $this->preventAccessInDemoMode();
         $this->checkPermissionOrCurrentUser('users-manage', $id);
 
         $user = $this->userRepo->getById($id);
 
         if ($this->userRepo->isOnlyAdmin($user)) {
-            session()->flash('error', trans('errors.users_cannot_delete_only_admin'));
+            $this->showErrorNotification(trans('errors.users_cannot_delete_only_admin'));
             return redirect($user->getEditUrl());
         }
 
         if ($user->system_name === 'public') {
-            session()->flash('error', trans('errors.users_cannot_delete_guest'));
+            $this->showErrorNotification(trans('errors.users_cannot_delete_guest'));
             return redirect($user->getEditUrl());
         }
 
         $this->userRepo->destroy($user);
-        session()->flash('success', trans('settings.users_delete_success'));
+        $this->showSuccessNotification(trans('settings.users_delete_success'));
 
         return redirect('/settings/users');
     }
@@ -260,7 +261,7 @@ class UserController extends Controller
         $user = $this->userRepo->getById($id);
 
         $userActivity = $this->userRepo->getActivity($user);
-        $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
+        $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5);
         $assetCounts = $this->userRepo->getAssetCounts($user);
 
         return view('users.profile', [
@@ -273,22 +274,22 @@ class UserController extends Controller
 
     /**
      * Update the user's preferred book-list display setting.
-     * @param $id
      * @param Request $request
+     * @param $id
      * @return \Illuminate\Http\RedirectResponse
      */
-    public function switchBookView($id, Request $request)
+    public function switchBookView(Request $request, $id)
     {
         return $this->switchViewType($id, $request, 'books');
     }
 
     /**
      * Update the user's preferred shelf-list display setting.
-     * @param $id
      * @param Request $request
+     * @param $id
      * @return \Illuminate\Http\RedirectResponse
      */
-    public function switchShelfView($id, Request $request)
+    public function switchShelfView(Request $request, $id)
     {
         return $this->switchViewType($id, $request, 'bookshelves');
     }
@@ -318,12 +319,12 @@ class UserController extends Controller
 
     /**
      * Change the stored sort type for a particular view.
+     * @param Request $request
      * @param string $id
      * @param string $type
-     * @param Request $request
      * @return \Illuminate\Http\RedirectResponse
      */
-    public function changeSort(string $id, string $type, Request $request)
+    public function changeSort(Request $request, string $id, string $type)
     {
         $validSortTypes = ['books', 'bookshelves'];
         if (!in_array($type, $validSortTypes)) {
@@ -334,12 +335,12 @@ class UserController extends Controller
 
     /**
      * Update the stored section expansion preference for the given user.
+     * @param Request $request
      * @param string $id
      * @param string $key
-     * @param Request $request
      * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
      */
-    public function updateExpansionPreference(string $id, string $key, Request $request)
+    public function updateExpansionPreference(Request $request, string $id, string $key)
     {
         $this->checkPermissionOrCurrentUser('users-manage', $id);
         $keyWhitelist = ['home-details'];
index cd894de95340471f87e73c876cac8e2a4e49a657..f9752da09d6492430dd2fe2f4c131dcac301aedf 100644 (file)
@@ -12,7 +12,7 @@ class Kernel extends HttpKernel
      * @var array
      */
     protected $middleware = [
-        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
+        \BookStack\Http\Middleware\CheckForMaintenanceMode::class,
         \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
         \BookStack\Http\Middleware\TrimStrings::class,
         \BookStack\Http\Middleware\TrustProxies::class,
@@ -29,9 +29,11 @@ class Kernel extends HttpKernel
             \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
             \Illuminate\Session\Middleware\StartSession::class,
             \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+            \Illuminate\Routing\Middleware\ThrottleRequests::class,
             \BookStack\Http\Middleware\VerifyCsrfToken::class,
             \Illuminate\Routing\Middleware\SubstituteBindings::class,
-            \BookStack\Http\Middleware\Localization::class
+            \BookStack\Http\Middleware\Localization::class,
+            \BookStack\Http\Middleware\GlobalViewData::class,
         ],
         'api' => [
             'throttle:60,1',
diff --git a/app/Http/Middleware/CheckForMaintenanceMode.php b/app/Http/Middleware/CheckForMaintenanceMode.php
new file mode 100644 (file)
index 0000000..0c76838
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+
+namespace BookStack\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
+
+class CheckForMaintenanceMode extends Middleware
+{
+    /**
+     * The URIs that should be reachable while maintenance mode is enabled.
+     *
+     * @var array
+     */
+    protected $except = [
+        //
+    ];
+}
diff --git a/app/Http/Middleware/GlobalViewData.php b/app/Http/Middleware/GlobalViewData.php
new file mode 100644 (file)
index 0000000..bc132df
--- /dev/null
@@ -0,0 +1,27 @@
+<?php namespace BookStack\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+
+/**
+ * Class GlobalViewData
+ * Sets up data that is accessible to any view rendered by the web routes.
+ */
+class GlobalViewData
+{
+
+    /**
+     * Handle an incoming request.
+     *
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        view()->share('signedIn', auth()->check());
+        view()->share('currentUser', user());
+
+        return $next($request);
+    }
+}
index 73c11a82769d861ed0ab1793bd7eb7473466d74b..878c2f1647e968cfdeb91ce985cb7d5f7f088c78 100644 (file)
@@ -16,17 +16,11 @@ class TrustProxies extends Middleware
     protected $proxies;
 
     /**
-     * The current proxy header mappings.
+     * The headers that should be used to detect proxies.
      *
-     * @var array
+     * @var int
      */
-    protected $headers = [
-        Request::HEADER_FORWARDED => 'FORWARDED',
-        Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
-        Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
-        Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
-        Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
-    ];
+    protected $headers = Request::HEADER_X_FORWARDED_ALL;
 
     /**
      * Handle the request, Set the correct user-configured proxy information.
index 291b8326f36d4657e46197117e6d61bfc84762a6..1a29a2b1d121f35f56dfd9e2eeb5611c84c4b42a 100644 (file)
@@ -6,6 +6,13 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
 
 class VerifyCsrfToken extends Middleware
 {
+    /**
+     * Indicates whether the XSRF-TOKEN cookie should be set on the response.
+     *
+     * @var bool
+     */
+    protected $addHttpCookie = true;
+
     /**
      * The URIs that should be excluded from CSRF verification.
      *
index bd2761a0b5cfcd9fe8f8dec6149ddf0a3a9e5315..183686f67b4eef3a6299c2cb2e25893b4f4c4a61 100644 (file)
@@ -22,5 +22,4 @@ class Request extends LaravelRequest
 
         return $base;
     }
-
-}
\ No newline at end of file
+}
index b46a716cc4e0e985b287f233437b66eafcca7357..3a1b4f42ee63e02118a72d798d8e837f12499018 100644 (file)
@@ -48,7 +48,7 @@ class AppServiceProvider extends ServiceProvider
             return "<?php echo icon($expression); ?>";
         });
 
-        Blade::directive('exposeTranslations', function($expression) {
+        Blade::directive('exposeTranslations', function ($expression) {
             return "<?php \$__env->startPush('translations'); ?>" .
                 "<?php foreach({$expression} as \$key): ?>" .
                 '<meta name="translation" key="<?php echo e($key); ?>" value="<?php echo e(trans($key)); ?>">' . "\n" .
index e7bde5290490fc5d1fc06bf01c0c91c6de2afb55..b4158187cd5fe5225b68aa013d033d67d05edb30 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Providers;
 
 use BookStack\Actions\ActivityService;
 use BookStack\Actions\ViewService;
+use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Settings\SettingService;
 use BookStack\Uploads\ImageService;
 use Illuminate\Support\ServiceProvider;
@@ -27,20 +28,24 @@ class CustomFacadeProvider extends ServiceProvider
      */
     public function register()
     {
-        $this->app->bind('activity', function () {
+        $this->app->singleton('activity', function () {
             return $this->app->make(ActivityService::class);
         });
 
-        $this->app->bind('views', function () {
+        $this->app->singleton('views', function () {
             return $this->app->make(ViewService::class);
         });
 
-        $this->app->bind('setting', function () {
+        $this->app->singleton('setting', function () {
             return $this->app->make(SettingService::class);
         });
 
-        $this->app->bind('images', function () {
+        $this->app->singleton('images', function () {
             return $this->app->make(ImageService::class);
         });
+
+        $this->app->singleton('permissions', function () {
+            return $this->app->make(PermissionService::class);
+        });
     }
 }
index 8720d3c098e74eb96c0890dc174089de43f8be70..3f0b447df7388a573602396d052bce2e5c5bca7a 100644 (file)
@@ -13,7 +13,7 @@ class Attachment extends Ownable
      */
     public function getFileName()
     {
-        if (str_contains($this->name, '.')) {
+        if (strpos($this->name, '.') !== false) {
             return $this->name;
         }
         return $this->name . '.' . $this->extension;
index 6e875a1e7a35c9cec514ccd3e21316ef04980019..ae4fb6e967160787e1c46dde0e442228386f9af9 100644 (file)
@@ -2,6 +2,7 @@
 
 use BookStack\Exceptions\FileUploadException;
 use Exception;
+use Illuminate\Support\Str;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
 
 class AttachmentService extends UploadService
@@ -185,9 +186,9 @@ class AttachmentService extends UploadService
         $storage = $this->getStorage();
         $basePath = 'uploads/files/' . Date('Y-m-M') . '/';
 
-        $uploadFileName = str_random(16) . '.' . $uploadedFile->getClientOriginalExtension();
+        $uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension();
         while ($storage->exists($basePath . $uploadFileName)) {
-            $uploadFileName = str_random(3) . $uploadFileName;
+            $uploadFileName = Str::random(3) . $uploadFileName;
         }
 
         $attachmentPath = $basePath . $uploadFileName;
index 860230d00f914169969e40d00efdca77e6f0845d..e7668471bd64c174c858eef0ac6c9c36a16e8880 100644 (file)
@@ -7,6 +7,7 @@ use DB;
 use Exception;
 use Illuminate\Contracts\Cache\Repository as Cache;
 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
+use Illuminate\Support\Str;
 use Intervention\Image\Exception\NotSupportedException;
 use Intervention\Image\ImageManager;
 use phpDocumentor\Reflection\Types\Integer;
@@ -140,12 +141,12 @@ class ImageService extends UploadService
         $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
 
         while ($storage->exists($imagePath . $imageName)) {
-            $imageName = str_random(3) . $imageName;
+            $imageName = Str::random(3) . $imageName;
         }
 
         $fullPath = $imagePath . $imageName;
         if ($secureUploads) {
-            $fullPath = $imagePath . str_random(16) . '-' . $imageName;
+            $fullPath = $imagePath . Str::random(16) . '-' . $imageName;
         }
 
         try {
@@ -220,7 +221,7 @@ class ImageService extends UploadService
 
         $storage->put($thumbFilePath, $thumbData);
         $storage->setVisibility($thumbFilePath, 'public');
-        $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
+        $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
 
         return $this->getPublicUrl($thumbFilePath);
     }
index f36f2e59dcef11b461cd5efdf239f6ea683dda1c..6211f41be0c4ef1a866a4c043287b2d76c1f3f60 100644 (file)
@@ -12,7 +12,7 @@ use BookStack\Settings\SettingService;
  * @return string
  * @throws Exception
  */
-function versioned_asset($file = '') : string
+function versioned_asset(string $file = ''): string
 {
     static $version = null;
 
@@ -35,7 +35,7 @@ function versioned_asset($file = '') : string
  * Defaults to public 'Guest' user if not logged in.
  * @return User
  */
-function user() : User
+function user(): User
 {
     return auth()->user() ?: User::getDefault();
 }
@@ -44,7 +44,7 @@ function user() : User
  * Check if current user is a signed in user.
  * @return bool
  */
-function signedInUser() : bool
+function signedInUser(): bool
 {
     return auth()->user() && !auth()->user()->isDefault();
 }
@@ -53,7 +53,7 @@ function signedInUser() : bool
  * Check if the current user has general access.
  * @return bool
  */
-function hasAppAccess() : bool
+function hasAppAccess(): bool
 {
     return !auth()->guest() || setting('app-public');
 }
@@ -66,7 +66,7 @@ function hasAppAccess() : bool
  * @param Ownable $ownable
  * @return bool
  */
-function userCan(string $permission, Ownable $ownable = null) : bool
+function userCan(string $permission, Ownable $ownable = null): bool
 {
     if ($ownable === null) {
         return user() && user()->can($permission);
@@ -84,7 +84,7 @@ function userCan(string $permission, Ownable $ownable = null) : bool
  * @param string|null $entityClass
  * @return bool
  */
-function userCanOnAny(string $permission, string $entityClass = null) : bool
+function userCanOnAny(string $permission, string $entityClass = null): bool
 {
     $permissionService = app(PermissionService::class);
     return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
@@ -92,11 +92,11 @@ function userCanOnAny(string $permission, string $entityClass = null) : bool
 
 /**
  * Helper to access system settings.
- * @param $key
- * @param bool $default
+ * @param string $key
+ * @param $default
  * @return bool|string|SettingService
  */
-function setting($key = null, $default = false)
+function setting(string $key = null, $default = false)
 {
     $settingService = resolve(SettingService::class);
     if (is_null($key)) {
@@ -110,7 +110,7 @@ function setting($key = null, $default = false)
  * @param string $path
  * @return string
  */
-function theme_path($path = '') : string
+function theme_path(string $path = ''): string
 {
     $theme = config('view.theme');
     if (!$theme) {
@@ -130,7 +130,7 @@ function theme_path($path = '') : string
  * @param array $attrs
  * @return mixed
  */
-function icon($name, $attrs = [])
+function icon(string $name, array $attrs = []): string
 {
     $attrs = array_merge([
         'class'     => 'svg-icon',
@@ -142,7 +142,7 @@ function icon($name, $attrs = [])
         $attrString .=  $attrName . '="' . $attr . '" ';
     }
 
-    $iconPath = resource_path('assets/icons/' . $name . '.svg');
+    $iconPath = resource_path('icons/' . $name . '.svg');
     $themeIconPath = theme_path('icons/' . $name . '.svg');
     if ($themeIconPath && file_exists($themeIconPath)) {
         $iconPath = $themeIconPath;
@@ -158,12 +158,12 @@ function icon($name, $attrs = [])
  * Generate a url with multiple parameters for sorting purposes.
  * Works out the logic to set the correct sorting direction
  * Discards empty parameters and allows overriding.
- * @param $path
+ * @param string $path
  * @param array $data
  * @param array $overrideData
  * @return string
  */
-function sortUrl($path, $data, $overrideData = [])
+function sortUrl(string $path, array $data, array $overrideData = []): string
 {
     $queryStringSections = [];
     $queryData = array_merge($data, $overrideData);
index 516980cc10c745783b9668fcf7d144ffa683556b..6538aa81c50789b32194f77ddb4e7108dafe407c 100644 (file)
@@ -11,8 +11,8 @@
 |
 */
 
-$app = new \BookStack\Application(
-    realpath(__DIR__.'/../')
+$app = new BookStack\Application(
+    dirname(__DIR__)
 );
 
 /*
index 61bb8509e9b87717b3eaf6257ddd82624e81876d..a8b9456a1856c01fb2bf65e14ea32de0f7c55a15 100644 (file)
@@ -5,46 +5,47 @@
     "license": "MIT",
     "type": "project",
     "require": {
-        "php": ">=7.0.5",
+        "php": "^7.2",
+        "ext-curl": "*",
+        "ext-dom": "*",
+        "ext-gd": "*",
         "ext-json": "*",
+        "ext-mbstring": "*",
         "ext-tidy": "*",
-        "ext-dom": "*",
         "ext-xml": "*",
-        "ext-mbstring": "*",
-        "ext-gd": "*",
-        "ext-curl": "*",
-        "laravel/framework": "~5.5.44",
-        "fideloper/proxy": "~3.3",
-        "intervention/image": "^2.4",
-        "laravel/socialite": "3.0.x-dev",
+        "barryvdh/laravel-dompdf": "^0.8.5",
+        "barryvdh/laravel-snappy": "^0.4.5",
+        "doctrine/dbal": "^2.9",
+        "fideloper/proxy": "^4.0",
+        "gathercontent/htmldiff": "^0.2.1",
+        "intervention/image": "^2.5",
+        "laravel/framework": "^6.0",
+        "laravel/socialite": "^4.2",
         "league/flysystem-aws-s3-v3": "^1.0",
-        "barryvdh/laravel-dompdf": "^0.8.1",
         "predis/predis": "^1.1",
-        "gathercontent/htmldiff": "^0.2.1",
-        "barryvdh/laravel-snappy": "^0.4.0",
-        "socialiteproviders/slack": "^3.0",
+        "socialiteproviders/discord": "^2.0",
+        "socialiteproviders/gitlab": "^3.0",
         "socialiteproviders/microsoft-azure": "^3.0",
         "socialiteproviders/okta": "^1.0",
-        "socialiteproviders/gitlab": "^3.0",
-        "socialiteproviders/twitch": "^3.0",
-        "socialiteproviders/discord": "^2.0",
-        "doctrine/dbal": "^2.5"
+        "socialiteproviders/slack": "^3.0",
+        "socialiteproviders/twitch": "^5.0"
     },
     "require-dev": {
-        "filp/whoops": "~2.0",
-        "fzaninotto/faker": "~1.4",
-        "mockery/mockery": "~1.0",
-        "phpunit/phpunit": "~6.0",
-        "symfony/css-selector": "3.1.*",
-        "symfony/dom-crawler": "3.1.*",
-        "laravel/browser-kit-testing": "^2.0",
-        "barryvdh/laravel-ide-helper": "^2.4.1",
-        "barryvdh/laravel-debugbar": "^3.1.0",
-        "squizlabs/php_codesniffer": "^3.2"
+        "barryvdh/laravel-debugbar": "^3.2.8",
+        "barryvdh/laravel-ide-helper": "^2.6.4",
+        "facade/ignition": "^1.4",
+        "fzaninotto/faker": "^1.4",
+        "laravel/browser-kit-testing": "^5.1",
+        "mockery/mockery": "^1.0",
+        "nunomaduro/collision": "^3.0",
+        "phpunit/phpunit": "^8.0",
+        "squizlabs/php_codesniffer": "^3.4",
+        "wnx/laravel-stats": "^2.0"
     },
     "autoload": {
         "classmap": [
-            "database"
+            "database/seeds",
+            "database/factories"
         ],
         "psr-4": {
             "BookStack\\": "app/"
     },
     "scripts": {
         "post-root-package-install": [
-            "php -r \"file_exists('.env') || copy('.env.example', '.env');\""
+            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
         ],
         "post-create-project-cmd": [
-            "php artisan key:generate"
+            "@php artisan key:generate --ansi"
         ],
         "pre-update-cmd": [
-            "php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
-            "php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
+            "@php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
+            "@php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
         ],
         "pre-install-cmd": [
-            "php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
-            "php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
+            "@php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
+            "@php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
         ],
         "post-install-cmd": [
-            "php artisan cache:clear",
-            "php artisan view:clear"
+            "@php artisan cache:clear",
+            "@php artisan view:clear"
         ],
         "post-autoload-dump": [
             "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
-            "@php artisan package:discover"
+            "@php artisan package:discover --ansi"
         ],
         "refresh-test-database": [
-            "php artisan migrate:refresh --database=mysql_testing",
-            "php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
+            "@php artisan migrate:refresh --database=mysql_testing",
+            "@php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
         ]
     },
     "config": {
         "optimize-autoloader": true,
         "preferred-install": "dist",
+        "sort-packages": true,
         "platform": {
-            "php": "7.0.5"
+            "php": "7.2.0"
+        }
+    },
+    "extra": {
+        "laravel": {
+            "dont-discover": []
         }
-    }
+    },
+    "minimum-stability": "dev",
+    "prefer-stable": true
 }
index d7734ce1a95c565dda65f40742bb52a97d4db992..3ec106ded4f0c57c9ca3556222dff16874606bc8 100644 (file)
@@ -4,27 +4,26 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "0946a07729a7a1bfef9bac185a870afd",
+    "content-hash": "c156e1738dbab2a57f9a926d9a9a5a6a",
     "packages": [
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.86.2",
+            "version": "3.112.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76"
+                "reference": "1e21446c6780a3b9b5e4315bd6d4347d2c3381eb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
-                "reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1e21446c6780a3b9b5e4315bd6d4347d2c3381eb",
+                "reference": "1e21446c6780a3b9b5e4315bd6d4347d2c3381eb",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "ext-pcre": "*",
                 "ext-simplexml": "*",
-                "ext-spl": "*",
                 "guzzlehttp/guzzle": "^5.3.3|^6.2.1",
                 "guzzlehttp/promises": "~1.0",
                 "guzzlehttp/psr7": "^1.4.1",
@@ -42,7 +41,8 @@
                 "ext-sockets": "*",
                 "nette/neon": "^2.3",
                 "phpunit/phpunit": "^4.8.35|^5.4.3",
-                "psr/cache": "^1.0"
+                "psr/cache": "^1.0",
+                "psr/simple-cache": "^1.0"
             },
             "suggest": {
                 "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
                 "s3",
                 "sdk"
             ],
-            "time": "2019-01-18T21:10:44+00:00"
+            "time": "2019-09-12T18:09:53+00:00"
         },
         {
             "name": "barryvdh/laravel-dompdf",
-            "version": "v0.8.3",
+            "version": "v0.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/barryvdh/laravel-dompdf.git",
-                "reference": "46781d0304277845a19c09c169bc595fd182cce4"
+                "reference": "7393732b2f3a3ee357974cbb0c46c9b65b84dad1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/46781d0304277845a19c09c169bc595fd182cce4",
-                "reference": "46781d0304277845a19c09c169bc595fd182cce4",
+                "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/7393732b2f3a3ee357974cbb0c46c9b65b84dad1",
+                "reference": "7393732b2f3a3ee357974cbb0c46c9b65b84dad1",
                 "shasum": ""
             },
             "require": {
                 "dompdf/dompdf": "^0.8",
-                "illuminate/support": "5.5.x|5.6.x|5.7.x",
+                "illuminate/support": "^5.5|^6",
                 "php": ">=7"
             },
             "type": "library",
                 "laravel",
                 "pdf"
             ],
-            "time": "2018-08-31T13:25:44+00:00"
+            "time": "2019-08-23T14:30:33+00:00"
         },
         {
             "name": "barryvdh/laravel-snappy",
-            "version": "v0.4.3",
+            "version": "v0.4.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/barryvdh/laravel-snappy.git",
-                "reference": "62bb5017b7004bf3e48bfed3d5c00d3dc6e60478"
+                "reference": "9be767fc7a082665a84945f36c70b0cbead91ce9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/62bb5017b7004bf3e48bfed3d5c00d3dc6e60478",
-                "reference": "62bb5017b7004bf3e48bfed3d5c00d3dc6e60478",
+                "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/9be767fc7a082665a84945f36c70b0cbead91ce9",
+                "reference": "9be767fc7a082665a84945f36c70b0cbead91ce9",
                 "shasum": ""
             },
             "require": {
-                "illuminate/filesystem": "5.5.x|5.6.x|5.7.x",
-                "illuminate/support": "5.5.x|5.6.x|5.7.x",
+                "illuminate/filesystem": "5.5.x|5.6.x|5.7.x|5.8.x|6.0.*",
+                "illuminate/support": "5.5.x|5.6.x|5.7.x|5.8.x|6.0.*",
                 "knplabs/knp-snappy": "^1",
                 "php": ">=7"
             },
                     "email": "[email protected]"
                 }
             ],
-            "description": "Snappy PDF/Image for Laravel 4",
+            "description": "Snappy PDF/Image for Laravel",
             "keywords": [
                 "image",
                 "laravel",
                 "wkhtmltoimage",
                 "wkhtmltopdf"
             ],
-            "time": "2018-09-06T10:14:15+00:00"
+            "time": "2019-08-30T16:12:23+00:00"
         },
         {
             "name": "cogpowered/finediff",
             ],
             "time": "2014-05-19T10:25:02+00:00"
         },
-        {
-            "name": "doctrine/annotations",
-            "version": "v1.4.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/doctrine/annotations.git",
-                "reference": "54cacc9b81758b14e3ce750f205a393d52339e97"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97",
-                "reference": "54cacc9b81758b14e3ce750f205a393d52339e97",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/lexer": "1.*",
-                "php": "^5.6 || ^7.0"
-            },
-            "require-dev": {
-                "doctrine/cache": "1.*",
-                "phpunit/phpunit": "^5.7"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.4.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Roman Borschel",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Benjamin Eberlei",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Guilherme Blanco",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Jonathan Wage",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Johannes Schmitt",
-                    "email": "[email protected]"
-                }
-            ],
-            "description": "Docblock Annotations Parser",
-            "homepage": "http://www.doctrine-project.org",
-            "keywords": [
-                "annotations",
-                "docblock",
-                "parser"
-            ],
-            "time": "2017-02-24T16:22:25+00:00"
-        },
         {
             "name": "doctrine/cache",
-            "version": "v1.6.2",
+            "version": "v1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/cache.git",
-                "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b"
+                "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b",
-                "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b",
+                "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57",
+                "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57",
                 "shasum": ""
             },
             "require": {
-                "php": "~5.5|~7.0"
+                "php": "~7.1"
             },
             "conflict": {
                 "doctrine/common": ">2.2,<2.4"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.8|~5.0",
-                "predis/predis": "~1.0",
-                "satooshi/php-coveralls": "~0.6"
+                "alcaeus/mongo-php-adapter": "^1.1",
+                "doctrine/coding-standard": "^4.0",
+                "mongodb/mongodb": "^1.1",
+                "phpunit/phpunit": "^7.0",
+                "predis/predis": "~1.0"
+            },
+            "suggest": {
+                "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.6.x-dev"
+                    "dev-master": "1.8.x-dev"
                 }
             },
             "autoload": {
                 }
             ],
             "description": "Caching library offering an object-oriented API for many cache backends",
-            "homepage": "http://www.doctrine-project.org",
+            "homepage": "https://www.doctrine-project.org",
             "keywords": [
                 "cache",
                 "caching"
             ],
-            "time": "2017-07-22T12:49:21+00:00"
+            "time": "2018-08-21T18:01:43+00:00"
         },
         {
-            "name": "doctrine/collections",
-            "version": "v1.4.0",
+            "name": "doctrine/dbal",
+            "version": "v2.9.2",
             "source": {
                 "type": "git",
-                "url": "https://github.com/doctrine/collections.git",
-                "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba"
+                "url": "https://github.com/doctrine/dbal.git",
+                "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba",
-                "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9",
+                "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0"
+                "doctrine/cache": "^1.0",
+                "doctrine/event-manager": "^1.0",
+                "ext-pdo": "*",
+                "php": "^7.1"
             },
             "require-dev": {
-                "doctrine/coding-standard": "~0.1@dev",
-                "phpunit/phpunit": "^5.7"
+                "doctrine/coding-standard": "^5.0",
+                "jetbrains/phpstorm-stubs": "^2018.1.2",
+                "phpstan/phpstan": "^0.10.1",
+                "phpunit/phpunit": "^7.4",
+                "symfony/console": "^2.0.5|^3.0|^4.0",
+                "symfony/phpunit-bridge": "^3.4.5|^4.0.5"
+            },
+            "suggest": {
+                "symfony/console": "For helpful console commands such as SQL execution and import of files."
             },
+            "bin": [
+                "bin/doctrine-dbal"
+            ],
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.3.x-dev"
+                    "dev-master": "2.9.x-dev",
+                    "dev-develop": "3.0.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Doctrine\\Common\\Collections\\": "lib/"
+                "psr-4": {
+                    "Doctrine\\DBAL\\": "lib/Doctrine/DBAL"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 {
                     "name": "Jonathan Wage",
                     "email": "[email protected]"
-                },
-                {
-                    "name": "Johannes Schmitt",
-                    "email": "[email protected]"
                 }
             ],
-            "description": "Collections Abstraction library",
-            "homepage": "http://www.doctrine-project.org",
+            "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
+            "homepage": "https://www.doctrine-project.org/projects/dbal.html",
             "keywords": [
-                "array",
-                "collections",
-                "iterator"
+                "abstraction",
+                "database",
+                "dbal",
+                "mysql",
+                "persistence",
+                "pgsql",
+                "php",
+                "queryobject"
             ],
-            "time": "2017-01-03T10:49:41+00:00"
+            "time": "2018-12-31T03:27:51+00:00"
         },
         {
-            "name": "doctrine/common",
-            "version": "v2.7.3",
+            "name": "doctrine/event-manager",
+            "version": "v1.0.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/doctrine/common.git",
-                "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9"
+                "url": "https://github.com/doctrine/event-manager.git",
+                "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9",
-                "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9",
+                "url": "https://api.github.com/repos/doctrine/event-manager/zipball/a520bc093a0170feeb6b14e9d83f3a14452e64b3",
+                "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3",
                 "shasum": ""
             },
             "require": {
-                "doctrine/annotations": "1.*",
-                "doctrine/cache": "1.*",
-                "doctrine/collections": "1.*",
-                "doctrine/inflector": "1.*",
-                "doctrine/lexer": "1.*",
-                "php": "~5.6|~7.0"
+                "php": "^7.1"
+            },
+            "conflict": {
+                "doctrine/common": "<2.9@dev"
             },
             "require-dev": {
-                "phpunit/phpunit": "^5.4.6"
+                "doctrine/coding-standard": "^4.0",
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.7.x-dev"
+                    "dev-master": "1.0.x-dev"
                 }
             },
             "autoload": {
                 {
                     "name": "Johannes Schmitt",
                     "email": "[email protected]"
-                }
-            ],
-            "description": "Common Library for Doctrine projects",
-            "homepage": "http://www.doctrine-project.org",
-            "keywords": [
-                "annotations",
-                "collections",
-                "eventmanager",
-                "persistence",
-                "spl"
-            ],
-            "time": "2017-07-22T08:35:12+00:00"
-        },
-        {
-            "name": "doctrine/dbal",
-            "version": "v2.5.13",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/doctrine/dbal.git",
-                "reference": "729340d8d1eec8f01bff708e12e449a3415af873"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/729340d8d1eec8f01bff708e12e449a3415af873",
-                "reference": "729340d8d1eec8f01bff708e12e449a3415af873",
-                "shasum": ""
-            },
-            "require": {
-                "doctrine/common": ">=2.4,<2.8-dev",
-                "php": ">=5.3.2"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "4.*",
-                "symfony/console": "2.*||^3.0"
-            },
-            "suggest": {
-                "symfony/console": "For helpful console commands such as SQL execution and import of files."
-            },
-            "bin": [
-                "bin/doctrine-dbal"
-            ],
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "2.5.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-0": {
-                    "Doctrine\\DBAL\\": "lib/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Roman Borschel",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Benjamin Eberlei",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Guilherme Blanco",
-                    "email": "[email protected]"
                 },
                 {
-                    "name": "Jonathan Wage",
-                    "email": "jonwage@gmail.com"
+                    "name": "Marco Pivetta",
+                    "email": "ocramius@gmail.com"
                 }
             ],
-            "description": "Database Abstraction Layer",
-            "homepage": "http://www.doctrine-project.org",
+            "description": "Doctrine Event Manager component",
+            "homepage": "https://www.doctrine-project.org/projects/event-manager.html",
             "keywords": [
-                "database",
-                "dbal",
-                "persistence",
-                "queryobject"
+                "event",
+                "eventdispatcher",
+                "eventmanager"
             ],
-            "time": "2017-07-22T20:44:48+00:00"
+            "time": "2018-06-11T11:59:03+00:00"
         },
         {
             "name": "doctrine/inflector",
-            "version": "v1.2.0",
+            "version": "v1.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/inflector.git",
-                "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462"
+                "reference": "5527a48b7313d15261292c149e55e26eae771b0a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462",
-                "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462",
+                "url": "https://api.github.com/repos/doctrine/inflector/zipball/5527a48b7313d15261292c149e55e26eae771b0a",
+                "reference": "5527a48b7313d15261292c149e55e26eae771b0a",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
                 "phpunit/phpunit": "^6.2"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.2.x-dev"
+                    "dev-master": "1.3.x-dev"
                 }
             },
             "autoload": {
                 "singularize",
                 "string"
             ],
-            "time": "2017-07-22T12:18:28+00:00"
+            "time": "2018-01-09T20:05:19+00:00"
         },
         {
             "name": "doctrine/lexer",
-            "version": "v1.0.1",
+            "version": "1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/lexer.git",
-                "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
+                "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
-                "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
+                "url": "https://api.github.com/repos/doctrine/lexer/zipball/e17f069ede36f7534b95adec71910ed1b49c74ea",
+                "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.2"
+                "php": "^7.2"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^6.0",
+                "phpstan/phpstan": "^0.11.8",
+                "phpunit/phpunit": "^8.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.1.x-dev"
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Doctrine\\Common\\Lexer\\": "lib/"
+                "psr-4": {
+                    "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "Roman Borschel",
-                    "email": "[email protected]"
-                },
                 {
                     "name": "Guilherme Blanco",
                     "email": "[email protected]"
                 },
+                {
+                    "name": "Roman Borschel",
+                    "email": "[email protected]"
+                },
                 {
                     "name": "Johannes Schmitt",
                     "email": "[email protected]"
                 }
             ],
-            "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
-            "homepage": "http://www.doctrine-project.org",
+            "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+            "homepage": "https://www.doctrine-project.org/projects/lexer.html",
             "keywords": [
+                "annotations",
+                "docblock",
                 "lexer",
-                "parser"
+                "parser",
+                "php"
             ],
-            "time": "2014-09-09T13:34:57+00:00"
+            "time": "2019-07-30T19:33:28+00:00"
         },
         {
             "name": "dompdf/dompdf",
             "homepage": "https://github.com/dompdf/dompdf",
             "time": "2018-12-14T02:40:31+00:00"
         },
+        {
+            "name": "dragonmantank/cron-expression",
+            "version": "v2.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/dragonmantank/cron-expression.git",
+                "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/72b6fbf76adb3cf5bc0db68559b33d41219aba27",
+                "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.4|^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Cron\\": "src/Cron/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "[email protected]",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Chris Tankersley",
+                    "email": "[email protected]",
+                    "homepage": "https://github.com/dragonmantank"
+                }
+            ],
+            "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
+            "keywords": [
+                "cron",
+                "schedule"
+            ],
+            "time": "2019-03-31T00:38:28+00:00"
+        },
         {
             "name": "egulias/email-validator",
-            "version": "2.1.7",
+            "version": "2.1.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/egulias/EmailValidator.git",
-                "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e"
+                "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/709f21f92707308cdf8f9bcfa1af4cb26586521e",
-                "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e",
+                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23",
+                "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23",
                 "shasum": ""
             },
             "require": {
             "require-dev": {
                 "dominicsayers/isemail": "dev-master",
                 "phpunit/phpunit": "^4.8.35||^5.7||^6.0",
-                "satooshi/php-coveralls": "^1.0.1"
+                "satooshi/php-coveralls": "^1.0.1",
+                "symfony/phpunit-bridge": "^4.4@dev"
             },
             "suggest": {
                 "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0.x-dev"
+                    "dev-master": "2.1.x-dev"
                 }
             },
             "autoload": {
                 "validation",
                 "validator"
             ],
-            "time": "2018-12-04T22:38:24+00:00"
+            "time": "2019-08-13T17:33:27+00:00"
         },
         {
             "name": "erusev/parsedown",
-            "version": "1.7.1",
+            "version": "1.7.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/erusev/parsedown.git",
-                "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1"
+                "reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/erusev/parsedown/zipball/92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
-                "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
+                "url": "https://api.github.com/repos/erusev/parsedown/zipball/6d893938171a817f4e9bc9e86f2da1e370b7bcd7",
+                "reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7",
                 "shasum": ""
             },
             "require": {
                 "markdown",
                 "parser"
             ],
-            "time": "2018-03-08T01:11:30+00:00"
+            "time": "2019-03-17T18:48:37+00:00"
         },
         {
             "name": "fideloper/proxy",
-            "version": "3.3.4",
+            "version": "4.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/fideloper/TrustedProxy.git",
-                "reference": "9cdf6f118af58d89764249bbcc7bb260c132924f"
+                "reference": "03085e58ec7bee24773fa5a8850751a6e61a7e8a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/9cdf6f118af58d89764249bbcc7bb260c132924f",
-                "reference": "9cdf6f118af58d89764249bbcc7bb260c132924f",
+                "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/03085e58ec7bee24773fa5a8850751a6e61a7e8a",
+                "reference": "03085e58ec7bee24773fa5a8850751a6e61a7e8a",
                 "shasum": ""
             },
             "require": {
-                "illuminate/contracts": "~5.0",
+                "illuminate/contracts": "^5.0|^6.0|^7.0",
                 "php": ">=5.4.0"
             },
             "require-dev": {
-                "illuminate/http": "~5.0",
-                "mockery/mockery": "~0.9.3",
-                "phpunit/phpunit": "^5.7"
+                "illuminate/http": "^5.0|^6.0|^7.0",
+                "mockery/mockery": "^1.0",
+                "phpunit/phpunit": "^6.0"
             },
             "type": "library",
             "extra": {
-                "branch-alias": {
-                    "dev-master": "3.3-dev"
-                },
                 "laravel": {
                     "providers": [
                         "Fideloper\\Proxy\\TrustedProxyServiceProvider"
                 "proxy",
                 "trusted proxy"
             ],
-            "time": "2017-06-15T17:19:42+00:00"
+            "time": "2019-09-03T16:45:42+00:00"
         },
         {
             "name": "gathercontent/htmldiff",
         },
         {
             "name": "guzzlehttp/psr7",
-            "version": "1.5.2",
+            "version": "1.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/psr7.git",
-                "reference": "9f83dded91781a01c63574e387eaa769be769115"
+                "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115",
-                "reference": "9f83dded91781a01c63574e387eaa769be769115",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
+                "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.4.0",
                 "psr/http-message": "~1.0",
-                "ralouphie/getallheaders": "^2.0.5"
+                "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
             },
             "provide": {
                 "psr/http-message-implementation": "1.0"
             },
             "require-dev": {
+                "ext-zlib": "*",
                 "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
             },
+            "suggest": {
+                "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
+            },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.5-dev"
+                    "dev-master": "1.6-dev"
                 }
             },
             "autoload": {
                 "uri",
                 "url"
             ],
-            "time": "2018-12-04T20:46:45+00:00"
+            "time": "2019-07-01T23:21:34+00:00"
         },
         {
             "name": "intervention/image",
-            "version": "2.4.2",
+            "version": "2.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Intervention/image.git",
-                "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb"
+                "reference": "39eaef720d082ecc54c64bf54541c55f10db546d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Intervention/image/zipball/e82d274f786e3d4b866a59b173f42e716f0783eb",
-                "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb",
+                "url": "https://api.github.com/repos/Intervention/image/zipball/39eaef720d082ecc54c64bf54541c55f10db546d",
+                "reference": "39eaef720d082ecc54c64bf54541c55f10db546d",
                 "shasum": ""
             },
             "require": {
                 "thumbnail",
                 "watermark"
             ],
-            "time": "2018-05-29T14:19:03+00:00"
+            "time": "2019-06-24T14:06:31+00:00"
         },
         {
             "name": "knplabs/knp-snappy",
-            "version": "v1.0.4",
+            "version": "v1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/KnpLabs/snappy.git",
-                "reference": "144c4ecd1ccaeda936bf832b93079efc490e6850"
+                "reference": "ea037298d3c613454da77ecb9588cf0397d695e1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/144c4ecd1ccaeda936bf832b93079efc490e6850",
-                "reference": "144c4ecd1ccaeda936bf832b93079efc490e6850",
+                "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/ea037298d3c613454da77ecb9588cf0397d695e1",
+                "reference": "ea037298d3c613454da77ecb9588cf0397d695e1",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.6",
+                "php": ">=7.1",
                 "psr/log": "^1.0",
-                "symfony/process": "~2.3 || ~3.0 || ~4.0"
+                "symfony/process": "~3.4||~4.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.8.36"
+                "phpunit/phpunit": "~7.4"
             },
             "suggest": {
                 "h4cc/wkhtmltoimage-amd64": "Provides wkhtmltoimage-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.x-dev"
                 }
             },
             "autoload": {
                 "thumbnail",
                 "wkhtmltopdf"
             ],
-            "time": "2018-01-22T19:40:51+00:00"
+            "time": "2018-12-14T14:59:37+00:00"
         },
         {
             "name": "laravel/framework",
-            "version": "v5.5.44",
+            "version": "v6.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b"
+                "reference": "56789e9dec750e0fbe8e9e6ae90a01a4e6887902"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b",
-                "reference": "00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/56789e9dec750e0fbe8e9e6ae90a01a4e6887902",
+                "reference": "56789e9dec750e0fbe8e9e6ae90a01a4e6887902",
                 "shasum": ""
             },
             "require": {
-                "doctrine/inflector": "~1.1",
-                "erusev/parsedown": "~1.7",
+                "doctrine/inflector": "^1.1",
+                "dragonmantank/cron-expression": "^2.0",
+                "egulias/email-validator": "^2.1.10",
+                "erusev/parsedown": "^1.7",
+                "ext-json": "*",
                 "ext-mbstring": "*",
                 "ext-openssl": "*",
                 "league/flysystem": "^1.0.8",
-                "monolog/monolog": "~1.12",
-                "mtdowling/cron-expression": "~1.0",
-                "nesbot/carbon": "^1.24.1",
-                "php": ">=7.0",
-                "psr/container": "~1.0",
+                "monolog/monolog": "^1.12|^2.0",
+                "nesbot/carbon": "^2.0",
+                "opis/closure": "^3.1",
+                "php": "^7.2",
+                "psr/container": "^1.0",
                 "psr/simple-cache": "^1.0",
-                "ramsey/uuid": "~3.0",
-                "swiftmailer/swiftmailer": "~6.0",
-                "symfony/console": "~3.3",
-                "symfony/debug": "~3.3",
-                "symfony/finder": "~3.3",
-                "symfony/http-foundation": "~3.3",
-                "symfony/http-kernel": "~3.3",
-                "symfony/process": "~3.3",
-                "symfony/routing": "~3.3",
-                "symfony/var-dumper": "~3.3",
-                "tijsverkoyen/css-to-inline-styles": "~2.2",
-                "vlucas/phpdotenv": "~2.2"
+                "ramsey/uuid": "^3.7",
+                "swiftmailer/swiftmailer": "^6.0",
+                "symfony/console": "^4.3.4",
+                "symfony/debug": "^4.3.4",
+                "symfony/finder": "^4.3.4",
+                "symfony/http-foundation": "^4.3.4",
+                "symfony/http-kernel": "^4.3.4",
+                "symfony/process": "^4.3.4",
+                "symfony/routing": "^4.3.4",
+                "symfony/var-dumper": "^4.3.4",
+                "tijsverkoyen/css-to-inline-styles": "^2.2.1",
+                "vlucas/phpdotenv": "^3.3"
+            },
+            "conflict": {
+                "tightenco/collect": "<5.5.33"
             },
             "replace": {
                 "illuminate/auth": "self.version",
                 "illuminate/support": "self.version",
                 "illuminate/translation": "self.version",
                 "illuminate/validation": "self.version",
-                "illuminate/view": "self.version",
-                "tightenco/collect": "<5.5.33"
+                "illuminate/view": "self.version"
             },
             "require-dev": {
-                "aws/aws-sdk-php": "~3.0",
-                "doctrine/dbal": "~2.5",
-                "filp/whoops": "^2.1.4",
-                "mockery/mockery": "~1.0",
-                "orchestra/testbench-core": "3.5.*",
-                "pda/pheanstalk": "~3.0",
-                "phpunit/phpunit": "~6.0",
+                "aws/aws-sdk-php": "^3.0",
+                "doctrine/dbal": "^2.6",
+                "filp/whoops": "^2.4",
+                "guzzlehttp/guzzle": "^6.3",
+                "league/flysystem-cached-adapter": "^1.0",
+                "mockery/mockery": "^1.2.3",
+                "moontoast/math": "^1.1",
+                "orchestra/testbench-core": "^4.0",
+                "pda/pheanstalk": "^4.0",
+                "phpunit/phpunit": "^8.3",
                 "predis/predis": "^1.1.1",
-                "symfony/css-selector": "~3.3",
-                "symfony/dom-crawler": "~3.3"
+                "symfony/cache": "^4.3",
+                "true/punycode": "^2.1"
             },
             "suggest": {
-                "aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).",
-                "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.5).",
+                "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.0).",
+                "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).",
+                "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
+                "ext-memcached": "Required to use the memcache cache driver.",
                 "ext-pcntl": "Required to use all features of the queue worker.",
                 "ext-posix": "Required to use all features of the queue worker.",
-                "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).",
-                "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).",
-                "laravel/tinker": "Required to use the tinker console command (~1.0).",
-                "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).",
-                "league/flysystem-cached-adapter": "Required to use Flysystem caching (~1.0).",
-                "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).",
-                "nexmo/client": "Required to use the Nexmo transport (~1.0).",
-                "pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).",
-                "predis/predis": "Required to use the redis cache and queue drivers (~1.0).",
-                "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~3.0).",
-                "symfony/css-selector": "Required to use some of the crawler integration testing tools (~3.3).",
-                "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~3.3).",
-                "symfony/psr-http-message-bridge": "Required to psr7 bridging features (~1.0)."
+                "ext-redis": "Required to use the Redis cache and queue drivers.",
+                "filp/whoops": "Required for friendly error pages in development (^2.4).",
+                "fzaninotto/faker": "Required to use the eloquent factory builder (^1.4).",
+                "guzzlehttp/guzzle": "Required to use the Mailgun mail driver and the ping methods on schedules (^6.0).",
+                "laravel/tinker": "Required to use the tinker console command (^1.0).",
+                "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).",
+                "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).",
+                "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).",
+                "moontoast/math": "Required to use ordered UUIDs (^1.1).",
+                "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).",
+                "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^3.0).",
+                "symfony/cache": "Required to PSR-6 cache bridge (^4.3.4).",
+                "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^1.2).",
+                "wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)."
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "5.5-dev"
+                    "dev-master": "6.x-dev"
                 }
             },
             "autoload": {
                 "framework",
                 "laravel"
             ],
-            "time": "2018-10-04T14:51:24+00:00"
+            "time": "2019-09-10T18:46:24+00:00"
         },
         {
             "name": "laravel/socialite",
-            "version": "3.0.x-dev",
+            "version": "v4.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/socialite.git",
-                "reference": "79316f36641f1916a50ab14d368acdf1d97e46de"
+                "reference": "f509d06e1e7323997b804c5152874f8aad4508e9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/socialite/zipball/79316f36641f1916a50ab14d368acdf1d97e46de",
-                "reference": "79316f36641f1916a50ab14d368acdf1d97e46de",
+                "url": "https://api.github.com/repos/laravel/socialite/zipball/f509d06e1e7323997b804c5152874f8aad4508e9",
+                "reference": "f509d06e1e7323997b804c5152874f8aad4508e9",
                 "shasum": ""
             },
             "require": {
+                "ext-json": "*",
                 "guzzlehttp/guzzle": "~6.0",
-                "illuminate/contracts": "~5.4",
-                "illuminate/http": "~5.4",
-                "illuminate/support": "~5.4",
+                "illuminate/http": "~5.7.0|~5.8.0|^6.0|^7.0",
+                "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0",
                 "league/oauth1-client": "~1.0",
-                "php": ">=5.6.4"
+                "php": "^7.1.3"
             },
             "require-dev": {
-                "mockery/mockery": "~0.9",
-                "phpunit/phpunit": "~4.0|~5.0"
+                "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0",
+                "mockery/mockery": "^1.0",
+                "phpunit/phpunit": "^7.0|^8.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "4.0-dev"
                 },
                 "laravel": {
                     "providers": [
                 "laravel",
                 "oauth"
             ],
-            "time": "2018-12-21T14:06:32+00:00"
+            "time": "2019-09-03T15:27:17+00:00"
         },
         {
             "name": "league/flysystem",
-            "version": "1.0.49",
+            "version": "1.0.55",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem.git",
-                "reference": "a63cc83d8a931b271be45148fa39ba7156782ffd"
+                "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a63cc83d8a931b271be45148fa39ba7156782ffd",
-                "reference": "a63cc83d8a931b271be45148fa39ba7156782ffd",
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/33c91155537c6dc899eacdc54a13ac6303f156e6",
+                "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6",
                 "shasum": ""
             },
             "require": {
                 "sftp",
                 "storage"
             ],
-            "time": "2018-11-23T23:41:29+00:00"
+            "time": "2019-08-24T11:17:19+00:00"
         },
         {
             "name": "league/flysystem-aws-s3-v3",
-            "version": "1.0.21",
+            "version": "1.0.23",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
-                "reference": "43523fec10a831ea48bedb3277e3f3fa218f4e49"
+                "reference": "15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/43523fec10a831ea48bedb3277e3f3fa218f4e49",
-                "reference": "43523fec10a831ea48bedb3277e3f3fa218f4e49",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4",
+                "reference": "15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "Flysystem adapter for the AWS S3 SDK v3.x",
-            "time": "2018-10-08T07:53:55+00:00"
+            "time": "2019-06-05T17:18:29+00:00"
         },
         {
             "name": "league/oauth1-client",
         },
         {
             "name": "monolog/monolog",
-            "version": "1.24.0",
+            "version": "2.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Seldaek/monolog.git",
-                "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266"
+                "reference": "68545165e19249013afd1d6f7485aecff07a2d22"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
-                "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/68545165e19249013afd1d6f7485aecff07a2d22",
+                "reference": "68545165e19249013afd1d6f7485aecff07a2d22",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.0",
-                "psr/log": "~1.0"
+                "php": "^7.2",
+                "psr/log": "^1.0.1"
             },
             "provide": {
                 "psr/log-implementation": "1.0.0"
             "require-dev": {
                 "aws/aws-sdk-php": "^2.4.9 || ^3.0",
                 "doctrine/couchdb": "~1.0@dev",
-                "graylog2/gelf-php": "~1.0",
-                "jakub-onderka/php-parallel-lint": "0.9",
+                "elasticsearch/elasticsearch": "^6.0",
+                "graylog2/gelf-php": "^1.4.2",
+                "jakub-onderka/php-parallel-lint": "^0.9",
                 "php-amqplib/php-amqplib": "~2.4",
                 "php-console/php-console": "^3.1.3",
-                "phpunit/phpunit": "~4.5",
-                "phpunit/phpunit-mock-objects": "2.3.0",
+                "phpspec/prophecy": "^1.6.1",
+                "phpunit/phpunit": "^8.3",
+                "predis/predis": "^1.1",
+                "rollbar/rollbar": "^1.3",
                 "ruflin/elastica": ">=0.90 <3.0",
-                "sentry/sentry": "^0.13",
                 "swiftmailer/swiftmailer": "^5.3|^6.0"
             },
             "suggest": {
                 "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
                 "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+                "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
                 "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
-                "ext-mongo": "Allow sending log messages to a MongoDB server",
+                "ext-mbstring": "Allow to work properly with unicode symbols",
+                "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
                 "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
-                "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+                "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
                 "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
                 "php-console/php-console": "Allow sending log messages to Google Chrome",
                 "rollbar/rollbar": "Allow sending log messages to Rollbar",
-                "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
-                "sentry/sentry": "Allow sending log messages to a Sentry server"
+                "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0.x-dev"
+                    "dev-master": "2.x-dev"
                 }
             },
             "autoload": {
                 "logging",
                 "psr-3"
             ],
-            "time": "2018-11-05T09:00:11+00:00"
+            "time": "2019-08-30T09:56:44+00:00"
         },
         {
-            "name": "mtdowling/cron-expression",
-            "version": "v1.2.1",
+            "name": "mtdowling/jmespath.php",
+            "version": "2.4.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/mtdowling/cron-expression.git",
-                "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad"
+                "url": "https://github.com/jmespath/jmespath.php.git",
+                "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad",
-                "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad",
+                "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
+                "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.2"
+                "php": ">=5.4.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.0|~5.0"
-            },
-            "type": "library",
-            "autoload": {
-                "psr-4": {
-                    "Cron\\": "src/Cron/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Michael Dowling",
-                    "email": "[email protected]",
-                    "homepage": "https://github.com/mtdowling"
-                }
-            ],
-            "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
-            "keywords": [
-                "cron",
-                "schedule"
-            ],
-            "time": "2017-01-23T04:29:33+00:00"
-        },
-        {
-            "name": "mtdowling/jmespath.php",
-            "version": "2.4.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/jmespath/jmespath.php.git",
-                "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
-                "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=5.4.0"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "~4.0"
+                "phpunit/phpunit": "~4.0"
             },
             "bin": [
                 "bin/jp.php"
         },
         {
             "name": "nesbot/carbon",
-            "version": "1.36.2",
+            "version": "2.24.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9"
+                "reference": "934459c5ac0658bc765ad1e53512c7c77adcac29"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
-                "reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/934459c5ac0658bc765ad1e53512c7c77adcac29",
+                "reference": "934459c5ac0658bc765ad1e53512c7c77adcac29",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.9",
-                "symfony/translation": "~2.6 || ~3.0 || ~4.0"
+                "ext-json": "*",
+                "php": "^7.1.8 || ^8.0",
+                "symfony/translation": "^3.4 || ^4.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7"
-            },
-            "suggest": {
-                "friendsofphp/php-cs-fixer": "Needed for the `composer phpcs` command. Allow to automatically fix code style.",
-                "phpstan/phpstan": "Needed for the `composer phpstan` command. Allow to detect potential errors."
+                "friendsofphp/php-cs-fixer": "^2.14 || ^3.0",
+                "kylekatarnls/multi-tester": "^1.1",
+                "phpmd/phpmd": "dev-php-7.1-compatibility",
+                "phpstan/phpstan": "^0.11",
+                "phpunit/phpunit": "^7.5 || ^8.0",
+                "squizlabs/php_codesniffer": "^3.4"
             },
+            "bin": [
+                "bin/carbon"
+            ],
             "type": "library",
             "extra": {
                 "laravel": {
             },
             "autoload": {
                 "psr-4": {
-                    "": "src/"
+                    "Carbon\\": "src/Carbon/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                     "name": "Brian Nesbitt",
                     "email": "[email protected]",
                     "homepage": "http://nesbot.com"
+                },
+                {
+                    "name": "kylekatarnls",
+                    "homepage": "http://github.com/kylekatarnls"
                 }
             ],
-            "description": "A simple API extension for DateTime.",
+            "description": "A API extension for DateTime that supports 281 different languages.",
             "homepage": "http://carbon.nesbot.com",
             "keywords": [
                 "date",
                 "datetime",
                 "time"
             ],
-            "time": "2018-12-28T10:07:33+00:00"
+            "time": "2019-08-31T16:37:55+00:00"
+        },
+        {
+            "name": "opis/closure",
+            "version": "3.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/opis/closure.git",
+                "reference": "60a97fff133b1669a5b1776aa8ab06db3f3962b7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/opis/closure/zipball/60a97fff133b1669a5b1776aa8ab06db3f3962b7",
+                "reference": "60a97fff133b1669a5b1776aa8ab06db3f3962b7",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.4 || ^7.0"
+            },
+            "require-dev": {
+                "jeremeamia/superclosure": "^2.0",
+                "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Opis\\Closure\\": "src/"
+                },
+                "files": [
+                    "functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marius Sarca",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Sorin Sarca",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.",
+            "homepage": "https://opis.io/closure",
+            "keywords": [
+                "anonymous functions",
+                "closure",
+                "function",
+                "serializable",
+                "serialization",
+                "serialize"
+            ],
+            "time": "2019-09-02T21:07:33+00:00"
         },
         {
             "name": "paragonie/random_compat",
         },
         {
             "name": "phenx/php-svg-lib",
-            "version": "v0.3.2",
+            "version": "v0.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/PhenX/php-svg-lib.git",
-                "reference": "ccc46ef6340d4b8a4a68047e68d8501ea961442c"
+                "reference": "5fa61b65e612ce1ae15f69b3d223cb14ecc60e32"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/ccc46ef6340d4b8a4a68047e68d8501ea961442c",
-                "reference": "ccc46ef6340d4b8a4a68047e68d8501ea961442c",
+                "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/5fa61b65e612ce1ae15f69b3d223cb14ecc60e32",
+                "reference": "5fa61b65e612ce1ae15f69b3d223cb14ecc60e32",
                 "shasum": ""
             },
             "require": {
-                "sabberworm/php-css-parser": "8.1.*"
+                "sabberworm/php-css-parser": "^8.3"
             },
             "require-dev": {
-                "phpunit/phpunit": "~5.0"
+                "phpunit/phpunit": "^5.5|^6.5"
             },
             "type": "library",
             "autoload": {
-                "psr-0": {
-                    "Svg\\": "src/"
+                "psr-4": {
+                    "Svg\\": "src/Svg"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "description": "A library to read, parse and export to PDF SVG files.",
             "homepage": "https://github.com/PhenX/php-svg-lib",
-            "time": "2018-06-03T10:10:03+00:00"
+            "time": "2019-09-11T20:02:13+00:00"
+        },
+        {
+            "name": "phpoption/phpoption",
+            "version": "1.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/schmittjoh/php-option.git",
+                "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed",
+                "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.7.*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "PhpOption\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache2"
+            ],
+            "authors": [
+                {
+                    "name": "Johannes M. Schmitt",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Option Type for PHP",
+            "keywords": [
+                "language",
+                "option",
+                "php",
+                "type"
+            ],
+            "time": "2015-07-25T16:39:46+00:00"
         },
         {
             "name": "predis/predis",
         },
         {
             "name": "ralouphie/getallheaders",
-            "version": "2.0.5",
+            "version": "3.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/ralouphie/getallheaders.git",
-                "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa"
+                "reference": "120b605dfeb996808c31b6477290a714d356e822"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
-                "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
+                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3"
+                "php": ">=5.6"
             },
             "require-dev": {
-                "phpunit/phpunit": "~3.7.0",
-                "satooshi/php-coveralls": ">=1.0"
+                "php-coveralls/php-coveralls": "^2.1",
+                "phpunit/phpunit": "^5 || ^6.5"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "A polyfill for getallheaders.",
-            "time": "2016-02-11T07:05:27+00:00"
+            "time": "2019-03-08T08:55:37+00:00"
         },
         {
             "name": "ramsey/uuid",
         },
         {
             "name": "sabberworm/php-css-parser",
-            "version": "8.1.0",
+            "version": "8.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sabberworm/PHP-CSS-Parser.git",
-                "reference": "850cbbcbe7fbb155387a151ea562897a67e242ef"
+                "reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/850cbbcbe7fbb155387a151ea562897a67e242ef",
-                "reference": "850cbbcbe7fbb155387a151ea562897a67e242ef",
+                "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f",
+                "reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "*"
+                "codacy/coverage": "^1.4",
+                "phpunit/phpunit": "~4.8"
             },
             "type": "library",
             "autoload": {
                 "parser",
                 "stylesheet"
             ],
-            "time": "2016-07-19T19:14:21+00:00"
+            "time": "2019-02-22T07:42:52+00:00"
         },
         {
             "name": "socialiteproviders/discord",
         },
         {
             "name": "socialiteproviders/manager",
-            "version": "v3.3.4",
+            "version": "v3.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Manager.git",
-                "reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030"
+                "reference": "e3e8e78b9a3060801cd008941a0894a0a0c479e1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/58b72a667da292a1d0a0b1e6e9aeda4053617030",
-                "reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030",
+                "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/e3e8e78b9a3060801cd008941a0894a0a0c479e1",
+                "reference": "e3e8e78b9a3060801cd008941a0894a0a0c479e1",
                 "shasum": ""
             },
             "require": {
+                "illuminate/support": "~5.4|~5.7.0|~5.8.0|^6.0",
                 "laravel/socialite": "~3.0|~4.0",
                 "php": "^5.6 || ^7.0"
             },
                 {
                     "name": "Anton Komarev",
                     "email": "[email protected]"
+                },
+                {
+                    "name": "Miguel Piedrafita",
+                    "email": "[email protected]"
                 }
             ],
             "description": "Easily add new or override built-in providers in Laravel Socialite.",
-            "time": "2019-01-16T07:58:54+00:00"
+            "time": "2019-09-09T03:07:52+00:00"
         },
         {
             "name": "socialiteproviders/microsoft-azure",
         },
         {
             "name": "socialiteproviders/twitch",
-            "version": "v3.0.0",
+            "version": "v5.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Twitch.git",
-                "reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93"
+                "reference": "8c19b26ff24c40cc019413042a5492c5ed21a658"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/a7ad148c0b42d0c607d8a034b6e47faf5fc85e93",
-                "reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93",
+                "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/8c19b26ff24c40cc019413042a5492c5ed21a658",
+                "reference": "8c19b26ff24c40cc019413042a5492c5ed21a658",
                 "shasum": ""
             },
             "require": {
                 "php": "^5.6 || ^7.0",
-                "socialiteproviders/manager": "~3.0"
+                "socialiteproviders/manager": "~2.0 || ~3.0"
             },
             "type": "library",
             "autoload": {
                 }
             ],
             "description": "Twitch OAuth2 Provider for Laravel Socialite",
-            "time": "2017-01-25T09:48:29+00:00"
+            "time": "2018-06-20T10:59:51+00:00"
         },
         {
             "name": "swiftmailer/swiftmailer",
-            "version": "v6.1.3",
+            "version": "v6.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/swiftmailer/swiftmailer.git",
-                "reference": "8ddcb66ac10c392d3beb54829eef8ac1438595f4"
+                "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/8ddcb66ac10c392d3beb54829eef8ac1438595f4",
-                "reference": "8ddcb66ac10c392d3beb54829eef8ac1438595f4",
+                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
+                "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
                 "shasum": ""
             },
             "require": {
                 "egulias/email-validator": "~2.0",
-                "php": ">=7.0.0"
+                "php": ">=7.0.0",
+                "symfony/polyfill-iconv": "^1.0",
+                "symfony/polyfill-intl-idn": "^1.10",
+                "symfony/polyfill-mbstring": "^1.0"
             },
             "require-dev": {
                 "mockery/mockery": "~0.9.1",
-                "symfony/phpunit-bridge": "~3.3@dev"
+                "symfony/phpunit-bridge": "^3.4.19|^4.1.8"
             },
             "suggest": {
                 "ext-intl": "Needed to support internationalized email addresses",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.1-dev"
+                    "dev-master": "6.2-dev"
                 }
             },
             "autoload": {
                 "mail",
                 "mailer"
             ],
-            "time": "2018-09-11T07:12:52+00:00"
+            "time": "2019-04-21T09:21:45+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "b0878233cb5c4391347e5495089c7af11b8e6201"
+                "reference": "de63799239b3881b8a08f8481b22348f77ed7b36"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/b0878233cb5c4391347e5495089c7af11b8e6201",
-                "reference": "b0878233cb5c4391347e5495089c7af11b8e6201",
+                "url": "https://api.github.com/repos/symfony/console/zipball/de63799239b3881b8a08f8481b22348f77ed7b36",
+                "reference": "de63799239b3881b8a08f8481b22348f77ed7b36",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
-                "symfony/debug": "~2.8|~3.0",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php73": "^1.8",
+                "symfony/service-contracts": "^1.1"
             },
             "conflict": {
-                "symfony/dependency-injection": "<3.3"
+                "symfony/dependency-injection": "<3.4",
+                "symfony/event-dispatcher": "<4.3",
+                "symfony/process": "<3.3"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~3.3",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/event-dispatcher": "~2.8|~3.0",
-                "symfony/filesystem": "~2.8|~3.0",
-                "symfony/http-kernel": "~2.8|~3.0",
-                "symfony/process": "~2.8|~3.0"
+                "symfony/config": "~3.4|~4.0",
+                "symfony/dependency-injection": "~3.4|~4.0",
+                "symfony/event-dispatcher": "^4.3",
+                "symfony/lock": "~3.4|~4.0",
+                "symfony/process": "~3.4|~4.0",
+                "symfony/var-dumper": "^4.3"
             },
             "suggest": {
                 "psr/log": "For using the console logger",
                 "symfony/event-dispatcher": "",
-                "symfony/filesystem": "",
+                "symfony/lock": "",
                 "symfony/process": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-29T21:27:59+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.1.10",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d"
+                "reference": "c6e5e2a00db768c92c3ae131532af4e1acc7bd03"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d",
-                "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/c6e5e2a00db768c92c3ae131532af4e1acc7bd03",
+                "reference": "c6e5e2a00db768c92c3ae131532af4e1acc7bd03",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^7.1.3"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.1-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "Jean-François Simon",
-                    "email": "[email protected]"
-                },
                 {
                     "name": "Fabien Potencier",
                     "email": "[email protected]"
                 },
+                {
+                    "name": "Jean-François Simon",
+                    "email": "[email protected]"
+                },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2017-01-02T20:31:54+00:00"
+            "time": "2019-08-20T14:07:54+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13"
+                "reference": "afcdea44a2e399c1e4b52246ec8d54c715393ced"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/7c13ae8ce1e2adbbd574fc39de7be498e1284e13",
-                "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/afcdea44a2e399c1e4b52246ec8d54c715393ced",
+                "reference": "afcdea44a2e399c1e4b52246ec8d54c715393ced",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
+                "php": "^7.1.3",
                 "psr/log": "~1.0"
             },
             "conflict": {
-                "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
+                "symfony/http-kernel": "<3.4"
             },
             "require-dev": {
-                "symfony/http-kernel": "~2.8|~3.0"
+                "symfony/http-kernel": "~3.4|~4.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-28T15:27:31+00:00"
+            "time": "2019-08-20T14:27:59+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e"
+                "reference": "429d0a1451d4c9c4abe1959b2986b88794b9b7d2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/67535f1e3fd662bdc68d7ba317c93eecd973617e",
-                "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/429d0a1451d4c9c4abe1959b2986b88794b9b7d2",
+                "reference": "429d0a1451d4c9c4abe1959b2986b88794b9b7d2",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^7.1.3",
+                "symfony/event-dispatcher-contracts": "^1.1"
             },
             "conflict": {
-                "symfony/dependency-injection": "<3.3"
+                "symfony/dependency-injection": "<3.4"
+            },
+            "provide": {
+                "psr/event-dispatcher-implementation": "1.0",
+                "symfony/event-dispatcher-implementation": "1.1"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/expression-language": "~2.8|~3.0",
-                "symfony/stopwatch": "~2.8|~3.0"
+                "symfony/config": "~3.4|~4.0",
+                "symfony/dependency-injection": "~3.4|~4.0",
+                "symfony/expression-language": "~3.4|~4.0",
+                "symfony/http-foundation": "^3.4|^4.0",
+                "symfony/service-contracts": "^1.1",
+                "symfony/stopwatch": "~3.4|~4.0"
             },
             "suggest": {
                 "symfony/dependency-injection": "",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2017-06-09T14:53:08+00:00"
+            "time": "2019-08-26T08:55:16+00:00"
+        },
+        {
+            "name": "symfony/event-dispatcher-contracts",
+            "version": "v1.1.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+                "reference": "c61766f4440ca687de1084a5c00b08e167a2575c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c61766f4440ca687de1084a5c00b08e167a2575c",
+                "reference": "c61766f4440ca687de1084a5c00b08e167a2575c",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3"
+            },
+            "suggest": {
+                "psr/event-dispatcher": "",
+                "symfony/event-dispatcher-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\EventDispatcher\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to dispatching event",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-06-20T06:46:26+00:00"
         },
         {
             "name": "symfony/finder",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/finder.git",
-                "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
+                "reference": "86c1c929f0a4b24812e1eb109262fc3372c8e9f2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4",
-                "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/86c1c929f0a4b24812e1eb109262fc3372c8e9f2",
+                "reference": "86c1c929f0a4b24812e1eb109262fc3372c8e9f2",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^7.1.3"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Finder Component",
             "homepage": "https://symfony.com",
-            "time": "2017-06-01T21:01:25+00:00"
+            "time": "2019-08-14T12:26:46+00:00"
         },
         {
             "name": "symfony/http-foundation",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031"
+                "reference": "d804bea118ff340a12e22a79f9c7e7eb56b35adc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/49e8cd2d59a7aa9bfab19e46de680c76e500a031",
-                "reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d804bea118ff340a12e22a79f9c7e7eb56b35adc",
+                "reference": "d804bea118ff340a12e22a79f9c7e7eb56b35adc",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
+                "php": "^7.1.3",
+                "symfony/mime": "^4.3",
                 "symfony/polyfill-mbstring": "~1.1"
             },
             "require-dev": {
-                "symfony/expression-language": "~2.8|~3.0"
+                "predis/predis": "~1.0",
+                "symfony/expression-language": "~3.4|~4.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony HttpFoundation Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-21T11:04:46+00:00"
+            "time": "2019-08-26T08:55:16+00:00"
         },
         {
             "name": "symfony/http-kernel",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-                "reference": "db10d05f1d95e4168e638db7a81c79616f568ea5"
+                "reference": "5e0fc71be03d52cd00c423061cfd300bd6f92a52"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/db10d05f1d95e4168e638db7a81c79616f568ea5",
-                "reference": "db10d05f1d95e4168e638db7a81c79616f568ea5",
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/5e0fc71be03d52cd00c423061cfd300bd6f92a52",
+                "reference": "5e0fc71be03d52cd00c423061cfd300bd6f92a52",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
+                "php": "^7.1.3",
                 "psr/log": "~1.0",
-                "symfony/debug": "~2.8|~3.0",
-                "symfony/event-dispatcher": "~2.8|~3.0",
-                "symfony/http-foundation": "~3.3"
+                "symfony/debug": "~3.4|~4.0",
+                "symfony/event-dispatcher": "^4.3",
+                "symfony/http-foundation": "^4.1.1",
+                "symfony/polyfill-ctype": "~1.8",
+                "symfony/polyfill-php73": "^1.9"
             },
             "conflict": {
-                "symfony/config": "<2.8",
-                "symfony/dependency-injection": "<3.3",
-                "symfony/var-dumper": "<3.3",
+                "symfony/browser-kit": "<4.3",
+                "symfony/config": "<3.4",
+                "symfony/dependency-injection": "<4.3",
+                "symfony/translation": "<4.2",
+                "symfony/var-dumper": "<4.1.1",
                 "twig/twig": "<1.34|<2.4,>=2"
             },
+            "provide": {
+                "psr/log-implementation": "1.0"
+            },
             "require-dev": {
                 "psr/cache": "~1.0",
-                "symfony/browser-kit": "~2.8|~3.0",
-                "symfony/class-loader": "~2.8|~3.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/console": "~2.8|~3.0",
-                "symfony/css-selector": "~2.8|~3.0",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/dom-crawler": "~2.8|~3.0",
-                "symfony/expression-language": "~2.8|~3.0",
-                "symfony/finder": "~2.8|~3.0",
-                "symfony/process": "~2.8|~3.0",
-                "symfony/routing": "~2.8|~3.0",
-                "symfony/stopwatch": "~2.8|~3.0",
-                "symfony/templating": "~2.8|~3.0",
-                "symfony/translation": "~2.8|~3.0",
-                "symfony/var-dumper": "~3.3"
+                "symfony/browser-kit": "^4.3",
+                "symfony/config": "~3.4|~4.0",
+                "symfony/console": "~3.4|~4.0",
+                "symfony/css-selector": "~3.4|~4.0",
+                "symfony/dependency-injection": "^4.3",
+                "symfony/dom-crawler": "~3.4|~4.0",
+                "symfony/expression-language": "~3.4|~4.0",
+                "symfony/finder": "~3.4|~4.0",
+                "symfony/process": "~3.4|~4.0",
+                "symfony/routing": "~3.4|~4.0",
+                "symfony/stopwatch": "~3.4|~4.0",
+                "symfony/templating": "~3.4|~4.0",
+                "symfony/translation": "~4.2",
+                "symfony/translation-contracts": "^1.1",
+                "symfony/var-dumper": "^4.1.1",
+                "twig/twig": "^1.34|^2.4"
             },
             "suggest": {
                 "symfony/browser-kit": "",
-                "symfony/class-loader": "",
                 "symfony/config": "",
                 "symfony/console": "",
                 "symfony/dependency-injection": "",
-                "symfony/finder": "",
                 "symfony/var-dumper": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony HttpKernel Component",
             "homepage": "https://symfony.com",
-            "time": "2017-08-01T10:25:59+00:00"
+            "time": "2019-08-26T16:47:42+00:00"
         },
         {
-            "name": "symfony/polyfill-ctype",
-            "version": "v1.10.0",
+            "name": "symfony/mime",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
+                "url": "https://github.com/symfony/mime.git",
+                "reference": "987a05df1c6ac259b34008b932551353f4f408df"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
-                "reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/987a05df1c6ac259b34008b932551353f4f408df",
+                "reference": "987a05df1c6ac259b34008b932551353f4f408df",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": "^7.1.3",
+                "symfony/polyfill-intl-idn": "^1.10",
+                "symfony/polyfill-mbstring": "^1.0"
             },
-            "suggest": {
-                "ext-ctype": "For best performance"
+            "require-dev": {
+                "egulias/email-validator": "^2.1.10",
+                "symfony/dependency-injection": "~3.4|^4.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.9-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Polyfill\\Ctype\\": ""
+                    "Symfony\\Component\\Mime\\": ""
                 },
-                "files": [
-                    "bootstrap.php"
+                "exclude-from-classmap": [
+                    "/Tests/"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "authors": [
                 {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
+                    "name": "Fabien Potencier",
+                    "email": "[email protected]"
                 },
                 {
-                    "name": "Gert de Pagter",
-                    "email": "[email protected]"
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony polyfill for ctype functions",
+            "description": "A library to manipulate MIME messages",
             "homepage": "https://symfony.com",
             "keywords": [
-                "compatibility",
-                "ctype",
-                "polyfill",
-                "portable"
+                "mime",
+                "mime-type"
             ],
-            "time": "2018-08-06T14:22:27+00:00"
+            "time": "2019-08-22T08:16:11+00:00"
         },
         {
-            "name": "symfony/polyfill-mbstring",
-            "version": "v1.10.0",
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.12.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "550ebaac289296ce228a706d0867afc34687e3f4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
-                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4",
+                "reference": "550ebaac289296ce228a706d0867afc34687e3f4",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.3"
             },
             "suggest": {
-                "ext-mbstring": "For best performance"
+                "ext-ctype": "For best performance"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.9-dev"
+                    "dev-master": "1.12-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Polyfill\\Mbstring\\": ""
+                    "Symfony\\Polyfill\\Ctype\\": ""
                 },
                 "files": [
                     "bootstrap.php"
             ],
             "authors": [
                 {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
                 },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony polyfill for the Mbstring extension",
+            "description": "Symfony polyfill for ctype functions",
             "homepage": "https://symfony.com",
             "keywords": [
                 "compatibility",
-                "mbstring",
+                "ctype",
                 "polyfill",
-                "portable",
-                "shim"
+                "portable"
             ],
-            "time": "2018-09-21T13:07:52+00:00"
+            "time": "2019-08-06T08:03:45+00:00"
         },
         {
-            "name": "symfony/process",
-            "version": "v3.3.6",
+            "name": "symfony/polyfill-iconv",
+            "version": "v1.12.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/symfony/process.git",
-                "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a"
+                "url": "https://github.com/symfony/polyfill-iconv.git",
+                "reference": "685968b11e61a347c18bf25db32effa478be610f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/07432804942b9f6dd7b7377faf9920af5f95d70a",
-                "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a",
+                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/685968b11e61a347c18bf25db32effa478be610f",
+                "reference": "685968b11e61a347c18bf25db32effa478be610f",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "ext-iconv": "For best performance"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "1.12-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Symfony\\Component\\Process\\": ""
+                    "Symfony\\Polyfill\\Iconv\\": ""
                 },
-                "exclude-from-classmap": [
-                    "/Tests/"
+                "files": [
+                    "bootstrap.php"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "authors": [
                 {
-                    "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com"
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
                 },
                 {
                     "name": "Symfony Community",
                     "homepage": "https://symfony.com/contributors"
                 }
             ],
-            "description": "Symfony Process Component",
+            "description": "Symfony polyfill for the Iconv extension",
             "homepage": "https://symfony.com",
-            "time": "2017-07-13T13:05:09+00:00"
+            "keywords": [
+                "compatibility",
+                "iconv",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-08-06T08:03:45+00:00"
+        },
+        {
+            "name": "symfony/polyfill-intl-idn",
+            "version": "v1.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-idn.git",
+                "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
+                "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "symfony/polyfill-mbstring": "^1.3",
+                "symfony/polyfill-php72": "^1.9"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.12-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\Idn\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Laurent Bassin",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "idn",
+                "intl",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-08-06T08:03:45+00:00"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+                "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.12-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-08-06T08:03:45+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php72",
+            "version": "v1.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php72.git",
+                "reference": "04ce3335667451138df4307d6a9b61565560199e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e",
+                "reference": "04ce3335667451138df4307d6a9b61565560199e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.12-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php72\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-08-06T08:03:45+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php73",
+            "version": "v1.12.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php73.git",
+                "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/2ceb49eaccb9352bff54d22570276bb75ba4a188",
+                "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.12-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php73\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ],
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2019-08-06T08:03:45+00:00"
+        },
+        {
+            "name": "symfony/process",
+            "version": "v4.3.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/process.git",
+                "reference": "e89969c00d762349f078db1128506f7f3dcc0d4a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/process/zipball/e89969c00d762349f078db1128506f7f3dcc0d4a",
+                "reference": "e89969c00d762349f078db1128506f7f3dcc0d4a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.3-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Process\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony Process Component",
+            "homepage": "https://symfony.com",
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "symfony/routing",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/routing.git",
-                "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26"
+                "reference": "ff1049f6232dc5b6023b1ff1c6de56f82bcd264f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/routing/zipball/4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26",
-                "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26",
+                "url": "https://api.github.com/repos/symfony/routing/zipball/ff1049f6232dc5b6023b1ff1c6de56f82bcd264f",
+                "reference": "ff1049f6232dc5b6023b1ff1c6de56f82bcd264f",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^7.1.3"
             },
             "conflict": {
-                "symfony/config": "<2.8",
-                "symfony/dependency-injection": "<3.3",
-                "symfony/yaml": "<3.3"
+                "symfony/config": "<4.2",
+                "symfony/dependency-injection": "<3.4",
+                "symfony/yaml": "<3.4"
             },
             "require-dev": {
-                "doctrine/annotations": "~1.0",
-                "doctrine/common": "~2.2",
+                "doctrine/annotations": "~1.2",
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/dependency-injection": "~3.3",
-                "symfony/expression-language": "~2.8|~3.0",
-                "symfony/http-foundation": "~2.8|~3.0",
-                "symfony/yaml": "~3.3"
+                "symfony/config": "~4.2",
+                "symfony/dependency-injection": "~3.4|~4.0",
+                "symfony/expression-language": "~3.4|~4.0",
+                "symfony/http-foundation": "~3.4|~4.0",
+                "symfony/yaml": "~3.4|~4.0"
             },
             "suggest": {
                 "doctrine/annotations": "For using the annotation loader",
                 "symfony/config": "For using the all-in-one router or any loader",
-                "symfony/dependency-injection": "For loading routes from a service",
                 "symfony/expression-language": "For using expression matching",
                 "symfony/http-foundation": "For using a Symfony Request object",
                 "symfony/yaml": "For using the YAML loader"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
                 "uri",
                 "url"
             ],
-            "time": "2017-07-21T17:43:13+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
+        },
+        {
+            "name": "symfony/service-contracts",
+            "version": "v1.1.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/service-contracts.git",
+                "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ea7263d6b6d5f798b56a45a5b8d686725f2719a3",
+                "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3",
+                "psr/container": "^1.0"
+            },
+            "suggest": {
+                "symfony/service-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Service\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to writing services",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-08-20T14:44:19+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3"
+                "reference": "28498169dd334095fa981827992f3a24d50fed0f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3",
-                "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/28498169dd334095fa981827992f3a24d50fed0f",
+                "reference": "28498169dd334095fa981827992f3a24d50fed0f",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/translation-contracts": "^1.1.6"
             },
             "conflict": {
-                "symfony/config": "<2.8",
-                "symfony/yaml": "<3.3"
+                "symfony/config": "<3.4",
+                "symfony/dependency-injection": "<3.4",
+                "symfony/yaml": "<3.4"
+            },
+            "provide": {
+                "symfony/translation-implementation": "1.0"
             },
             "require-dev": {
                 "psr/log": "~1.0",
-                "symfony/config": "~2.8|~3.0",
-                "symfony/intl": "^2.8.18|^3.2.5",
-                "symfony/yaml": "~3.3"
+                "symfony/config": "~3.4|~4.0",
+                "symfony/console": "~3.4|~4.0",
+                "symfony/dependency-injection": "~3.4|~4.0",
+                "symfony/finder": "~2.8|~3.0|~4.0",
+                "symfony/http-kernel": "~3.4|~4.0",
+                "symfony/intl": "~3.4|~4.0",
+                "symfony/service-contracts": "^1.1.2",
+                "symfony/var-dumper": "~3.4|~4.0",
+                "symfony/yaml": "~3.4|~4.0"
             },
             "suggest": {
-                "psr/log": "To use logging capability in translator",
+                "psr/log-implementation": "To use logging capability in translator",
                 "symfony/config": "",
                 "symfony/yaml": ""
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2017-06-24T16:45:30+00:00"
+            "time": "2019-08-26T08:55:16+00:00"
+        },
+        {
+            "name": "symfony/translation-contracts",
+            "version": "v1.1.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/translation-contracts.git",
+                "reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/325b17c24f3ee23cbecfa63ba809c6d89b5fa04a",
+                "reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1.3"
+            },
+            "suggest": {
+                "symfony/translation-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Translation\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to translation",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "time": "2019-08-02T12:15:04+00:00"
         },
         {
             "name": "symfony/var-dumper",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
-                "reference": "b2623bccb969ad595c2090f9be498b74670d0663"
+                "reference": "641043e0f3e615990a0f29479f9c117e8a6698c6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b2623bccb969ad595c2090f9be498b74670d0663",
-                "reference": "b2623bccb969ad595c2090f9be498b74670d0663",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/641043e0f3e615990a0f29479f9c117e8a6698c6",
+                "reference": "641043e0f3e615990a0f29479f9c117e8a6698c6",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
-                "symfony/polyfill-mbstring": "~1.0"
+                "php": "^7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php72": "~1.5"
             },
             "conflict": {
-                "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
+                "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
+                "symfony/console": "<3.4"
             },
             "require-dev": {
                 "ext-iconv": "*",
+                "symfony/console": "~3.4|~4.0",
+                "symfony/process": "~3.4|~4.0",
                 "twig/twig": "~1.34|~2.4"
             },
             "suggest": {
                 "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
-                "ext-symfony_debug": ""
+                "ext-intl": "To show region name in time zone dump",
+                "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
             },
+            "bin": [
+                "Resources/bin/var-dump-server"
+            ],
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
                 "debug",
                 "dump"
             ],
-            "time": "2017-07-28T06:06:09+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "tijsverkoyen/css-to-inline-styles",
         },
         {
             "name": "vlucas/phpdotenv",
-            "version": "v2.5.2",
+            "version": "v3.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/vlucas/phpdotenv.git",
-                "reference": "cfd5dc225767ca154853752abc93aeec040fcf36"
+                "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/cfd5dc225767ca154853752abc93aeec040fcf36",
-                "reference": "cfd5dc225767ca154853752abc93aeec040fcf36",
+                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1bdf24f065975594f6a117f0f1f6cabf1333b156",
+                "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.9"
+                "php": "^5.4 || ^7.0",
+                "phpoption/phpoption": "^1.5",
+                "symfony/polyfill-ctype": "^1.9"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.0"
+                "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.5-dev"
+                    "dev-master": "3.6-dev"
                 }
             },
             "autoload": {
                 "BSD-3-Clause"
             ],
             "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "[email protected]",
+                    "homepage": "https://gjcampbell.co.uk/"
+                },
                 {
                     "name": "Vance Lucas",
                     "email": "[email protected]",
-                    "homepage": "http://www.vancelucas.com"
+                    "homepage": "https://vancelucas.com/"
                 }
             ],
             "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
                 "env",
                 "environment"
             ],
-            "time": "2018-10-30T17:29:25+00:00"
+            "time": "2019-09-10T21:37:39+00:00"
         }
     ],
     "packages-dev": [
         {
             "name": "barryvdh/laravel-debugbar",
-            "version": "v3.2.1",
+            "version": "v3.2.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/barryvdh/laravel-debugbar.git",
-                "reference": "9d5caf43c5f3a3aea2178942f281054805872e7c"
+                "reference": "18208d64897ab732f6c04a19b319fe8f1d57a9c0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/9d5caf43c5f3a3aea2178942f281054805872e7c",
-                "reference": "9d5caf43c5f3a3aea2178942f281054805872e7c",
+                "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/18208d64897ab732f6c04a19b319fe8f1d57a9c0",
+                "reference": "18208d64897ab732f6c04a19b319fe8f1d57a9c0",
                 "shasum": ""
             },
             "require": {
-                "illuminate/routing": "5.5.x|5.6.x|5.7.x",
-                "illuminate/session": "5.5.x|5.6.x|5.7.x",
-                "illuminate/support": "5.5.x|5.6.x|5.7.x",
+                "illuminate/routing": "^5.5|^6",
+                "illuminate/session": "^5.5|^6",
+                "illuminate/support": "^5.5|^6",
                 "maximebf/debugbar": "~1.15.0",
                 "php": ">=7.0",
                 "symfony/debug": "^3|^4",
                 "profiler",
                 "webprofiler"
             ],
-            "time": "2018-11-09T08:37:55+00:00"
+            "time": "2019-08-29T07:01:03+00:00"
         },
         {
             "name": "barryvdh/laravel-ide-helper",
-            "version": "v2.5.3",
+            "version": "v2.6.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/barryvdh/laravel-ide-helper.git",
-                "reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2"
+                "reference": "8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/3d7f1240896a075aa23b13f82dfcbe165dadeef2",
-                "reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2",
+                "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3",
+                "reference": "8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3",
                 "shasum": ""
             },
             "require": {
                 "barryvdh/reflection-docblock": "^2.0.6",
                 "composer/composer": "^1.6",
-                "illuminate/console": "^5.5,<5.8",
-                "illuminate/filesystem": "^5.5,<5.8",
-                "illuminate/support": "^5.5,<5.8",
+                "doctrine/dbal": "~2.3",
+                "illuminate/console": "^5.5|^6",
+                "illuminate/filesystem": "^5.5|^6",
+                "illuminate/support": "^5.5|^6",
                 "php": ">=7"
             },
             "require-dev": {
-                "doctrine/dbal": "~2.3",
-                "illuminate/config": "^5.1,<5.8",
-                "illuminate/view": "^5.1,<5.8",
+                "illuminate/config": "^5.5|^6",
+                "illuminate/view": "^5.5|^6",
                 "phpro/grumphp": "^0.14",
                 "phpunit/phpunit": "4.*",
                 "scrutinizer/ocular": "~1.1",
                 "squizlabs/php_codesniffer": "^3"
             },
-            "suggest": {
-                "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)"
-            },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.5-dev"
+                    "dev-master": "2.6-dev"
                 },
                 "laravel": {
                     "providers": [
                 "phpstorm",
                 "sublime"
             ],
-            "time": "2018-12-19T12:12:05+00:00"
+            "time": "2019-09-08T09:56:38+00:00"
         },
         {
             "name": "barryvdh/reflection-docblock",
         },
         {
             "name": "composer/ca-bundle",
-            "version": "1.1.3",
+            "version": "1.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/ca-bundle.git",
-                "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660"
+                "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8afa52cd417f4ec417b4bfe86b68106538a87660",
-                "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660",
+                "url": "https://api.github.com/repos/composer/ca-bundle/zipball/10bb96592168a0f8e8f6dcde3532d9fa50b0b527",
+                "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527",
                 "shasum": ""
             },
             "require": {
                 "ext-openssl": "*",
                 "ext-pcre": "*",
-                "php": "^5.3.2 || ^7.0"
+                "php": "^5.3.2 || ^7.0 || ^8.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8",
                 "psr/log": "^1.0",
                 "symfony/process": "^2.5 || ^3.0 || ^4.0"
             },
                 "ssl",
                 "tls"
             ],
-            "time": "2018-10-18T06:09:13+00:00"
+            "time": "2019-08-30T08:44:50+00:00"
         },
         {
             "name": "composer/composer",
-            "version": "1.8.0",
+            "version": "1.9.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/composer.git",
-                "reference": "d8aef3af866b28786ce9b8647e52c42496436669"
+                "reference": "314aa57fdcfc942065996f59fb73a8b3f74f3fa5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/composer/zipball/d8aef3af866b28786ce9b8647e52c42496436669",
-                "reference": "d8aef3af866b28786ce9b8647e52c42496436669",
+                "url": "https://api.github.com/repos/composer/composer/zipball/314aa57fdcfc942065996f59fb73a8b3f74f3fa5",
+                "reference": "314aa57fdcfc942065996f59fb73a8b3f74f3fa5",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.8-dev"
+                    "dev-master": "1.9-dev"
                 }
             },
             "autoload": {
                     "homepage": "http://seld.be"
                 }
             ],
-            "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",
+            "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
             "homepage": "https://getcomposer.org/",
             "keywords": [
                 "autoload",
                 "dependency",
                 "package"
             ],
-            "time": "2018-12-03T09:31:16+00:00"
+            "time": "2019-08-02T18:55:33+00:00"
         },
         {
             "name": "composer/semver",
-            "version": "1.4.2",
+            "version": "1.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/semver.git",
-                "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573"
+                "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573",
-                "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573",
+                "url": "https://api.github.com/repos/composer/semver/zipball/46d9139568ccb8d9e7cdd4539cab7347568a5e2e",
+                "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e",
                 "shasum": ""
             },
             "require": {
                     "homepage": "http://robbast.nl"
                 }
             ],
-            "description": "Semver library that offers utilities, version constraint parsing and validation.",
+            "description": "Semver library that offers utilities, version constraint parsing and validation.",
+            "keywords": [
+                "semantic",
+                "semver",
+                "validation",
+                "versioning"
+            ],
+            "time": "2019-03-19T17:25:45+00:00"
+        },
+        {
+            "name": "composer/spdx-licenses",
+            "version": "1.5.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/spdx-licenses.git",
+                "reference": "7ac1e6aec371357df067f8a688c3d6974df68fa5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7ac1e6aec371357df067f8a688c3d6974df68fa5",
+                "reference": "7ac1e6aec371357df067f8a688c3d6974df68fa5",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.2 || ^7.0 || ^8.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Composer\\Spdx\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nils Adermann",
+                    "email": "[email protected]",
+                    "homepage": "http://www.naderman.de"
+                },
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "[email protected]",
+                    "homepage": "http://seld.be"
+                },
+                {
+                    "name": "Rob Bast",
+                    "email": "[email protected]",
+                    "homepage": "http://robbast.nl"
+                }
+            ],
+            "description": "SPDX licenses list and validation library.",
+            "keywords": [
+                "license",
+                "spdx",
+                "validator"
+            ],
+            "time": "2019-07-29T10:31:59+00:00"
+        },
+        {
+            "name": "composer/xdebug-handler",
+            "version": "1.3.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/xdebug-handler.git",
+                "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/46867cbf8ca9fb8d60c506895449eb799db1184f",
+                "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.2 || ^7.0",
+                "psr/log": "^1.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Composer\\XdebugHandler\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "John Stevenson",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Restarts a process without xdebug.",
+            "keywords": [
+                "Xdebug",
+                "performance"
+            ],
+            "time": "2019-05-27T17:52:04+00:00"
+        },
+        {
+            "name": "doctrine/instantiator",
+            "version": "1.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/instantiator.git",
+                "reference": "a2c590166b2133a4633738648b6b064edae0814a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a",
+                "reference": "a2c590166b2133a4633738648b6b064edae0814a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^6.0",
+                "ext-pdo": "*",
+                "ext-phar": "*",
+                "phpbench/phpbench": "^0.13",
+                "phpstan/phpstan-phpunit": "^0.11",
+                "phpstan/phpstan-shim": "^0.11",
+                "phpunit/phpunit": "^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "[email protected]",
+                    "homepage": "http://ocramius.github.com/"
+                }
+            ],
+            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+            "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
             "keywords": [
-                "semantic",
-                "semver",
-                "validation",
-                "versioning"
+                "constructor",
+                "instantiate"
             ],
-            "time": "2016-08-30T16:08:34+00:00"
+            "time": "2019-03-17T17:37:11+00:00"
         },
         {
-            "name": "composer/spdx-licenses",
-            "version": "1.5.0",
+            "name": "facade/flare-client-php",
+            "version": "1.0.4",
             "source": {
                 "type": "git",
-                "url": "https://github.com/composer/spdx-licenses.git",
-                "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2"
+                "url": "https://github.com/facade/flare-client-php.git",
+                "reference": "7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7a9556b22bd9d4df7cad89876b00af58ef20d3a2",
-                "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2",
+                "url": "https://api.github.com/repos/facade/flare-client-php/zipball/7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e",
+                "reference": "7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.2 || ^7.0"
+                "facade/ignition-contracts": "~1.0",
+                "illuminate/pipeline": "~5.5|~5.6|~5.7|~5.8|^6.0",
+                "php": "^7.1",
+                "symfony/http-foundation": "~3.3|~4.1",
+                "symfony/var-dumper": "^3.4|^4.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
-                "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+                "larapack/dd": "^1.1",
+                "phpunit/phpunit": "^7.0",
+                "spatie/phpunit-snapshot-assertions": "^2.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.x-dev"
+                    "dev-master": "1.0-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "Composer\\Spdx\\": "src"
-                }
+                    "Facade\\FlareClient\\": "src"
+                },
+                "files": [
+                    "src/helpers.php"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "MIT"
             ],
-            "authors": [
-                {
-                    "name": "Nils Adermann",
-                    "email": "[email protected]",
-                    "homepage": "http://www.naderman.de"
-                },
-                {
-                    "name": "Jordi Boggiano",
-                    "email": "[email protected]",
-                    "homepage": "http://seld.be"
-                },
-                {
-                    "name": "Rob Bast",
-                    "email": "[email protected]",
-                    "homepage": "http://robbast.nl"
-                }
-            ],
-            "description": "SPDX licenses list and validation library.",
+            "description": "Send PHP errors to Flare",
+            "homepage": "https://github.com/facade/flare-client-php",
             "keywords": [
-                "license",
-                "spdx",
-                "validator"
+                "exception",
+                "facade",
+                "flare",
+                "reporting"
             ],
-            "time": "2018-11-01T09:45:54+00:00"
+            "time": "2019-09-11T14:19:56+00:00"
         },
         {
-            "name": "composer/xdebug-handler",
-            "version": "1.3.1",
+            "name": "facade/ignition",
+            "version": "1.6.5",
             "source": {
                 "type": "git",
-                "url": "https://github.com/composer/xdebug-handler.git",
-                "reference": "dc523135366eb68f22268d069ea7749486458562"
+                "url": "https://github.com/facade/ignition.git",
+                "reference": "97244f6d511332f3574acab8242c09ddcfda892b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562",
-                "reference": "dc523135366eb68f22268d069ea7749486458562",
+                "url": "https://api.github.com/repos/facade/ignition/zipball/97244f6d511332f3574acab8242c09ddcfda892b",
+                "reference": "97244f6d511332f3574acab8242c09ddcfda892b",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.2 || ^7.0",
-                "psr/log": "^1.0"
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "facade/flare-client-php": "^1.0.4",
+                "facade/ignition-contracts": "^1.0",
+                "filp/whoops": "^2.4",
+                "illuminate/support": "~5.5.0 || ~5.6.0 || ~5.7.0 || ~5.8.0 || ^6.0",
+                "monolog/monolog": "^1.12 || ^2.0",
+                "php": "^7.1",
+                "scrivo/highlight.php": "^9.15",
+                "symfony/console": "^3.4 || ^4.0",
+                "symfony/var-dumper": "^3.4 || ^4.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
+                "friendsofphp/php-cs-fixer": "^2.14",
+                "mockery/mockery": "^1.2",
+                "orchestra/testbench": "^3.5 || ^3.6 || ^3.7 || ^3.8 || ^4.0"
+            },
+            "suggest": {
+                "laravel/telescope": "^2.0"
             },
             "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0-dev"
+                },
+                "laravel": {
+                    "providers": [
+                        "Facade\\Ignition\\IgnitionServiceProvider"
+                    ],
+                    "aliases": {
+                        "Flare": "Facade\\Ignition\\Facades\\Flare"
+                    }
+                }
+            },
             "autoload": {
                 "psr-4": {
-                    "Composer\\XdebugHandler\\": "src"
+                    "Facade\\Ignition\\": "src"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "MIT"
             ],
-            "authors": [
-                {
-                    "name": "John Stevenson",
-                    "email": "[email protected]"
-                }
-            ],
-            "description": "Restarts a process without xdebug.",
+            "description": "A beautiful error page for Laravel applications.",
+            "homepage": "https://github.com/facade/ignition",
             "keywords": [
-                "Xdebug",
-                "performance"
+                "error",
+                "flare",
+                "laravel",
+                "page"
             ],
-            "time": "2018-11-29T10:59:02+00:00"
+            "time": "2019-09-13T13:38:04+00:00"
         },
         {
-            "name": "doctrine/instantiator",
-            "version": "1.0.5",
+            "name": "facade/ignition-contracts",
+            "version": "1.0.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
+                "url": "https://github.com/facade/ignition-contracts.git",
+                "reference": "f445db0fb86f48e205787b2592840dd9c80ded28"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
-                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
+                "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/f445db0fb86f48e205787b2592840dd9c80ded28",
+                "reference": "f445db0fb86f48e205787b2592840dd9c80ded28",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3,<8.0-DEV"
-            },
-            "require-dev": {
-                "athletic/athletic": "~0.1.8",
-                "ext-pdo": "*",
-                "ext-phar": "*",
-                "phpunit/phpunit": "~4.0",
-                "squizlabs/php_codesniffer": "~2.0"
+                "php": "^7.1"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.0.x-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
-                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+                    "Facade\\IgnitionContracts\\": "src"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "authors": [
                 {
-                    "name": "Marco Pivetta",
-                    "email": "[email protected]",
-                    "homepage": "http://ocramius.github.com/"
+                    "name": "Freek Van der Herten",
+                    "email": "[email protected]",
+                    "homepage": "https://flareapp.io",
+                    "role": "Developer"
                 }
             ],
-            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
-            "homepage": "https://github.com/doctrine/instantiator",
+            "description": "Solution contracts for Ignition",
+            "homepage": "https://github.com/facade/ignition-contracts",
             "keywords": [
-                "constructor",
-                "instantiate"
+                "contracts",
+                "flare",
+                "ignition"
             ],
-            "time": "2015-06-14T21:17:01+00:00"
+            "time": "2019-08-30T14:06:08+00:00"
         },
         {
             "name": "filp/whoops",
-            "version": "2.3.1",
+            "version": "2.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/filp/whoops.git",
-                "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7"
+                "reference": "cde50e6720a39fdacb240159d3eea6865d51fd96"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/filp/whoops/zipball/bc0fd11bc455cc20ee4b5edabc63ebbf859324c7",
-                "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/cde50e6720a39fdacb240159d3eea6865d51fd96",
+                "reference": "cde50e6720a39fdacb240159d3eea6865d51fd96",
                 "shasum": ""
             },
             "require": {
             "authors": [
                 {
                     "name": "Filipe Dobreira",
-                    "homepage": "https://github.com/filp",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "homepage": "https://github.com/filp"
                 }
             ],
             "description": "php error handling for cool kids",
                 "throwable",
                 "whoops"
             ],
-            "time": "2018-10-23T09:00:00+00:00"
+            "time": "2019-08-07T09:00:00+00:00"
         },
         {
             "name": "fzaninotto/faker",
             ],
             "time": "2016-01-20T08:20:44+00:00"
         },
+        {
+            "name": "jakub-onderka/php-console-color",
+            "version": "v0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/JakubOnderka/PHP-Console-Color.git",
+                "reference": "d5deaecff52a0d61ccb613bb3804088da0307191"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/d5deaecff52a0d61ccb613bb3804088da0307191",
+                "reference": "d5deaecff52a0d61ccb613bb3804088da0307191",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "jakub-onderka/php-code-style": "1.0",
+                "jakub-onderka/php-parallel-lint": "1.0",
+                "jakub-onderka/php-var-dump-check": "0.*",
+                "phpunit/phpunit": "~4.3",
+                "squizlabs/php_codesniffer": "1.*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "JakubOnderka\\PhpConsoleColor\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-2-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Jakub Onderka",
+                    "email": "[email protected]"
+                }
+            ],
+            "time": "2018-09-29T17:23:10+00:00"
+        },
+        {
+            "name": "jakub-onderka/php-console-highlighter",
+            "version": "v0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git",
+                "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/9f7a229a69d52506914b4bc61bfdb199d90c5547",
+                "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547",
+                "shasum": ""
+            },
+            "require": {
+                "ext-tokenizer": "*",
+                "jakub-onderka/php-console-color": "~0.2",
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "jakub-onderka/php-code-style": "~1.0",
+                "jakub-onderka/php-parallel-lint": "~1.0",
+                "jakub-onderka/php-var-dump-check": "~0.1",
+                "phpunit/phpunit": "~4.0",
+                "squizlabs/php_codesniffer": "~1.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "JakubOnderka\\PhpConsoleHighlighter\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jakub Onderka",
+                    "email": "[email protected]",
+                    "homepage": "http://www.acci.cz/"
+                }
+            ],
+            "description": "Highlight PHP code in terminal",
+            "time": "2018-09-29T18:48:56+00:00"
+        },
         {
             "name": "justinrainbow/json-schema",
             "version": "5.2.8",
         },
         {
             "name": "laravel/browser-kit-testing",
-            "version": "v2.0.1",
+            "version": "v5.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/browser-kit-testing.git",
-                "reference": "f0bb9f200ec35f9d876ded6eacfbc60868d311b9"
+                "reference": "cb0cf22cf38fe8796842adc8b9ad550ded2a1377"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/f0bb9f200ec35f9d876ded6eacfbc60868d311b9",
-                "reference": "f0bb9f200ec35f9d876ded6eacfbc60868d311b9",
+                "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/cb0cf22cf38fe8796842adc8b9ad550ded2a1377",
+                "reference": "cb0cf22cf38fe8796842adc8b9ad550ded2a1377",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
-                "phpunit/phpunit": "~6.0",
-                "symfony/css-selector": "~3.1",
-                "symfony/dom-crawler": "~3.1"
+                "ext-dom": "*",
+                "ext-json": "*",
+                "illuminate/contracts": "~5.7.0|~5.8.0|^6.0",
+                "illuminate/database": "~5.7.0|~5.8.0|^6.0",
+                "illuminate/http": "~5.7.0|~5.8.0|^6.0",
+                "illuminate/support": "~5.7.0|~5.8.0|^6.0",
+                "mockery/mockery": "^1.0",
+                "php": ">=7.1.3",
+                "phpunit/phpunit": "^7.0|^8.0",
+                "symfony/console": "^4.2",
+                "symfony/css-selector": "^4.2",
+                "symfony/dom-crawler": "^4.2",
+                "symfony/http-foundation": "^4.2",
+                "symfony/http-kernel": "^4.2"
+            },
+            "require-dev": {
+                "laravel/framework": "~5.7.0|~5.8.0|^6.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
                     "email": "[email protected]"
                 }
             ],
-            "description": "Provides backwards compatibility for BrowserKit testing in Laravel 5.4.",
+            "description": "Provides backwards compatibility for BrowserKit testing in the latest Laravel release.",
             "keywords": [
                 "laravel",
                 "testing"
             ],
-            "time": "2017-06-21T11:44:53+00:00"
+            "time": "2019-07-30T14:57:44+00:00"
         },
         {
             "name": "maximebf/debugbar",
         },
         {
             "name": "mockery/mockery",
-            "version": "1.2.0",
+            "version": "1.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/mockery/mockery.git",
-                "reference": "100633629bf76d57430b86b7098cd6beb996a35a"
+                "reference": "4eff936d83eb809bde2c57a3cea0ee9643769031"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/mockery/mockery/zipball/100633629bf76d57430b86b7098cd6beb996a35a",
-                "reference": "100633629bf76d57430b86b7098cd6beb996a35a",
+                "url": "https://api.github.com/repos/mockery/mockery/zipball/4eff936d83eb809bde2c57a3cea0ee9643769031",
+                "reference": "4eff936d83eb809bde2c57a3cea0ee9643769031",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.6.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~5.7.10|~6.5|~7.0"
+                "phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0"
             },
             "type": "library",
             "extra": {
                 "test double",
                 "testing"
             ],
-            "time": "2018-10-02T21:52:37+00:00"
+            "time": "2019-08-07T15:01:07+00:00"
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.7.0",
+            "version": "1.9.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e"
+                "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
-                "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
+                "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0"
+                "php": "^7.1"
+            },
+            "replace": {
+                "myclabs/deep-copy": "self.version"
             },
             "require-dev": {
                 "doctrine/collections": "^1.0",
                 "doctrine/common": "^2.6",
-                "phpunit/phpunit": "^4.1"
+                "phpunit/phpunit": "^7.1"
             },
             "type": "library",
             "autoload": {
                 "object",
                 "object graph"
             ],
-            "time": "2017-10-19T19:58:43+00:00"
+            "time": "2019-08-09T12:45:53+00:00"
+        },
+        {
+            "name": "nunomaduro/collision",
+            "version": "v3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nunomaduro/collision.git",
+                "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nunomaduro/collision/zipball/af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
+                "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
+                "shasum": ""
+            },
+            "require": {
+                "filp/whoops": "^2.1.4",
+                "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*",
+                "php": "^7.1",
+                "symfony/console": "~2.8|~3.3|~4.0"
+            },
+            "require-dev": {
+                "laravel/framework": "5.8.*",
+                "nunomaduro/larastan": "^0.3.0",
+                "phpstan/phpstan": "^0.11",
+                "phpunit/phpunit": "~8.0"
+            },
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "providers": [
+                        "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "NunoMaduro\\Collision\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nuno Maduro",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "Cli error handling for console/command-line PHP applications.",
+            "keywords": [
+                "artisan",
+                "cli",
+                "command-line",
+                "console",
+                "error",
+                "handling",
+                "laravel",
+                "laravel-zero",
+                "php",
+                "symfony"
+            ],
+            "time": "2019-03-07T21:35:13+00:00"
         },
         {
             "name": "phar-io/manifest",
-            "version": "1.0.1",
+            "version": "1.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/manifest.git",
-                "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0"
+                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0",
-                "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0",
+                "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-phar": "*",
-                "phar-io/version": "^1.0.1",
+                "phar-io/version": "^2.0",
                 "php": "^5.6 || ^7.0"
             },
             "type": "library",
             "authors": [
                 {
                     "name": "Arne Blankerts",
-                    "email": "[email protected]",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "[email protected]"
                 },
                 {
                     "name": "Sebastian Heuer",
-                    "email": "[email protected]",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "[email protected]"
                 },
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "[email protected]",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "[email protected]"
                 }
             ],
             "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
-            "time": "2017-03-05T18:14:27+00:00"
+            "time": "2018-07-08T19:23:20+00:00"
         },
         {
             "name": "phar-io/version",
-            "version": "1.0.1",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phar-io/version.git",
-                "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df"
+                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df",
-                "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df",
+                "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "Library for handling version information and constraints",
-            "time": "2017-03-05T17:38:23+00:00"
+            "time": "2018-07-08T19:19:57+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
-            "version": "1.0.1",
+            "version": "2.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
-                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
+                "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
-                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
+                "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5"
+                "php": ">=7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.6"
+                "phpunit/phpunit": "~6"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "2.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "phpDocumentor\\Reflection\\": [
-                        "src"
-                    ]
+                    "phpDocumentor\\Reflection\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "reflection",
                 "static analysis"
             ],
-            "time": "2017-09-11T18:02:19+00:00"
+            "time": "2018-08-07T13:53:10+00:00"
         },
         {
             "name": "phpdocumentor/reflection-docblock",
-            "version": "4.3.0",
+            "version": "4.3.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "94fd0001232e47129dd3504189fa1c7225010d08"
+                "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08",
-                "reference": "94fd0001232e47129dd3504189fa1c7225010d08",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
+                "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.0",
-                "phpdocumentor/reflection-common": "^1.0.0",
-                "phpdocumentor/type-resolver": "^0.4.0",
+                "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
+                "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
                 "webmozart/assert": "^1.0"
             },
             "require-dev": {
-                "doctrine/instantiator": "~1.0.5",
+                "doctrine/instantiator": "^1.0.5",
                 "mockery/mockery": "^1.0",
                 "phpunit/phpunit": "^6.4"
             },
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2017-11-30T07:14:17+00:00"
+            "time": "2019-09-12T14:27:41+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",
-            "version": "0.4.0",
+            "version": "1.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
+                "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
-                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+                "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5 || ^7.0",
-                "phpdocumentor/reflection-common": "^1.0"
+                "php": "^7.1",
+                "phpdocumentor/reflection-common": "^2.0"
             },
             "require-dev": {
-                "mockery/mockery": "^0.9.4",
-                "phpunit/phpunit": "^5.2||^4.8.24"
+                "ext-tokenizer": "^7.1",
+                "mockery/mockery": "~1",
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "phpDocumentor\\Reflection\\": [
-                        "src/"
-                    ]
+                    "phpDocumentor\\Reflection\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+            "time": "2019-08-22T18:11:29+00:00"
+        },
+        {
+            "name": "phploc/phploc",
+            "version": "5.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phploc.git",
+                "reference": "5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/phploc/zipball/5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884",
+                "reference": "5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2",
+                "sebastian/finder-facade": "^1.1",
+                "sebastian/version": "^2.0",
+                "symfony/console": "^4.0"
+            },
+            "bin": [
+                "phploc"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0-dev"
                 }
             },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
-                "MIT"
+                "BSD-3-Clause"
             ],
             "authors": [
                 {
-                    "name": "Mike van Riel",
-                    "email": "[email protected]"
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
                 }
             ],
-            "time": "2017-07-14T14:27:02+00:00"
+            "description": "A tool for quickly measuring the size of a PHP project.",
+            "homepage": "https://github.com/sebastianbergmann/phploc",
+            "time": "2019-03-16T10:41:19+00:00"
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.8.0",
+            "version": "1.8.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
+                "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
-                "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
+                "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
                 "shasum": ""
             },
             "require": {
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Prophecy\\": "src/"
+                "psr-4": {
+                    "Prophecy\\": "src/Prophecy"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "spy",
                 "stub"
             ],
-            "time": "2018-08-05T17:53:17+00:00"
+            "time": "2019-06-13T12:50:23+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "5.3.2",
+            "version": "7.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "c89677919c5dd6d3b3852f230a663118762218ac"
+                "reference": "7743bbcfff2a907e9ee4a25be13d0f8ec5e73800"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
-                "reference": "c89677919c5dd6d3b3852f230a663118762218ac",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7743bbcfff2a907e9ee4a25be13d0f8ec5e73800",
+                "reference": "7743bbcfff2a907e9ee4a25be13d0f8ec5e73800",
                 "shasum": ""
             },
             "require": {
                 "ext-dom": "*",
                 "ext-xmlwriter": "*",
-                "php": "^7.0",
-                "phpunit/php-file-iterator": "^1.4.2",
+                "php": "^7.2",
+                "phpunit/php-file-iterator": "^2.0.2",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-token-stream": "^2.0.1",
+                "phpunit/php-token-stream": "^3.1.0",
                 "sebastian/code-unit-reverse-lookup": "^1.0.1",
-                "sebastian/environment": "^3.0",
+                "sebastian/environment": "^4.2.2",
                 "sebastian/version": "^2.0.1",
-                "theseer/tokenizer": "^1.1"
+                "theseer/tokenizer": "^1.1.3"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.0"
+                "phpunit/phpunit": "^8.2.2"
             },
             "suggest": {
-                "ext-xdebug": "^2.5.5"
+                "ext-xdebug": "^2.7.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "5.3.x-dev"
+                    "dev-master": "7.0-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "xunit"
             ],
-            "time": "2018-04-06T15:36:58+00:00"
+            "time": "2019-07-25T05:31:54+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
-            "version": "1.4.5",
+            "version": "2.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
+                "reference": "050bedf145a257b1ff02746c31894800e5122946"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
-                "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
+                "reference": "050bedf145a257b1ff02746c31894800e5122946",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.3"
+                "php": "^7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.4.x-dev"
+                    "dev-master": "2.0.x-dev"
                 }
             },
             "autoload": {
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
+                    "email": "sebastian@phpunit.de",
                     "role": "lead"
                 }
             ],
                 "filesystem",
                 "iterator"
             ],
-            "time": "2017-11-27T13:52:08+00:00"
+            "time": "2018-09-13T20:33:42+00:00"
         },
         {
             "name": "phpunit/php-text-template",
         },
         {
             "name": "phpunit/php-timer",
-            "version": "1.0.9",
+            "version": "2.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-timer.git",
-                "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
+                "reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
-                "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
+                "reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.3.3 || ^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "2.1-dev"
                 }
             },
             "autoload": {
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "email": "sb@sebastian-bergmann.de",
+                    "email": "sebastian@phpunit.de",
                     "role": "lead"
                 }
             ],
             "keywords": [
                 "timer"
             ],
-            "time": "2017-02-26T11:10:40+00:00"
+            "time": "2019-06-07T04:22:29+00:00"
         },
         {
             "name": "phpunit/php-token-stream",
-            "version": "2.0.2",
+            "version": "3.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "791198a2c6254db10131eecfe8c06670700904db"
+                "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db",
-                "reference": "791198a2c6254db10131eecfe8c06670700904db",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e899757bb3df5ff6e95089132f32cd59aac2220a",
+                "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a",
                 "shasum": ""
             },
             "require": {
                 "ext-tokenizer": "*",
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.2.4"
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "tokenizer"
             ],
-            "time": "2017-11-27T05:48:46+00:00"
+            "time": "2019-07-25T05:29:42+00:00"
         },
         {
             "name": "phpunit/phpunit",
-            "version": "6.5.13",
+            "version": "8.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "0973426fb012359b2f18d3bd1e90ef1172839693"
+                "reference": "302faed7059fde575cf3403a78c730c5e3a62750"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693",
-                "reference": "0973426fb012359b2f18d3bd1e90ef1172839693",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/302faed7059fde575cf3403a78c730c5e3a62750",
+                "reference": "302faed7059fde575cf3403a78c730c5e3a62750",
                 "shasum": ""
             },
             "require": {
+                "doctrine/instantiator": "^1.2.0",
                 "ext-dom": "*",
                 "ext-json": "*",
                 "ext-libxml": "*",
                 "ext-mbstring": "*",
                 "ext-xml": "*",
-                "myclabs/deep-copy": "^1.6.1",
-                "phar-io/manifest": "^1.0.1",
-                "phar-io/version": "^1.0",
-                "php": "^7.0",
-                "phpspec/prophecy": "^1.7",
-                "phpunit/php-code-coverage": "^5.3",
-                "phpunit/php-file-iterator": "^1.4.3",
+                "ext-xmlwriter": "*",
+                "myclabs/deep-copy": "^1.9.1",
+                "phar-io/manifest": "^1.0.3",
+                "phar-io/version": "^2.0.1",
+                "php": "^7.2",
+                "phpspec/prophecy": "^1.8.1",
+                "phpunit/php-code-coverage": "^7.0.7",
+                "phpunit/php-file-iterator": "^2.0.2",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-timer": "^1.0.9",
-                "phpunit/phpunit-mock-objects": "^5.0.9",
-                "sebastian/comparator": "^2.1",
-                "sebastian/diff": "^2.0",
-                "sebastian/environment": "^3.1",
-                "sebastian/exporter": "^3.1",
-                "sebastian/global-state": "^2.0",
+                "phpunit/php-timer": "^2.1.2",
+                "sebastian/comparator": "^3.0.2",
+                "sebastian/diff": "^3.0.2",
+                "sebastian/environment": "^4.2.2",
+                "sebastian/exporter": "^3.1.1",
+                "sebastian/global-state": "^3.0.0",
                 "sebastian/object-enumerator": "^3.0.3",
-                "sebastian/resource-operations": "^1.0",
+                "sebastian/resource-operations": "^2.0.1",
+                "sebastian/type": "^1.1.3",
                 "sebastian/version": "^2.0.1"
             },
-            "conflict": {
-                "phpdocumentor/reflection-docblock": "3.0.2",
-                "phpunit/dbunit": "<3.0"
-            },
             "require-dev": {
                 "ext-pdo": "*"
             },
             "suggest": {
+                "ext-soap": "*",
                 "ext-xdebug": "*",
-                "phpunit/php-invoker": "^1.1"
+                "phpunit/php-invoker": "^2.0.0"
             },
             "bin": [
                 "phpunit"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "6.5.x-dev"
+                    "dev-master": "8.3-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "xunit"
             ],
-            "time": "2018-09-08T15:10:43+00:00"
+            "time": "2019-09-14T09:12:03+00:00"
         },
         {
-            "name": "phpunit/phpunit-mock-objects",
-            "version": "5.0.10",
+            "name": "scrivo/highlight.php",
+            "version": "v9.15.10.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f"
+                "url": "https://github.com/scrivo/highlight.php.git",
+                "reference": "9ad3adb4456dc91196327498dbbce6aa1ba1239e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f",
-                "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f",
+                "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/9ad3adb4456dc91196327498dbbce6aa1ba1239e",
+                "reference": "9ad3adb4456dc91196327498dbbce6aa1ba1239e",
                 "shasum": ""
             },
             "require": {
-                "doctrine/instantiator": "^1.0.5",
-                "php": "^7.0",
-                "phpunit/php-text-template": "^1.2.1",
-                "sebastian/exporter": "^3.1"
-            },
-            "conflict": {
-                "phpunit/phpunit": "<6.0"
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "php": ">=5.4"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.5.11"
+                "phpunit/phpunit": "^4.8|^5.7",
+                "symfony/finder": "^2.8"
             },
             "suggest": {
-                "ext-soap": "*"
+                "ext-dom": "Needed to make use of the features in the utilities namespace"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "5.0.x-dev"
-                }
-            },
             "autoload": {
-                "classmap": [
-                    "src/"
+                "psr-0": {
+                    "Highlight\\": "",
+                    "HighlightUtilities\\": ""
+                },
+                "files": [
+                    "HighlightUtilities/functions.php"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
             ],
             "authors": [
                 {
-                    "name": "Sebastian Bergmann",
-                    "email": "[email protected]",
-                    "role": "lead"
+                    "name": "Geert Bergman",
+                    "role": "Project Author",
+                    "homepage": "http://www.scrivo.org/"
+                },
+                {
+                    "name": "Vladimir Jimenez",
+                    "role": "Contributor",
+                    "homepage": "https://allejo.io"
+                },
+                {
+                    "name": "Martin Folkers",
+                    "role": "Contributor",
+                    "homepage": "https://twobrain.io"
                 }
             ],
-            "description": "Mock Object library for PHPUnit",
-            "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
+            "description": "Server side syntax highlighter that supports 185 languages. It's a PHP port of highlight.js",
             "keywords": [
-                "mock",
-                "xunit"
+                "code",
+                "highlight",
+                "highlight.js",
+                "highlight.php",
+                "syntax"
             ],
-            "time": "2018-08-09T05:50:03+00:00"
+            "time": "2019-08-27T04:27:48+00:00"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
         },
         {
             "name": "sebastian/comparator",
-            "version": "2.1.3",
+            "version": "3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9"
+                "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9",
-                "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+                "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0",
-                "sebastian/diff": "^2.0 || ^3.0",
+                "php": "^7.1",
+                "sebastian/diff": "^3.0",
                 "sebastian/exporter": "^3.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.4"
+                "phpunit/phpunit": "^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.1.x-dev"
+                    "dev-master": "3.0-dev"
                 }
             },
             "autoload": {
                 "compare",
                 "equality"
             ],
-            "time": "2018-02-01T13:46:46+00:00"
+            "time": "2018-07-12T15:12:46+00:00"
         },
         {
             "name": "sebastian/diff",
-            "version": "2.0.1",
+            "version": "3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd"
+                "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
-                "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
+                "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.2"
+                "phpunit/phpunit": "^7.5 || ^8.0",
+                "symfony/process": "^2 || ^3.3 || ^4"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "3.0-dev"
                 }
             },
             "autoload": {
             "description": "Diff implementation",
             "homepage": "https://github.com/sebastianbergmann/diff",
             "keywords": [
-                "diff"
+                "diff",
+                "udiff",
+                "unidiff",
+                "unified diff"
             ],
-            "time": "2017-08-03T08:09:46+00:00"
+            "time": "2019-02-04T06:01:07+00:00"
         },
         {
             "name": "sebastian/environment",
-            "version": "3.1.0",
+            "version": "4.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5"
+                "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
-                "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
+                "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.1"
+                "phpunit/phpunit": "^7.5"
+            },
+            "suggest": {
+                "ext-posix": "*"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.1.x-dev"
+                    "dev-master": "4.2-dev"
                 }
             },
             "autoload": {
                 "environment",
                 "hhvm"
             ],
-            "time": "2017-07-01T08:51:00+00:00"
+            "time": "2019-05-05T09:05:15+00:00"
         },
         {
             "name": "sebastian/exporter",
-            "version": "3.1.0",
+            "version": "3.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "234199f4528de6d12aaa58b612e98f7d36adb937"
+                "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937",
-                "reference": "234199f4528de6d12aaa58b612e98f7d36adb937",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
+                "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
                 "shasum": ""
             },
             "require": {
                 "BSD-3-Clause"
             ],
             "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]"
+                },
                 {
                     "name": "Jeff Welch",
                     "email": "[email protected]"
                     "name": "Volker Dusch",
                     "email": "[email protected]"
                 },
-                {
-                    "name": "Bernhard Schussek",
-                    "email": "[email protected]"
-                },
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "[email protected]"
-                },
                 {
                     "name": "Adam Harvey",
                     "email": "[email protected]"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "[email protected]"
                 }
             ],
             "description": "Provides the functionality to export PHP variables for visualization",
                 "export",
                 "exporter"
             ],
-            "time": "2017-04-03T13:19:02+00:00"
+            "time": "2019-09-14T09:02:43+00:00"
+        },
+        {
+            "name": "sebastian/finder-facade",
+            "version": "1.2.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/finder-facade.git",
+                "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f",
+                "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f",
+                "shasum": ""
+            },
+            "require": {
+                "symfony/finder": "~2.3|~3.0|~4.0",
+                "theseer/fdomdocument": "~1.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
+            "homepage": "https://github.com/sebastianbergmann/finder-facade",
+            "time": "2017-11-18T17:31:49+00:00"
         },
         {
             "name": "sebastian/global-state",
-            "version": "2.0.0",
+            "version": "3.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
+                "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
-                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
+                "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0"
+                "php": "^7.2",
+                "sebastian/object-reflector": "^1.1.1",
+                "sebastian/recursion-context": "^3.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.0"
+                "ext-dom": "*",
+                "phpunit/phpunit": "^8.0"
             },
             "suggest": {
                 "ext-uopz": "*"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "3.0-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "global state"
             ],
-            "time": "2017-04-27T15:39:26+00:00"
+            "time": "2019-02-01T05:30:01+00:00"
         },
         {
             "name": "sebastian/object-enumerator",
         },
         {
             "name": "sebastian/resource-operations",
-            "version": "1.0.0",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/resource-operations.git",
-                "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
+                "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
-                "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
+                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+                "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.6.0"
+                "php": "^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "2.0-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Provides a list of PHP built-in functions that operate on resources",
             "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
-            "time": "2015-07-28T20:34:47+00:00"
+            "time": "2018-10-04T04:07:39+00:00"
+        },
+        {
+            "name": "sebastian/type",
+            "version": "1.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/type.git",
+                "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3",
+                "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "[email protected]",
+                    "role": "lead"
+                }
+            ],
+            "description": "Collection of value objects that represent the types of the PHP type system",
+            "homepage": "https://github.com/sebastianbergmann/type",
+            "time": "2019-07-02T08:10:15+00:00"
         },
         {
             "name": "sebastian/version",
         },
         {
             "name": "squizlabs/php_codesniffer",
-            "version": "3.4.0",
+            "version": "3.4.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
-                "reference": "379deb987e26c7cd103a7b387aea178baec96e48"
+                "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/379deb987e26c7cd103a7b387aea178baec96e48",
-                "reference": "379deb987e26c7cd103a7b387aea178baec96e48",
+                "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8",
+                "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
-            "homepage": "http://www.squizlabs.com/php-codesniffer",
+            "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
             "keywords": [
                 "phpcs",
                 "standards"
             ],
-            "time": "2018-12-19T23:57:18+00:00"
+            "time": "2019-04-10T23:49:02+00:00"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.1.10",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "7eede2a901a19928494194f7d1815a77b9a473a0"
+                "reference": "cc686552948d627528c0e2e759186dff67c2610e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7eede2a901a19928494194f7d1815a77b9a473a0",
-                "reference": "7eede2a901a19928494194f7d1815a77b9a473a0",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/cc686552948d627528c0e2e759186dff67c2610e",
+                "reference": "cc686552948d627528c0e2e759186dff67c2610e",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9",
+                "php": "^7.1.3",
+                "symfony/polyfill-ctype": "~1.8",
                 "symfony/polyfill-mbstring": "~1.0"
             },
+            "conflict": {
+                "masterminds/html5": "<2.6"
+            },
             "require-dev": {
-                "symfony/css-selector": "~2.8|~3.0"
+                "masterminds/html5": "^2.6",
+                "symfony/css-selector": "~3.4|~4.0"
             },
             "suggest": {
                 "symfony/css-selector": ""
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.1-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2017-01-21T17:13:55+00:00"
+            "time": "2019-08-26T08:26:39+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.3.6",
+            "version": "v4.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "427987eb4eed764c3b6e38d52a0f87989e010676"
+                "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676",
-                "reference": "427987eb4eed764c3b6e38d52a0f87989e010676",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/9abbb7ef96a51f4d7e69627bc6f63307994e4263",
+                "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5.9"
+                "php": "^7.1.3",
+                "symfony/polyfill-ctype": "~1.8"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.3-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2017-07-11T07:17:58+00:00"
+            "time": "2019-08-20T14:07:54+00:00"
+        },
+        {
+            "name": "theseer/fdomdocument",
+            "version": "1.6.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/theseer/fDOMDocument.git",
+                "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca",
+                "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "lib-libxml": "*",
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "role": "lead",
+                    "email": "[email protected]"
+                }
+            ],
+            "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
+            "homepage": "https://github.com/theseer/fDOMDocument",
+            "time": "2017-06-30T11:53:12+00:00"
         },
         {
             "name": "theseer/tokenizer",
-            "version": "1.1.0",
+            "version": "1.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/theseer/tokenizer.git",
-                "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b"
+                "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b",
-                "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b",
+                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
+                "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
                 "shasum": ""
             },
             "require": {
             "authors": [
                 {
                     "name": "Arne Blankerts",
-                    "email": "[email protected]",
-                    "role": "Developer"
+                    "role": "Developer",
+                    "email": "[email protected]"
                 }
             ],
             "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
-            "time": "2017-04-07T12:08:54+00:00"
+            "time": "2019-06-13T22:48:21+00:00"
         },
         {
             "name": "webmozart/assert",
-            "version": "1.4.0",
+            "version": "1.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
+                "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
-                "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4",
+                "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4",
                 "shasum": ""
             },
             "require": {
                 "symfony/polyfill-ctype": "^1.8"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.6",
-                "sebastian/version": "^1.0.1"
+                "phpunit/phpunit": "^4.8.36 || ^7.5.13"
             },
             "type": "library",
             "extra": {
                 "check",
                 "validate"
             ],
-            "time": "2018-12-25T11:19:39+00:00"
+            "time": "2019-08-24T08:43:50+00:00"
+        },
+        {
+            "name": "wnx/laravel-stats",
+            "version": "v2.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/stefanzweifel/laravel-stats.git",
+                "reference": "1b3c60bfbf81233973cbc2a63be4e6f83b2d6205"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/stefanzweifel/laravel-stats/zipball/1b3c60bfbf81233973cbc2a63be4e6f83b2d6205",
+                "reference": "1b3c60bfbf81233973cbc2a63be4e6f83b2d6205",
+                "shasum": ""
+            },
+            "require": {
+                "illuminate/console": "~5.8.0|^6.0",
+                "illuminate/support": "~5.8.0|^6.0",
+                "php": ">=7.2.0",
+                "phploc/phploc": "~4.0|~5.0",
+                "symfony/finder": "~3.3|~4.0"
+            },
+            "require-dev": {
+                "laravel/browser-kit-testing": "~2.0|~3.0|~4.0|~5.0",
+                "laravel/dusk": "~3.0|~4.0|~5.0",
+                "mockery/mockery": "^1.1",
+                "orchestra/testbench": "^3.8",
+                "phpunit/phpunit": "6.*|7.*|8.*"
+            },
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "providers": [
+                        "Wnx\\LaravelStats\\StatsServiceProvider"
+                    ]
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Wnx\\LaravelStats\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Stefan Zweifel",
+                    "email": "[email protected]",
+                    "homepage": "https://stefanzweifel.io",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Get insights about your Laravel Project",
+            "homepage": "https://github.com/stefanzweifel/laravel-stats",
+            "keywords": [
+                "laravel",
+                "statistics",
+                "stats",
+                "wnx"
+            ],
+            "time": "2019-09-01T14:18:49+00:00"
         }
     ],
     "aliases": [],
-    "minimum-stability": "stable",
-    "stability-flags": {
-        "laravel/socialite": 20
-    },
-    "prefer-stable": false,
+    "minimum-stability": "dev",
+    "stability-flags": [],
+    "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
-        "php": ">=7.0.5",
-        "ext-json": "*",
-        "ext-tidy": "*",
+        "php": "^7.2",
+        "ext-curl": "*",
         "ext-dom": "*",
-        "ext-xml": "*",
-        "ext-mbstring": "*",
         "ext-gd": "*",
-        "ext-curl": "*"
+        "ext-json": "*",
+        "ext-mbstring": "*",
+        "ext-tidy": "*",
+        "ext-xml": "*"
     },
     "platform-dev": [],
     "platform-overrides": {
-        "php": "7.0.5"
+        "php": "7.2.0"
     }
 }
index de6b0b276899b976afd5f78235a6b5d7ec803f75..ddf3c295d22e09023fad7e62eaefc5c198f454a1 100644 (file)
@@ -15,8 +15,8 @@ $factory->define(\BookStack\Auth\User::class, function ($faker) {
     return [
         'name' => $faker->name,
         'email' => $faker->email,
-        'password' => str_random(10),
-        'remember_token' => str_random(10),
+        'password' => Str::random(10),
+        'remember_token' => Str::random(10),
         'email_confirmed' => 1
     ];
 });
@@ -24,7 +24,7 @@ $factory->define(\BookStack\Auth\User::class, function ($faker) {
 $factory->define(\BookStack\Entities\Bookshelf::class, function ($faker) {
     return [
         'name' => $faker->sentence,
-        'slug' => str_random(10),
+        'slug' => Str::random(10),
         'description' => $faker->paragraph
     ];
 });
@@ -32,7 +32,7 @@ $factory->define(\BookStack\Entities\Bookshelf::class, function ($faker) {
 $factory->define(\BookStack\Entities\Book::class, function ($faker) {
     return [
         'name' => $faker->sentence,
-        'slug' => str_random(10),
+        'slug' => Str::random(10),
         'description' => $faker->paragraph
     ];
 });
@@ -40,7 +40,7 @@ $factory->define(\BookStack\Entities\Book::class, function ($faker) {
 $factory->define(\BookStack\Entities\Chapter::class, function ($faker) {
     return [
         'name' => $faker->sentence,
-        'slug' => str_random(10),
+        'slug' => Str::random(10),
         'description' => $faker->paragraph
     ];
 });
@@ -49,7 +49,7 @@ $factory->define(\BookStack\Entities\Page::class, function ($faker) {
     $html = '<p>' . implode('</p>', $faker->paragraphs(5)) . '</p>';
     return [
         'name' => $faker->sentence,
-        'slug' => str_random(10),
+        'slug' => Str::random(10),
         'html' => $html,
         'text' => strip_tags($html),
         'revision_count' => 1
index 988ea2100519ef6598cc0b13f69105e8b8682355..d86cb0dddd06947d478dde126f7ae8c685ec17a9 100644 (file)
@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
 class DatabaseSeeder extends Seeder
 {
     /**
-     * Run the database seeds.
+     * Seed the application's database.
      *
      * @return void
      */
index ce3cd1307d6f5279f0f1a37ee830bd5d7c661a80..deb1aa11c3b3b196add089e024404eb7c73555a8 100644 (file)
@@ -1,6 +1,14 @@
 <?php
 
+use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Auth\Role;
+use BookStack\Auth\User;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Page;
+use BookStack\Entities\SearchService;
 use Illuminate\Database\Seeder;
+use Illuminate\Support\Str;
 
 class DummyContentSeeder extends Seeder
 {
@@ -12,39 +20,39 @@ class DummyContentSeeder extends Seeder
     public function run()
     {
         // Create an editor user
-        $editorUser = factory(\BookStack\Auth\User::class)->create();
-        $editorRole = \BookStack\Auth\Role::getRole('editor');
+        $editorUser = factory(User::class)->create();
+        $editorRole = Role::getRole('editor');
         $editorUser->attachRole($editorRole);
 
         // Create a viewer user
-        $viewerUser = factory(\BookStack\Auth\User::class)->create();
-        $role = \BookStack\Auth\Role::getRole('viewer');
+        $viewerUser = factory(User::class)->create();
+        $role = Role::getRole('viewer');
         $viewerUser->attachRole($role);
 
         $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
 
         factory(\BookStack\Entities\Book::class, 5)->create($byData)
             ->each(function($book) use ($editorUser, $byData) {
-                $chapters = factory(\BookStack\Entities\Chapter::class, 3)->create($byData)
+                $chapters = factory(Chapter::class, 3)->create($byData)
                     ->each(function($chapter) use ($editorUser, $book, $byData){
-                        $pages = factory(\BookStack\Entities\Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
+                        $pages = factory(Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
                         $chapter->pages()->saveMany($pages);
                     });
-                $pages = factory(\BookStack\Entities\Page::class, 3)->make($byData);
+                $pages = factory(Page::class, 3)->make($byData);
                 $book->chapters()->saveMany($chapters);
                 $book->pages()->saveMany($pages);
             });
 
-        $largeBook = factory(\BookStack\Entities\Book::class)->create(array_merge($byData, ['name' => 'Large book' . str_random(10)]));
-        $pages = factory(\BookStack\Entities\Page::class, 200)->make($byData);
-        $chapters = factory(\BookStack\Entities\Chapter::class, 50)->make($byData);
+        $largeBook = factory(\BookStack\Entities\Book::class)->create(array_merge($byData, ['name' => 'Large book' . Str::random(10)]));
+        $pages = factory(Page::class, 200)->make($byData);
+        $chapters = factory(Chapter::class, 50)->make($byData);
         $largeBook->pages()->saveMany($pages);
         $largeBook->chapters()->saveMany($chapters);
 
-        $shelves = factory(\BookStack\Entities\Bookshelf::class, 10)->create($byData);
+        $shelves = factory(Bookshelf::class, 10)->create($byData);
         $largeBook->shelves()->attach($shelves->pluck('id'));
 
-        app(\BookStack\Auth\Permissions\PermissionService::class)->buildJointPermissions();
-        app(\BookStack\Entities\SearchService::class)->indexAllEntities();
+        app(PermissionService::class)->buildJointPermissions();
+        app(SearchService::class)->indexAllEntities();
     }
 }
index 136b6cb6a9f6465627bdab0ccab31f7fd8c24112..4db10395adf037a48aac650a19b2cc02748f3f84 100644 (file)
@@ -1,6 +1,13 @@
 <?php
 
+use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Auth\Role;
+use BookStack\Auth\User;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Page;
+use BookStack\Entities\SearchService;
 use Illuminate\Database\Seeder;
+use Illuminate\Support\Str;
 
 class LargeContentSeeder extends Seeder
 {
@@ -12,16 +19,16 @@ class LargeContentSeeder extends Seeder
     public function run()
     {
         // Create an editor user
-        $editorUser = factory(\BookStack\Auth\User::class)->create();
-        $editorRole = \BookStack\Auth\Role::getRole('editor');
+        $editorUser = factory(User::class)->create();
+        $editorRole = Role::getRole('editor');
         $editorUser->attachRole($editorRole);
 
-        $largeBook = factory(\BookStack\Entities\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
-        $pages = factory(\BookStack\Entities\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
-        $chapters = factory(\BookStack\Entities\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+        $largeBook = factory(\BookStack\Entities\Book::class)->create(['name' => 'Large book' . Str::random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+        $pages = factory(Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+        $chapters = factory(Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
         $largeBook->pages()->saveMany($pages);
         $largeBook->chapters()->saveMany($chapters);
-        app(\BookStack\Auth\Permissions\PermissionService::class)->buildJointPermissions();
-        app(\BookStack\Entities\SearchService::class)->indexAllEntities();
+        app(PermissionService::class)->buildJointPermissions();
+        app(SearchService::class)->indexAllEntities();
     }
 }
index 82ea2b6f6db642d4c2e41e16a2b91ee82359b368..8816615cf69986b75a3713d02e80b5a283221d13 100644 (file)
@@ -4,7 +4,7 @@ ENV APACHE_DOCUMENT_ROOT /app/public
 WORKDIR /app
 
 RUN apt-get update -y \
-    && apt-get install -y libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it \
+    && apt-get install -y git zip unzip libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it \
     && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \
     && docker-php-ext-install pdo pdo_mysql tidy dom xml mbstring gd ldap \
     && a2enmod rewrite \
index 3330ad5c731df771a939c70cd8629b99c5b0277f..47afc27a15d7109a2bdec012a5735641f71ef44a 100644 (file)
       }
     },
     "lodash": {
-      "version": "4.17.11",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
       "dev": true
     },
     "lodash.tail": {
       }
     },
     "mixin-deep": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
-      "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+      "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
       "dev": true,
       "requires": {
         "for-in": "^1.0.2",
       "dev": true
     },
     "set-value": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
-      "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+      "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
       "dev": true,
       "requires": {
         "extend-shallow": "^2.0.1",
       "dev": true
     },
     "union-value": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
-      "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+      "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
       "dev": true,
       "requires": {
         "arr-union": "^3.1.0",
         "get-value": "^2.0.6",
         "is-extendable": "^0.1.1",
-        "set-value": "^0.4.3"
-      },
-      "dependencies": {
-        "extend-shallow": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-          "dev": true,
-          "requires": {
-            "is-extendable": "^0.1.0"
-          }
-        },
-        "set-value": {
-          "version": "0.4.3",
-          "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
-          "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
-          "dev": true,
-          "requires": {
-            "extend-shallow": "^2.0.1",
-            "is-extendable": "^0.1.1",
-            "is-plain-object": "^2.0.1",
-            "to-object-path": "^0.3.0"
-          }
-        }
+        "set-value": "^2.0.1"
       }
     },
     "uniq": {
index 53722a71b4e193a9a1949322c3581bfacc293c08..9f83e95ff238ea30a9672077b9fef6add7a885ab 100644 (file)
@@ -7,8 +7,7 @@
          convertNoticesToExceptions="true"
          convertWarningsToExceptions="true"
          processIsolation="false"
-         stopOnFailure="false"
-         syntaxCheck="false">
+         stopOnFailure="false">
     <testsuites>
         <testsuite name="Application Test Suite">
             <directory>./tests/</directory>
         </whitelist>
     </filter>
     <php>
-        <env name="APP_ENV" value="testing"/>
-        <env name="APP_DEBUG" value="false"/>
-        <env name="APP_LANG" value="en"/>
-        <env name="APP_AUTO_LANG_PUBLIC" value="true"/>
-        <env name="CACHE_DRIVER" value="array"/>
-        <env name="SESSION_DRIVER" value="array"/>
-        <env name="QUEUE_DRIVER" value="sync"/>
-        <env name="DB_CONNECTION" value="mysql_testing"/>
-        <env name="MAIL_DRIVER" value="log"/>
-        <env name="AUTH_METHOD" value="standard"/>
-        <env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
-        <env name="AVATAR_URL" value=""/>
-        <env name="LDAP_VERSION" value="3"/>
-        <env name="STORAGE_TYPE" value="local"/>
-        <env name="STORAGE_ATTACHMENT_TYPE" value="local"/>
-        <env name="STORAGE_IMAGE_TYPE" value="local"/>
-        <env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
-        <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
-        <env name="GITHUB_AUTO_REGISTER" value=""/>
-        <env name="GITHUB_AUTO_CONFIRM_EMAIL" value=""/>
-        <env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
-        <env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
-        <env name="GOOGLE_AUTO_REGISTER" value=""/>
-        <env name="GOOGLE_AUTO_CONFIRM_EMAIL" value=""/>
-        <env name="GOOGLE_SELECT_ACCOUNT" value=""/>
-        <env name="APP_URL" value="http://bookstack.dev"/>
-        <env name="DEBUGBAR_ENABLED" value="false"/>
+        <server name="APP_ENV" value="testing"/>
+        <server name="APP_DEBUG" value="false"/>
+        <server name="APP_LANG" value="en"/>
+        <server name="APP_AUTO_LANG_PUBLIC" value="true"/>
+        <server name="CACHE_DRIVER" value="array"/>
+        <server name="SESSION_DRIVER" value="array"/>
+        <server name="QUEUE_CONNECTION" value="sync"/>
+        <server name="DB_CONNECTION" value="mysql_testing"/>
+        <server name="BCRYPT_ROUNDS" value="4"/>
+        <server name="MAIL_DRIVER" value="array"/>
+        <server name="LOG_CHANNEL" value="single"/>
+        <server name="AUTH_METHOD" value="standard"/>
+        <server name="DISABLE_EXTERNAL_SERVICES" value="true"/>
+        <server name="AVATAR_URL" value=""/>
+        <server name="LDAP_VERSION" value="3"/>
+        <server name="STORAGE_TYPE" value="local"/>
+        <server name="STORAGE_ATTACHMENT_TYPE" value="local"/>
+        <server name="STORAGE_IMAGE_TYPE" value="local"/>
+        <server name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
+        <server name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
+        <server name="GITHUB_AUTO_REGISTER" value=""/>
+        <server name="GITHUB_AUTO_CONFIRM_EMAIL" value=""/>
+        <server name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
+        <server name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
+        <server name="GOOGLE_AUTO_REGISTER" value=""/>
+        <server name="GOOGLE_AUTO_CONFIRM_EMAIL" value=""/>
+        <server name="GOOGLE_SELECT_ACCOUNT" value=""/>
+        <server name="APP_URL" value="http://bookstack.dev"/>
+        <server name="DEBUGBAR_ENABLED" value="false"/>
     </php>
 </phpunit>
diff --git a/public/web.config b/public/web.config
new file mode 100644 (file)
index 0000000..474eb68
--- /dev/null
@@ -0,0 +1,28 @@
+<!--
+    Rewrites requires Microsoft URL Rewrite Module for IIS
+    Download: https://www.microsoft.com/en-us/download/details.aspx?id=47337
+    Debug Help: https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-failed-request-tracing-to-trace-rewrite-rules
+-->
+<configuration>
+  <system.webServer>
+    <rewrite>
+      <rules>
+        <rule name="Imported Rule 1" stopProcessing="true">
+          <match url="^(.*)/$" ignoreCase="false" />
+          <conditions>
+            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
+          </conditions>
+          <action type="Redirect" redirectType="Permanent" url="/{R:1}" />
+        </rule>
+        <rule name="Imported Rule 2" stopProcessing="true">
+          <match url="^" ignoreCase="false" />
+          <conditions>
+            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
+            <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
+          </conditions>
+          <action type="Rewrite" url="index.php" />
+        </rule>
+      </rules>
+    </rewrite>
+  </system.webServer>
+</configuration>
\ No newline at end of file
index 2efc6b160e853acd4fc6386d44ffe80f4af175b7..ca90be3053af821b307e8b77bd72548f846056fa 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -2,7 +2,7 @@
 
 [![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest)
 [![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
-[![Build Status](https://travis-ci.org/BookStackApp/BookStack.svg)](https://travis-ci.org/BookStackApp/BookStack)
+[![Build Status](https://github.com/BookStackApp/BookStack/workflows/phpunit/badge.svg)](https://github.com/BookStackApp/BookStack/actions)
 [![Discord](https://img.shields.io/static/v1?label=Chat&message=Discord&color=738adb&logo=discord)](https://discord.gg/ztkBqR2)
 
 A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
@@ -174,3 +174,4 @@ These are the great open-source projects used to help build BookStack:
     * [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
 * [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
 * [Draw.io](https://github.com/jgraph/drawio)
+* [Laravel Stats](https://github.com/stefanzweifel/laravel-stats)
\ No newline at end of file
index bad0910a28f20592d5fb836ab83f4f67cf4852f1..8c822a5a79fa9dc212c792837f37b99050941b0d 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'البريد الإلكتروني',
     'password' => 'كلمة المرور',
     'password_confirm' => 'تأكيد كلمة المرور',
-    'password_hint' => 'يجب أن تكون أكثر من 5 حروف',
+    'password_hint' => 'يجب أن تكون أكثر من 7 حروف',
     'forgot_password' => 'نسيت كلمة المرور؟',
     'remember_me' => 'تذكرني',
     'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.',
index 69d6f0b97946dd4c397d98bdabf25a6b9a4320b4..27cb33880de76d4997ed620272c4d4223738dbea 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'Email',
     'password' => 'Heslo',
     'password_confirm' => 'Potvrdit heslo',
-    'password_hint' => 'Musí mít víc než 5 znaků',
+    'password_hint' => 'Musí mít víc než 7 znaků',
     'forgot_password' => 'Zapomněli jste heslo?',
     'remember_me' => 'Neodhlašovat',
     'ldap_email_hint' => 'Zadejte email, který chcete přiřadit k tomuto účtu.',
index 46d4070b8c1e47d06df0d3656c61b7ec465e8824..b367fc63b3bf0c07cb6e0da85578f363c1044112 100644 (file)
@@ -25,7 +25,7 @@ return [
     'email' => 'E-Mail',
     'password' => 'Passwort',
     'password_confirm' => 'Passwort best&auml;tigen',
-    'password_hint' => 'Mindestlänge: 5 Zeichen',
+    'password_hint' => 'Mindestlänge: 7 Zeichen',
     'forgot_password' => 'Passwort vergessen?',
     'remember_me' => 'Angemeldet bleiben',
     'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',
index 37346097f234d73e8fc9845fa9d652694a7d3f2d..6961e049b3dc2ad15d25d2a1369f4dc6988e2a6b 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'Email',
     'password' => 'Password',
     'password_confirm' => 'Confirm Password',
-    'password_hint' => 'Must be over 5 characters',
+    'password_hint' => 'Must be over 7 characters',
     'forgot_password' => 'Forgot Password?',
     'remember_me' => 'Remember Me',
     'ldap_email_hint' => 'Please enter an email to use for this account.',
index 9f7d9e3cbcaccaaa5631115f4d387785b196c18c..f41ca7868f66f2937e8ad49e7e2daee1eff3c569 100644 (file)
@@ -6,7 +6,7 @@
  */
 return [
 
-    'password' => 'Passwords must be at least six characters and match the confirmation.',
+    'password' => 'Passwords must be at least eight characters and match the confirmation.',
     'user' => "We can't find a user with that e-mail address.",
     'token' => 'This password reset token is invalid.',
     'sent' => 'We have e-mailed your password reset link!',
index 210980ac2e172244dbcb9a52016afff7fcb5ef8d..76b57a2a3b58ddb8ef41e0562c5187359cc6e542 100644 (file)
@@ -12,7 +12,7 @@ return [
     'active_url'           => 'The :attribute is not a valid URL.',
     'after'                => 'The :attribute must be a date after :date.',
     'alpha'                => 'The :attribute may only contain letters.',
-    'alpha_dash'           => 'The :attribute may only contain letters, numbers, and dashes.',
+    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',
     'alpha_num'            => 'The :attribute may only contain letters and numbers.',
     'array'                => 'The :attribute must be an array.',
     'before'               => 'The :attribute must be a date before :date.',
@@ -30,13 +30,41 @@ return [
     'digits'               => 'The :attribute must be :digits digits.',
     'digits_between'       => 'The :attribute must be between :min and :max digits.',
     'email'                => 'The :attribute must be a valid email address.',
+    'ends_with' => 'The :attribute must end with one of the following: :values',
     'filled'               => 'The :attribute field is required.',
+    'gt'                   => [
+        'numeric' => 'The :attribute must be greater than :value.',
+        'file'    => 'The :attribute must be greater than :value kilobytes.',
+        'string'  => 'The :attribute must be greater than :value characters.',
+        'array'   => 'The :attribute must have more than :value items.',
+    ],
+    'gte'                  => [
+        'numeric' => 'The :attribute must be greater than or equal :value.',
+        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',
+        'string'  => 'The :attribute must be greater than or equal :value characters.',
+        'array'   => 'The :attribute must have :value items or more.',
+    ],
     'exists'               => 'The selected :attribute is invalid.',
     'image'                => 'The :attribute must be an image.',
     'image_extension'      => 'The :attribute must have a valid & supported image extension.',
     'in'                   => 'The selected :attribute is invalid.',
     'integer'              => 'The :attribute must be an integer.',
     'ip'                   => 'The :attribute must be a valid IP address.',
+    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',
+    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',
+    'json'                 => 'The :attribute must be a valid JSON string.',
+    'lt'                   => [
+        'numeric' => 'The :attribute must be less than :value.',
+        'file'    => 'The :attribute must be less than :value kilobytes.',
+        'string'  => 'The :attribute must be less than :value characters.',
+        'array'   => 'The :attribute must have less than :value items.',
+    ],
+    'lte'                  => [
+        'numeric' => 'The :attribute must be less than or equal :value.',
+        'file'    => 'The :attribute must be less than or equal :value kilobytes.',
+        'string'  => 'The :attribute must be less than or equal :value characters.',
+        'array'   => 'The :attribute must not have more than :value items.',
+    ],
     'max'                  => [
         'numeric' => 'The :attribute may not be greater than :max.',
         'file'    => 'The :attribute may not be greater than :max kilobytes.',
@@ -52,6 +80,7 @@ return [
     ],
     'no_double_extension'  => 'The :attribute must only have a single file extension.',
     'not_in'               => 'The selected :attribute is invalid.',
+    'not_regex'            => 'The :attribute format is invalid.',
     'numeric'              => 'The :attribute must be a number.',
     'regex'                => 'The :attribute format is invalid.',
     'required'             => 'The :attribute field is required.',
index f693e7976547ae16800b77ed1e296958c948e4bb..3636507fee72669b19fcc12aa023e1eaad476e22 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'Correo electrónico',
     'password' => 'Contraseña',
     'password_confirm' => 'Confirmar Contraseña',
-    'password_hint' => 'Debe contener más de 5 caracteres',
+    'password_hint' => 'Debe contener más de 7 caracteres',
     'forgot_password' => '¿Contraseña Olvidada?',
     'remember_me' => 'Recordarme',
     'ldap_email_hint' => 'Por favor introduzca un mail para utilizar con esta cuenta.',
index e8a62562bec48d973701da8c55595753ad970765..df1a33dde6aab49cedc075859d7b4770b40bbf9a 100644 (file)
@@ -24,7 +24,7 @@ return [
     'email' => 'Correo electrónico',
     'password' => 'Contraseña',
     'password_confirm' => 'Confirmar contraseña',
-    'password_hint' => 'Debe contener al menos 5 caracteres',
+    'password_hint' => 'Debe contener al menos 7 caracteres',
     'forgot_password' => '¿Olvidó la contraseña?',
     'remember_me' => 'Recordarme',
     'ldap_email_hint' => 'Por favor introduzca un correo electrónico para utilizar con esta cuenta.',
index c9ce6a4d74d18fb0d725236f6dfa45613ca9378b..89908c8c462efd62695c1c0e3f4b9899630869d5 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'E-mail',
     'password' => 'Mot de passe',
     'password_confirm' => 'Confirmez le mot de passe',
-    'password_hint' => 'Doit faire plus de 5 caractères',
+    'password_hint' => 'Doit faire plus de 7 caractères',
     'forgot_password' => 'Mot de passe oublié ?',
     'remember_me' => 'Se souvenir de moi',
     'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte",
index a1809b0de0851694be59b43e85366734e2467edc..d96509d05e7b40159ab611debe7f49d64351436e 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'Email',
     'password' => 'Jelszó',
     'password_confirm' => 'Jelszó megerősítése',
-    'password_hint' => 'Öt karakternél hosszabbnak kell lennie',
+    'password_hint' => 'Négy karakternél hosszabbnak kell lennie',
     'forgot_password' => 'Elfelejtett jelszó?',
     'remember_me' => 'Emlékezzen rám',
     'ldap_email_hint' => 'A fiókhoz használt email cím megadása.',
index 68fee41a56496436aeadbf0a4fba8cab4dd779b0..59af1fe0d28b46ad356002ef3ce1b10c580914c9 100755 (executable)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email',
     'password' => 'Password',
     'password_confirm' => 'Conferma Password',
-    'password_hint' => 'Deve essere più di 5 caratteri',
+    'password_hint' => 'Deve essere più di 7 caratteri',
     'forgot_password' => 'Password dimenticata?',
     'remember_me' => 'Ricordami',
     'ldap_email_hint' => 'Inserisci un email per usare quest\'account.',
index 4d5aee8b38b921b3d39857c18a2c1bb5eecd8ab7..fdfac5f39eac1a77a3c48fb60870a748648a12e5 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'メールアドレス',
     'password' => 'パスワード',
     'password_confirm' => 'パスワード (確認)',
-    'password_hint' => '5文字以上である必要があります',
+    'password_hint' => '7文字以上である必要があります',
     'forgot_password' => 'パスワードをお忘れですか?',
     'remember_me' => 'ログイン情報を保存する',
     'ldap_email_hint' => 'このアカウントで使用するEメールアドレスを入力してください。',
index 671ddc654109ee6c06732f679d34a2960fa7bd87..36534f0d4fca1018243c08574fed45c6697f061c 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => '이메일',
     'password' => '비밀번호',
     'password_confirm' => '비밀번호 (확인)',
-    'password_hint' => '5자 이상이어야 합니다.',
+    'password_hint' => '7자 이상이어야 합니다.',
     'forgot_password' => '비밀번호를 잊으셨습니까?',
     'remember_me' => '자동로그인',
     'ldap_email_hint' => '이 계정에서 사용하는 이메일을 입력해 주세요.',
index 31bd330cc3d5ae28acb9663883cfce9f775d3800..30dfdd78df7064b5e38b4e796a6dbd3792148543 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email',
     'password' => 'Wachtwoord',
     'password_confirm' => 'Wachtwoord Bevestigen',
-    'password_hint' => 'Minimaal 6 tekens',
+    'password_hint' => 'Minimaal 8 tekens',
     'forgot_password' => 'Wachtwoord vergeten?',
     'remember_me' => 'Mij onthouden',
     'ldap_email_hint' => 'Geef een email op waarmee je dit account wilt gebruiken.',
index 5cec651a9aeafec5ef73231990ae2cb081feac05..40c458c618d621207163d1bcbcfd7c16cd07f1e5 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'E-mail',
     'password' => 'Hasło',
     'password_confirm' => 'Potwierdzenie hasła',
-    'password_hint' => 'Musi mieć więcej niż 5 znaków',
+    'password_hint' => 'Musi mieć więcej niż 7 znaków',
     'forgot_password' => 'Zapomniałem hasła',
     'remember_me' => 'Zapamiętaj mnie',
     'ldap_email_hint' => 'Wprowadź adres e-mail dla tego konta.',
index 20dc690af868ba79c3defc33ed1b2e006ed522c1..79f743617d625624685424a12e8d30182dc06f02 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'E-mail',
     'password' => 'Senha',
     'password_confirm' => 'Confirmar Senha',
-    'password_hint' => 'Senha deverá ser maior que 5 caracteres',
+    'password_hint' => 'Senha deverá ser maior que 7 caracteres',
     'forgot_password' => 'Esqueceu a senha?',
     'remember_me' => 'Lembrar de mim',
     'ldap_email_hint' => 'Por favor, digite um e-mail para essa conta.',
index 8bd94513c7c22d442abaf87726cb58d80ec69441..d9baec686c2df18035c5f70a7df369abadbde62c 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'Email',
     'password' => 'Пароль',
     'password_confirm' => 'Подтверждение пароля',
-    'password_hint' => 'Должен быть больше 5 символов',
+    'password_hint' => 'Должен быть больше 7 символов',
     'forgot_password' => 'Забыли пароль?',
     'remember_me' => 'Запомнить меня',
     'ldap_email_hint' => 'Введите email адрес для данной учетной записи.',
index 2fa69ac3e9b88b374495ac860faf75268828b76a..69004e87ae97b56170d5a1547f86d2526e0e0706 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email',
     'password' => 'Heslo',
     'password_confirm' => 'Potvrdiť heslo',
-    'password_hint' => 'Musí mať viac ako 5 znakov',
+    'password_hint' => 'Musí mať viac ako 7 znakov',
     'forgot_password' => 'Zabudli ste heslo?',
     'remember_me' => 'Zapamätať si ma',
     'ldap_email_hint' => 'Zadajte prosím email, ktorý sa má použiť pre tento účet.',
index 30e1a1937744fd5678b3918b37e5eb8538612108..4eb2be028f58af2c2640a484feb329ef66a394c5 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'E-post',
     'password' => 'Lösenord',
     'password_confirm' => 'Bekräfta lösenord',
-    'password_hint' => 'Måste vara fler än 5 tecken',
+    'password_hint' => 'Måste vara fler än 7 tecken',
     'forgot_password' => 'Glömt lösenord?',
     'remember_me' => 'Kom ihåg mig',
     'ldap_email_hint' => 'Vänligen ange en e-postadress att använda till kontot.',
index cd73f92db239428f4361720c61c38a7f80a56bf9..32b35a156aa52adcb5cbc1ef87bb257c984d2319 100644 (file)
@@ -21,7 +21,7 @@ return [
     'email' => 'Email',
     'password' => 'Пароль',
     'password_confirm' => 'Підтвердження пароля',
-    'password_hint' => 'Має бути більше 5 символів',
+    'password_hint' => 'Має бути більше 7 символів',
     'forgot_password' => 'Забули пароль?',
     'remember_me' => 'Запам’ятати мене',
     'ldap_email_hint' => 'Введіть email для цього облікового запису.',
index 046f2360b6c85b6b05c021adfaa287af36eaae77..f4159826c52f1aed4351e4d2004980fb7e6ecf28 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email地址',
     'password' => '密码',
     'password_confirm' => '确认密码',
-    'password_hint' => '必须超过5个字符',
+    'password_hint' => '必须超过7个字符',
     'forgot_password' => '忘记密码?',
     'remember_me' => '记住我',
     'ldap_email_hint' => '请输入用于此帐户的电子邮件。',
index f44ac8af0560dfd217f241be6c5bd5b2350c6727..9e7fcd036e92f627beb83bdc85e9be9338acecc5 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email位址',
     'password' => '密碼',
     'password_confirm' => '確認密碼',
-    'password_hint' => '必須超過5個字元',
+    'password_hint' => '必須超過7個字元',
     'forgot_password' => '忘記密碼?',
     'remember_me' => '記住我',
     'ldap_email_hint' => '請輸入用於此帳號的電子郵件。',
index 438339e92580ce19c028e19bd318ebd38cebdf6f..2f3cc1f73c9b6a4756aff363977ed44bfb3a887d 100644 (file)
@@ -7,7 +7,7 @@
         <div class="my-l">&nbsp;</div>
 
         <div class="card content-wrap auto-height">
-            <h1 class="list-heading">{{ title_case(trans('auth.log_in')) }}</h1>
+            <h1 class="list-heading">{{ Str::title(trans('auth.log_in')) }}</h1>
 
             <form action="{{ url('/login') }}" method="POST" id="login-form" class="mt-l">
                 {!! csrf_field() !!}
@@ -28,7 +28,7 @@
                     </div>
 
                     <div class="text-right">
-                        <button class="button" tabindex="1">{{ title_case(trans('auth.log_in')) }}</button>
+                        <button class="button" tabindex="1">{{ Str::title(trans('auth.log_in')) }}</button>
                     </div>
                 </div>
 
index 60ceba93c55b78cbc4feaee14a4e337cfed926f2..0e996a00d2300e1d27ecba57976260af441e6ee0 100644 (file)
@@ -6,7 +6,7 @@
         <div class="my-l">&nbsp;</div>
 
         <div class="card content-wrap auto-height">
-            <h1 class="list-heading">{{ title_case(trans('auth.sign_up')) }}</h1>
+            <h1 class="list-heading">{{ Str::title(trans('auth.sign_up')) }}</h1>
 
             <form action="{{ url("/register") }}" method="POST" class="mt-l stretch-inputs">
                 {!! csrf_field() !!}
index 4a7c45e0b81ce1b1cedcd431e94e269a19d95739..1cf91046df1b5ab2043c5e7072ed265c9a9c5a89 100644 (file)
@@ -58,7 +58,7 @@
         <h1 id="{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</h1>
 
         @if($bookChild->isA('chapter'))
-            <p>{{ $bookChild->text }}</p>
+            <p>{{ $bookChild->description }}</p>
 
             @if(count($bookChild->pages) > 0)
                 @foreach($bookChild->pages as $page)
index 871d931f17953496478a7f456e910c5ec3d6ee36..42a2757f94e0e949d536791f6dd79f4daa5ea38a 100644 (file)
@@ -4,7 +4,11 @@
         <h1 class="list-heading">{{ trans('entities.books') }}</h1>
         <div class="text-m-right my-m">
 
-            @include('partials.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'books'])
+            @include('partials.sort', ['options' => [
+                'name' => trans('common.sort_name'),
+                'created_at' => trans('common.sort_created_at'),
+                'updated_at' => trans('common.sort_updated_at'),
+            ], 'order' => $order, 'sort' => $sort, 'type' => 'books'])
 
         </div>
     </div>
index c1937ff23e5ce0311ac7247e8bb7a4e57813625f..9c599307ed0279c22de797ea8790717aa91cadf8 100644 (file)
@@ -22,7 +22,7 @@
                 <div class="card mb-xl">
                     <h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
                     <div class="px-m">
-                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'page'), 'style' => 'compact'])
+                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['page']), 'style' => 'compact'])
                     </div>
                 </div>
             </div>
@@ -30,7 +30,7 @@
                 <div class="card mb-xl">
                     <h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
                     <div class="px-m">
-                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'book'), 'style' => 'compact'])
+                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['book']), 'style' => 'compact'])
                     </div>
                 </div>
             </div>
@@ -38,7 +38,7 @@
                 <div class="card mb-xl">
                     <h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
                     <div class="px-m">
-                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'chapter'), 'style' => 'compact'])
+                        @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['chapter']), 'style' => 'compact'])
                     </div>
                 </div>
             </div>
index f27209c484f7e0def4786df72bc32397200dd5e4..3581a545b1c96f6b082b11c3a379747251f13ad1 100644 (file)
@@ -19,7 +19,7 @@
                 <a href="#" permissions-table-toggle-all class="text-small ml-m text-primary">{{ trans('common.toggle_all') }}</a>
             </th>
         </tr>
-        @foreach($roles as $role)
+        @foreach(\BookStack\Auth\Role::restrictable() as $role)
             <tr>
                 <td width="33%" class="pt-m">
                     {{ $role->display_name }}
index e39f39fc2765a27a68faed7a52f3d32ff866209d..52644113802d419ed0e8bdafaede843dc0110fe4 100644 (file)
@@ -19,7 +19,7 @@
 
         <div markdown-input class="flex flex-fill">
                         <textarea  id="markdown-editor-input"  name="markdown" rows="5"
-                                   @if($errors->has('markdown')) class="text-neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
+                                   @if($errors->has('markdown')) class="text-neg" @endif>@if(isset($model) || old('markdown')){{ old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown) }}@endif</textarea>
         </div>
 
     </div>
index f9a0f03cf3d3e2fda35e6902133e9b39a7473bf5..1a67ee76f2d0992f7eb552878359f6d151db3b51 100644 (file)
@@ -5,7 +5,7 @@
     ])
 
     <textarea id="html-editor"  name="html" rows="5" v-pre
-          @if($errors->has('html')) class="text-neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
+          @if($errors->has('html')) class="text-neg" @endif>@if(isset($model) || old('html')){{ old('html') ? old('html') : $model->html }}@endif</textarea>
 </div>
 
 @if($errors->has('html'))
index 5e11a9190b8a935748069a92288331c1e55fe927..58ccd51257e0338686e746762781d36b318b5d70 100644 (file)
@@ -2,7 +2,7 @@
     <?php $breadcrumbCount = 0; ?>
 
     {{-- Show top level books item --}}
-    @if (count($crumbs) > 0 && array_first($crumbs) instanceof  \BookStack\Entities\Book)
+    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof  \BookStack\Entities\Book)
         <a href="{{  url('/books')  }}" class="text-book icon-list-item outline-hover">
             <span>@icon('books')</span>
             <span>{{ trans('entities.books') }}</span>
@@ -11,7 +11,7 @@
     @endif
 
     {{-- Show top level shelves item --}}
-    @if (count($crumbs) > 0 && array_first($crumbs) instanceof  \BookStack\Entities\Bookshelf)
+    @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof  \BookStack\Entities\Bookshelf)
         <a href="{{  url('/shelves')  }}" class="text-bookshelf icon-list-item outline-hover">
             <span>@icon('bookshelf')</span>
             <span>{{ trans('entities.shelves') }}</span>
index 5125e7e194ce250f53bbc6a4a0fd61b5a5837132..19c5bbecd69ae95c70bd782703cfe377ddd32b30 100644 (file)
     <div class="form-group">
         <label for="books">{{ trans('entities.shelves_books') }}</label>
         <input type="hidden" id="books-input" name="books"
-               value="{{ isset($shelf) ? $shelf->books->implode('id', ',') : '' }}">
+               value="{{ isset($shelf) ? $shelf->visibleBooks->implode('id', ',') : '' }}">
         <div class="scroll-box" shelf-sort-assigned-books data-instruction="{{ trans('entities.shelves_drag_books') }}">
-            @if (isset($shelfBooks) && count($shelfBooks) > 0)
-                @foreach ($shelfBooks as $book)
+            @if (count($shelf->visibleBooks ?? []) > 0)
+                @foreach ($shelf->visibleBooks as $book)
                     <div data-id="{{ $book->id }}" class="scroll-box-item">
                         <a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
                     </div>
index 6bfc525a5dc458852ce750984891862857f9b010..2212e1c1e32ae3014b3f839f802d1dc534542c3a 100644 (file)
@@ -12,9 +12,9 @@
         <h1 class="break-text">{{$shelf->name}}</h1>
         <div class="book-content">
             <p class="text-muted">{!! nl2br(e($shelf->description)) !!}</p>
-            @if(count($books) > 0)
+            @if(count($shelf->visibleBooks) > 0)
                 <div class="entity-list">
-                    @foreach($books as $book)
+                    @foreach($shelf->visibleBooks as $book)
                         @include('books.list-item', ['book' => $book])
                     @endforeach
                 </div>
index d9fdc7455586ca00feb76abd964ffc8d73a73184..5dee447a4c7abd56d67a495dcdd68336ac6b9026 100644 (file)
@@ -9,9 +9,7 @@ Route::group(['middleware' => 'auth'], function () {
     Route::get('/uploads/images/{path}', 'Images\ImageController@showImage')
         ->where('path', '.*$');
 
-    Route::group(['prefix' => 'pages'], function() {
-        Route::get('/recently-updated', 'PageController@showRecentlyUpdated');
-    });
+    Route::get('/pages/recently-updated', 'PageController@showRecentlyUpdated');
 
     // Shelves
     Route::get('/create-shelf', 'BookshelfController@create');
@@ -40,16 +38,16 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/{slug}/edit', 'BookController@edit');
         Route::put('/{slug}', 'BookController@update');
         Route::delete('/{id}', 'BookController@destroy');
-        Route::get('/{slug}/sort-item', 'BookController@getSortItem');
+        Route::get('/{slug}/sort-item', 'BookSortController@showItem');
         Route::get('/{slug}', 'BookController@show');
         Route::get('/{bookSlug}/permissions', 'BookController@showPermissions');
         Route::put('/{bookSlug}/permissions', 'BookController@permissions');
         Route::get('/{slug}/delete', 'BookController@showDelete');
-        Route::get('/{bookSlug}/sort', 'BookController@sort');
-        Route::put('/{bookSlug}/sort', 'BookController@saveSort');
-        Route::get('/{bookSlug}/export/html', 'BookController@exportHtml');
-        Route::get('/{bookSlug}/export/pdf', 'BookController@exportPdf');
-        Route::get('/{bookSlug}/export/plaintext', 'BookController@exportPlainText');
+        Route::get('/{bookSlug}/sort', 'BookSortController@show');
+        Route::put('/{bookSlug}/sort', 'BookSortController@update');
+        Route::get('/{bookSlug}/export/html', 'BookExportController@html');
+        Route::get('/{bookSlug}/export/pdf', 'BookExportController@pdf');
+        Route::get('/{bookSlug}/export/plaintext', 'BookExportController@plainText');
 
         // Pages
         Route::get('/{bookSlug}/create-page', 'PageController@create');
@@ -57,9 +55,9 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
         Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store');
         Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
-        Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
-        Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
-        Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
+        Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageExportController@pdf');
+        Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageExportController@html');
+        Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageExportController@plainText');
         Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
         Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove');
         Route::put('/{bookSlug}/page/{pageSlug}/move', 'PageController@move');
@@ -74,11 +72,11 @@ Route::group(['middleware' => 'auth'], function () {
         Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
 
         // Revisions
-        Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
-        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
-        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageController@showRevisionChanges');
-        Route::put('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
-        Route::delete('/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', 'PageController@destroyRevision');
+        Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageRevisionController@index');
+        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageRevisionController@show');
+        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageRevisionController@changes');
+        Route::put('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageRevisionController@restore');
+        Route::delete('/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', 'PageRevisionController@destroy');
 
         // Chapters
         Route::get('/{bookSlug}/chapter/{chapterSlug}/create-page', 'PageController@create');
@@ -91,9 +89,9 @@ Route::group(['middleware' => 'auth'], function () {
         Route::put('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@move');
         Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
         Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showPermissions');
-        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/pdf', 'ChapterController@exportPdf');
-        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/html', 'ChapterController@exportHtml');
-        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/plaintext', 'ChapterController@exportPlainText');
+        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/pdf', 'ChapterExportController@pdf');
+        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/html', 'ChapterExportController@html');
+        Route::get('/{bookSlug}/chapter/{chapterSlug}/export/plaintext', 'ChapterExportController@plainText');
         Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@permissions');
         Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
         Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
index c96a04f008ee21e260b28f7701595ed59e2839e3..869804c2ad0b113b8edfc31984e4e94c5dedcf0d 100755 (executable)
@@ -1,2 +1,3 @@
 *
+!data/
 !.gitignore
\ No newline at end of file
index 42b44c1525631cac003445c7012bea712c21c34c..eb83faded44edc67d721e595d1a11d3861e12255 100644 (file)
@@ -81,7 +81,7 @@ class AuthTest extends BrowserKitTest
             ->press('Create Account')
             ->see('The name must be at least 2 characters.')
             ->see('The email must be a valid email address.')
-            ->see('The password must be at least 6 characters.')
+            ->see('The password must be at least 8 characters.')
             ->seePageIs('/register');
     }
 
index 5923ef377700734f931b6a56098bfe14a18e255d..fe28698dfac6b79013020c43fb82a93f1bdcb8fe 100644 (file)
@@ -15,7 +15,7 @@ class LdapTest extends BrowserKitTest
     protected $mockUser;
     protected $resourceId = 'resource-test';
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
index b8ca81174c3911663c08e703266640ebd1abb212..526c0e199a184cf7ee0a7f74c49fcea0bf42cbba 100644 (file)
@@ -153,7 +153,7 @@ class SocialAuthTest extends TestCase
         config()->set('services.google.select_account', 'true');
 
         $resp = $this->get('/login/service/google');
-        $this->assertContains('prompt=select_account', $resp->headers->get('Location'));
+        $this->assertStringContainsString('prompt=select_account', $resp->headers->get('Location'));
     }
 
 }
index 3312626909541455a06ac65d8475a92ed6191484..d200134a5548707b52966d445d13dff1ef096316 100644 (file)
@@ -6,6 +6,7 @@ use BookStack\Auth\User;
 use BookStack\Notifications\UserInvite;
 use Carbon\Carbon;
 use DB;
+use Illuminate\Support\Str;
 use Notification;
 
 class UserInviteTest extends TestCase
@@ -68,11 +69,13 @@ class UserInviteTest extends TestCase
         $inviteService->sendInvitation($user);
         $token = DB::table('user_invites')->where('user_id', '=', $user->id)->first()->token;
 
+        $this->get('/register/invite/' . $token);
         $shortPassword = $this->followingRedirects()->post('/register/invite/' . $token, [
-            'password' => 'mypas',
+            'password' => 'mypassw',
         ]);
-        $shortPassword->assertSee('The password must be at least 6 characters.');
+        $shortPassword->assertSee('The password must be at least 8 characters.');
 
+        $this->get('/register/invite/' . $token);
         $noPassword = $this->followingRedirects()->post('/register/invite/' . $token, [
             'password' => '',
         ]);
@@ -85,10 +88,10 @@ class UserInviteTest extends TestCase
 
     public function test_non_existent_invite_token_redirects_to_home()
     {
-        $setPasswordPageResp = $this->get('/register/invite/' . str_random(12));
+        $setPasswordPageResp = $this->get('/register/invite/' . Str::random(12));
         $setPasswordPageResp->assertRedirect('/');
 
-        $setPasswordResp = $this->post('/register/invite/' . str_random(12), ['password' => 'Password Test']);
+        $setPasswordResp = $this->post('/register/invite/' . Str::random(12), ['password' => 'Password Test']);
         $setPasswordResp->assertRedirect('/');
     }
 
index ab0d9d898b31db9a9f167fc7a4b9131ffe722a1f..b81afe31106025352a3d5c50a23b2162b1ae6d5f 100644 (file)
@@ -21,7 +21,7 @@ abstract class BrowserKitTest extends TestCase
      */
     protected $baseUrl = 'http://localhost';
 
-    public function tearDown()
+    public function tearDown() : void
     {
         \DB::disconnect();
         parent::tearDown();
index a884809696d32cd4e4126f7c7c68121738ee905f..4aef0ed2658861b2fe8a6f326c12ea131e3888ee 100644 (file)
@@ -2,7 +2,6 @@
 
 use BookStack\Auth\Permissions\JointPermission;
 use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
 use BookStack\Auth\User;
 use BookStack\Entities\Repos\PageRepo;
 
@@ -56,7 +55,7 @@ class CommandsTest extends TestCase
         $this->asEditor();
         $pageRepo = app(PageRepo::class);
         $page = Page::first();
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+        $pageRepo->update($page, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
         $pageRepo->updatePageDraft($page, ['name' => 'updated page', 'html' => '<p>new content in draft</p>', 'summary' => 'page revision testing']);
 
         $this->assertDatabaseHas('page_revisions', [
index 158fb5ca184e2075e0cef15339bf71075576dc12..5c7673847140eb71d284e14d94d92921f6b706c6 100644 (file)
@@ -4,6 +4,7 @@ use BookStack\Auth\Role;
 use BookStack\Auth\User;
 use BookStack\Entities\Book;
 use BookStack\Entities\Bookshelf;
+use Illuminate\Support\Str;
 
 class BookShelfTest extends TestCase
 {
@@ -55,8 +56,8 @@ class BookShelfTest extends TestCase
     {
         $booksToInclude = Book::take(2)->get();
         $shelfInfo = [
-            'name' => 'My test book' . str_random(4),
-            'description' => 'Test book description ' . str_random(10)
+            'name' => 'My test book' . Str::random(4),
+            'description' => 'Test book description ' . Str::random(10)
         ];
         $resp = $this->asEditor()->post('/shelves', array_merge($shelfInfo, [
             'books' => $booksToInclude->implode('id', ','),
@@ -120,8 +121,8 @@ class BookShelfTest extends TestCase
 
         $booksToInclude = Book::take(2)->get();
         $shelfInfo = [
-            'name' => 'My test book' . str_random(4),
-            'description' => 'Test book description ' . str_random(10)
+            'name' => 'My test book' . Str::random(4),
+            'description' => 'Test book description ' . Str::random(10)
         ];
 
         $resp = $this->asEditor()->put($shelf->getUrl(), array_merge($shelfInfo, [
index 2683f57cb601767243b745edd31b2caf1ea4e457..967e550a735e6294faf970bcfc1a605b8a57c2f7 100644 (file)
@@ -3,7 +3,7 @@
 class CommentSettingTest extends BrowserKitTest {
   protected $page;
 
-  public function setUp() {
+  public function setUp(): void {
       parent::setUp();
       $this->page = \BookStack\Entities\Page::first();
   }
index a3fb1cfe11833591a5d1282aeda43fdce23f9eb9..b506da2aa62387126f3b5c69ffb678b0b6548fe6 100644 (file)
@@ -3,7 +3,6 @@
 use BookStack\Entities\Book;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
 use BookStack\Auth\UserRepo;
 use BookStack\Entities\Repos\PageRepo;
 use Carbon\Carbon;
@@ -192,7 +191,7 @@ class EntityTest extends BrowserKitTest
         $entities = $this->createEntityChainBelongingToUser($creator, $updater);
         $this->actingAs($creator);
         app(UserRepo::class)->destroy($creator);
-        app(PageRepo::class)->savePageRevision($entities['page']);
+        app(PageRepo::class)->update($entities['page'], ['html' => '<p>hello!</p>>']);
 
         $this->checkEntitiesViewable($entities);
     }
@@ -205,7 +204,7 @@ class EntityTest extends BrowserKitTest
         $entities = $this->createEntityChainBelongingToUser($creator, $updater);
         $this->actingAs($updater);
         app(UserRepo::class)->destroy($updater);
-        app(PageRepo::class)->savePageRevision($entities['page']);
+        app(PageRepo::class)->update($entities['page'], ['html' => '<p>Hello there!</p>']);
 
         $this->checkEntitiesViewable($entities);
     }
@@ -273,8 +272,7 @@ class EntityTest extends BrowserKitTest
 
     public function test_slug_multi_byte_lower_casing()
     {
-        $entityRepo = app(EntityRepo::class);
-        $book = $entityRepo->createFromInput('book', [
+        $book = $this->newBook([
             'name' => 'КНИГА'
         ]);
 
@@ -284,8 +282,7 @@ class EntityTest extends BrowserKitTest
 
     public function test_slug_format()
     {
-        $entityRepo = app(EntityRepo::class);
-        $book = $entityRepo->createFromInput('book', [
+        $book = $this->newBook([
             'name' => 'PartA / PartB / PartC'
         ]);
 
index e3a74f64d1d2c306df662071b369beadfd9bfbac..9a2d32028e4b26887c7d231b68df2b1887668e3a 100644 (file)
@@ -4,6 +4,7 @@
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Page;
 use BookStack\Uploads\HttpFetcher;
+use Illuminate\Support\Str;
 
 class ExportTest extends TestCase
 {
@@ -79,7 +80,7 @@ class ExportTest extends TestCase
 
     public function test_book_html_export_shows_chapter_descriptions()
     {
-        $chapterDesc = 'My custom test chapter description ' . str_random(12);
+        $chapterDesc = 'My custom test chapter description ' . Str::random(12);
         $chapter = Chapter::query()->first();
         $chapter->description = $chapterDesc;
         $chapter->save();
index c481e444f59cf563f25791bd3725bac74ee6df1c..5d3af4f6e26705b8fed3e617a78d9104fe3fb99d 100644 (file)
@@ -4,7 +4,7 @@ class MarkdownTest extends BrowserKitTest
 {
     protected $page;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->page = \BookStack\Entities\Page::first();
index e812d5bfe7fece4d5bebda333b353a1668a75887..8a78c8ac019fd08de3cc37d50e7df50954c83f6e 100644 (file)
@@ -1,8 +1,7 @@
 <?php namespace Tests;
 
+use BookStack\Entities\Managers\PageContent;
 use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\Repos\PageRepo;
 
 class PageContentTest extends TestCase
 {
@@ -50,7 +49,7 @@ class PageContentTest extends TestCase
         $resp->assertStatus(302);
 
         $page = Page::find($page->id);
-        $this->assertContains($includeTag, $page->html);
+        $this->assertStringContainsString($includeTag, $page->html);
         $this->assertEquals('', $page->text);
     }
 
@@ -242,4 +241,66 @@ class PageContentTest extends TestCase
         $updatedPage = Page::where('id', '=', $page->id)->first();
         $this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1);
     }
+
+    public function test_get_page_nav_sets_correct_properties()
+    {
+        $content = '<h1 id="testa">Hello</h1><h2 id="testb">There</h2><h3 id="testc">Donkey</h3>';
+        $pageContent = new PageContent(new Page(['html' => $content]));
+        $navMap = $pageContent->getNavigation($content);
+
+        $this->assertCount(3, $navMap);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h1',
+            'link' => '#testa',
+            'text' => 'Hello',
+            'level' => 1,
+        ], $navMap[0]);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h2',
+            'link' => '#testb',
+            'text' => 'There',
+            'level' => 2,
+        ], $navMap[1]);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h3',
+            'link' => '#testc',
+            'text' => 'Donkey',
+            'level' => 3,
+        ], $navMap[2]);
+    }
+
+    public function test_get_page_nav_does_not_show_empty_titles()
+    {
+        $content = '<h1 id="testa">Hello</h1><h2 id="testb">&nbsp;</h2><h3 id="testc"></h3>';
+        $pageContent = new PageContent(new Page(['html' => $content]));
+        $navMap = $pageContent->getNavigation($content);
+
+        $this->assertCount(1, $navMap);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h1',
+            'link' => '#testa',
+            'text' => 'Hello'
+        ], $navMap[0]);
+    }
+
+    public function test_get_page_nav_shifts_headers_if_only_smaller_ones_are_used()
+    {
+        $content = '<h4 id="testa">Hello</h4><h5 id="testb">There</h5><h6 id="testc">Donkey</h6>';
+        $pageContent = new PageContent(new Page(['html' => $content]));
+        $navMap = $pageContent->getNavigation($content);
+
+        $this->assertCount(3, $navMap);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h4',
+            'level' => 1,
+        ], $navMap[0]);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h5',
+            'level' => 2,
+        ], $navMap[1]);
+        $this->assertArrayMapIncludes([
+            'nodeName' => 'h6',
+            'level' => 3,
+        ], $navMap[2]);
+    }
 }
index f15651f39b672824520bbdb096962cc2fe81b256..e83f78a10a8a8f2d566ece9586a115dd24d4c323 100644 (file)
@@ -1,14 +1,17 @@
 <?php namespace Tests;
 
-
 use BookStack\Entities\Repos\PageRepo;
 
 class PageDraftTest extends BrowserKitTest
 {
     protected $page;
+
+    /**
+     * @var PageRepo
+     */
     protected $pageRepo;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->page = \BookStack\Entities\Page::first();
@@ -18,25 +21,26 @@ class PageDraftTest extends BrowserKitTest
     public function test_draft_content_shows_if_available()
     {
         $addedContent = '<p>test message content</p>';
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+        $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->dontSeeInField('html', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
         $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+        $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->seeInField('html', $newContent);
     }
 
     public function test_draft_not_visible_by_others()
     {
         $addedContent = '<p>test message content</p>';
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+        $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->dontSeeInField('html', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
         $newUser = $this->getEditor();
         $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
-        $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit')
+
+        $this->actingAs($newUser)->visit($this->page->getUrl('/edit'))
             ->dontSeeInField('html', $newContent);
     }
 
@@ -44,7 +48,7 @@ class PageDraftTest extends BrowserKitTest
     {
         $this->asAdmin();
         $this->pageRepo->updatePageDraft($this->page, ['html' => 'test content']);
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+        $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->see('You are currently editing a draft');
     }
 
@@ -52,7 +56,7 @@ class PageDraftTest extends BrowserKitTest
     {
         $nonEditedPage = \BookStack\Entities\Page::take(10)->get()->last();
         $addedContent = '<p>test message content</p>';
-        $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+        $this->asAdmin()->visit($this->page->getUrl('/edit'))
             ->dontSeeInField('html', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
@@ -60,7 +64,7 @@ class PageDraftTest extends BrowserKitTest
         $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
 
         $this->actingAs($newUser)
-            ->visit($this->page->getUrl() . '/edit')
+            ->visit($this->page->getUrl('/edit'))
             ->see('Admin has started editing this page');
             $this->flushSession();
         $this->visit($nonEditedPage->getUrl() . '/edit')
@@ -84,11 +88,11 @@ class PageDraftTest extends BrowserKitTest
         $newUser = $this->getEditor();
 
         $this->actingAs($newUser)->visit('/')
-            ->visit($book->getUrl() . '/create-page')
-            ->visit($chapter->getUrl() . '/create-page')
+            ->visit($book->getUrl('/create-page'))
+            ->visit($chapter->getUrl('/create-page'))
             ->visit($book->getUrl())
             ->seeInElement('.book-contents', 'New Page');
-        
+
         $this->asAdmin()
             ->visit($book->getUrl())
             ->dontSeeInElement('.book-contents', 'New Page')
index 140f67fe81abb339614978e9fd913531ac5b47e4..38193ec1a6b7bd09cadbe04e6a8bcb3ea8765f98 100644 (file)
@@ -12,7 +12,7 @@ class PageRevisionTest extends TestCase
 
         $pageRepo = app(PageRepo::class);
         $page = Page::first();
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+        $pageRepo->update($page, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
         $pageRevision = $page->revisions->last();
 
         $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id);
@@ -30,8 +30,8 @@ class PageRevisionTest extends TestCase
 
         $pageRepo = app(PageRepo::class);
         $page = Page::first();
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+        $pageRepo->update($page, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
+        $pageRepo->update($page, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
         $page =  Page::find($page->id);
 
 
@@ -98,7 +98,7 @@ class PageRevisionTest extends TestCase
         $beforeRevisionCount = $page->revisions->count();
         $currentRevision = $page->getCurrentRevision();
         $resp = $this->asEditor()->delete($currentRevision->getUrl('/delete/'));
-        $resp->assertStatus(400);
+        $resp->assertRedirect($page->getUrl('/revisions'));
 
         $page = Page::find($page->id);
         $afterRevisionCount = $page->revisions->count();
index a3c20e84c5b517bede1e3b3488516a299c1e853a..3c83d626aea73632dc93a5d08274a1a4d6e420da 100644 (file)
@@ -1,6 +1,5 @@
 <?php namespace Tests;
 
-use BookStack\Auth\Role;
 use BookStack\Entities\Book;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Page;
@@ -10,7 +9,7 @@ class SortTest extends TestCase
 {
     protected $book;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->book = Book::first();
@@ -20,7 +19,7 @@ class SortTest extends TestCase
     {
         $this->asAdmin();
         $pageRepo = app(PageRepo::class);
-        $draft = $pageRepo->getDraftPage($this->book);
+        $draft = $pageRepo->getNewDraftPage($this->book);
 
         $resp = $this->get($this->book->getUrl());
         $resp->assertSee($draft->name);
@@ -214,7 +213,6 @@ class SortTest extends TestCase
             'entity_selection' => 'book:' . $newBook->id,
             'name' => 'My copied test page'
         ]);
-
         $pageCopy = Page::where('name', '=', 'My copied test page')->first();
 
         $movePageResp->assertRedirect($pageCopy->getUrl());
index 70b8c960b73980544b74356d2666c0cd89fee067..13876410a0cddccaf4e3d04783e5735ccfd78697 100644 (file)
@@ -3,6 +3,7 @@
 use BookStack\Entities\Book;
 use BookStack\Entities\Chapter;
 use BookStack\Actions\Tag;
+use BookStack\Entities\Entity;
 use BookStack\Entities\Page;
 use BookStack\Auth\Permissions\PermissionService;
 
@@ -14,9 +15,9 @@ class TagTest extends BrowserKitTest
     /**
      * Get an instance of a page that has many tags.
      * @param \BookStack\Actions\Tag[]|bool $tags
-     * @return mixed
+     * @return Entity
      */
-    protected function getEntityWithTags($class, $tags = false)
+    protected function getEntityWithTags($class, $tags = false): Entity
     {
         $entity = $class::first();
 
@@ -122,7 +123,7 @@ class TagTest extends BrowserKitTest
         // Set restricted permission the page
         $page->restricted = true;
         $page->save();
-        $permissionService->buildJointPermissionsForEntity($page);
+        $page->rebuildPermissions();
 
         $this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']);
         $this->asEditor()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals([]);
index d9b8655ee1c67f83d827808fee236aacdebb40f4..c8bc4445117a5819fe3c7561f043bd742b5d7182 100644 (file)
@@ -8,7 +8,7 @@ class LanguageTest extends TestCase
     /**
      * LanguageTest constructor.
      */
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->langs = array_diff(scandir(resource_path('lang')), ['..', '.', 'check.php', 'format.php']);
index a7f681a37d1597d72a254d4e7b549e8c2e310202..d899c6396041a9d8a46d695efff0599ce0159a83 100644 (file)
@@ -5,14 +5,13 @@ use BookStack\Entities\Bookshelf;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Entity;
 use BookStack\Auth\User;
-use BookStack\Entities\Repos\EntityRepo;
 use BookStack\Entities\Page;
 
 class RestrictionsTest extends BrowserKitTest
 {
 
     /**
-     * @var \BookStack\Auth\User
+     * @var User
      */
     protected $user;
 
@@ -21,7 +20,7 @@ class RestrictionsTest extends BrowserKitTest
      */
     protected $viewer;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->user = $this->getEditor();
@@ -327,7 +326,7 @@ class RestrictionsTest extends BrowserKitTest
 
     public function test_page_view_restriction()
     {
-        $page = \BookStack\Entities\Page::first();
+        $page = Page::first();
 
         $pageUrl = $page->getUrl();
         $this->actingAs($this->user)
@@ -367,7 +366,7 @@ class RestrictionsTest extends BrowserKitTest
 
     public function test_page_delete_restriction()
     {
-        $page = \BookStack\Entities\Page::first();
+        $page = Page::first();
 
         $pageUrl = $page->getUrl();
         $this->actingAs($this->user)
@@ -438,7 +437,7 @@ class RestrictionsTest extends BrowserKitTest
 
     public function test_page_restriction_form()
     {
-        $page = \BookStack\Entities\Page::first();
+        $page = Page::first();
         $this->asAdmin()->visit($page->getUrl() . '/permissions')
             ->see('Page Permissions')
             ->check('restricted')
@@ -665,10 +664,8 @@ class RestrictionsTest extends BrowserKitTest
         $this->setEntityRestrictions($firstBook, ['view', 'update']);
         $this->setEntityRestrictions($secondBook, ['view']);
 
-        $firstBookChapter = $this->app[EntityRepo::class]->createFromInput('chapter',
-                ['name' => 'first book chapter'], $firstBook);
-        $secondBookChapter = $this->app[EntityRepo::class]->createFromInput('chapter',
-                ['name' => 'second book chapter'], $secondBook);
+        $firstBookChapter = $this->newChapter(['name' => 'first book chapter'], $firstBook);
+        $secondBookChapter = $this->newChapter(['name' => 'second book chapter'], $secondBook);
 
         // Create request data
         $reqData = [
index a1f19364352c5f1ed74fa86f427ff8d0d95d4aac..371cffc0f3bd1be95a263c2bffd4c5f4277bb8f2 100644 (file)
@@ -11,7 +11,7 @@ class RolesTest extends BrowserKitTest
 {
     protected $user;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->user = $this->getViewer();
index 1d87e942aaf2167e10dbaed48f0a20f887511b94..3433f3b832f86d61411729ea23983905596d57d0 100644 (file)
@@ -1,17 +1,23 @@
 <?php namespace Tests;
 
+use BookStack\Auth\User;
 use BookStack\Entities\Book;
 use BookStack\Entities\Bookshelf;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Entity;
 use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Repos\BookshelfRepo;
+use BookStack\Entities\Repos\ChapterRepo;
 use BookStack\Auth\Permissions\PermissionsRepo;
 use BookStack\Auth\Role;
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Settings\SettingService;
 use BookStack\Uploads\HttpFetcher;
+use Illuminate\Support\Env;
+use Mockery;
+use Throwable;
 
 trait SharedTestHelpers
 {
@@ -69,7 +75,7 @@ trait SharedTestHelpers
      */
     protected function getViewer($attributes = [])
     {
-        $user = \BookStack\Auth\Role::getRole('viewer')->users()->first();
+        $user = Role::getRole('viewer')->users()->first();
         if (!empty($attributes)) $user->forceFill($attributes)->save();
         return $user;
     }
@@ -77,20 +83,21 @@ trait SharedTestHelpers
     /**
      * Regenerate the permission for an entity.
      * @param Entity $entity
+     * @throws Throwable
      */
     protected function regenEntityPermissions(Entity $entity)
     {
-        app(PermissionService::class)->buildJointPermissionsForEntity($entity);
+        $entity->rebuildPermissions();
         $entity->load('jointPermissions');
     }
 
     /**
      * Create and return a new bookshelf.
      * @param array $input
-     * @return \BookStack\Entities\Bookshelf
+     * @return Bookshelf
      */
     public function newShelf($input = ['name' => 'test shelf', 'description' => 'My new test shelf']) {
-        return app(EntityRepo::class)->createFromInput('bookshelf', $input, false);
+        return app(BookshelfRepo::class)->create($input, []);
     }
 
     /**
@@ -99,29 +106,30 @@ trait SharedTestHelpers
      * @return Book
      */
     public function newBook($input = ['name' => 'test book', 'description' => 'My new test book']) {
-        return app(EntityRepo::class)->createFromInput('book', $input, false);
+        return app(BookRepo::class)->create($input);
     }
 
     /**
      * Create and return a new test chapter
      * @param array $input
      * @param Book $book
-     * @return \BookStack\Entities\Chapter
+     * @return Chapter
      */
     public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
-        return app(EntityRepo::class)->createFromInput('chapter', $input, $book);
+        return app(ChapterRepo::class)->create($input, $book);
     }
 
     /**
      * Create and return a new test page
      * @param array $input
      * @return Page
+     * @throws Throwable
      */
     public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) {
         $book = Book::first();
         $pageRepo = app(PageRepo::class);
-        $draftPage = $pageRepo->getDraftPage($book);
-        return $pageRepo->publishPageDraft($draftPage, $input);
+        $draftPage = $pageRepo->getNewDraftPage($book);
+        return $pageRepo->publishDraft($draftPage, $input);
     }
 
     /**
@@ -166,10 +174,10 @@ trait SharedTestHelpers
 
     /**
      * Give the given user some permissions.
-     * @param \BookStack\Auth\User $user
+     * @param User $user
      * @param array $permissions
      */
-    protected function giveUserPermissions(\BookStack\Auth\User $user, $permissions = [])
+    protected function giveUserPermissions(User $user, $permissions = [])
     {
         $newRole = $this->createNewRole($permissions);
         $user->attachRole($newRole);
@@ -197,11 +205,61 @@ trait SharedTestHelpers
      */
     protected function mockHttpFetch($returnData, int $times = 1)
     {
-        $mockHttp = \Mockery::mock(HttpFetcher::class);
+        $mockHttp = Mockery::mock(HttpFetcher::class);
         $this->app[HttpFetcher::class] = $mockHttp;
         $mockHttp->shouldReceive('fetch')
             ->times($times)
             ->andReturn($returnData);
     }
 
+    /**
+     * Run a set test with the given env variable.
+     * Remembers the original and resets the value after test.
+     * @param string $name
+     * @param $value
+     * @param callable $callback
+     */
+    protected function runWithEnv(string $name, $value, callable $callback)
+    {
+        Env::disablePutenv();
+        $originalVal = $_SERVER[$name] ?? null;
+
+        if (is_null($value)) {
+            unset($_SERVER[$name]);
+        } else {
+            $_SERVER[$name] = $value;
+        }
+
+        $this->refreshApplication();
+        $callback();
+
+        if (is_null($originalVal)) {
+            unset($_SERVER[$name]);
+        } else {
+            $_SERVER[$name] = $originalVal;
+        }
+    }
+
+    /**
+     * Check the keys and properties in the given map to include
+     * exist, albeit not exclusively, within the map to check.
+     * @param array $mapToInclude
+     * @param array $mapToCheck
+     * @param string $message
+     */
+    protected function assertArrayMapIncludes(array $mapToInclude, array $mapToCheck, string $message = '') : void
+    {
+        $passed = true;
+
+        foreach ($mapToInclude as $key => $value) {
+            if (!isset($mapToCheck[$key]) || $mapToCheck[$key] !== $mapToInclude[$key]) {
+                $passed = false;
+            }
+        }
+
+        $toIncludeStr = print_r($mapToInclude, true);
+        $toCheckStr = print_r($mapToCheck, true);
+        self::assertThat($passed, self::isTrue(), "Failed asserting that given map:\n\n{$toCheckStr}\n\nincludes:\n\n{$toIncludeStr}");
+    }
+
 }
\ No newline at end of file
index 967915af97a8560ef7310fd86853aec01afbe2cf..c84305ad88112354a418f6e9c01a58cded82b9eb 100644 (file)
@@ -12,22 +12,18 @@ class ConfigTest extends TestCase
 
     public function test_filesystem_images_falls_back_to_storage_type_var()
     {
-        putenv('STORAGE_TYPE=local_secure');
-
-        $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', 's3', 'filesystems.images', 's3');
-        $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', null, 'filesystems.images', 'local_secure');
-
-        putenv('STORAGE_TYPE=local');
+        $this->runWithEnv('STORAGE_TYPE', 'local_secure', function() {
+            $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', 's3', 'filesystems.images', 's3');
+            $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', null, 'filesystems.images', 'local_secure');
+        });
     }
 
     public function test_filesystem_attachments_falls_back_to_storage_type_var()
     {
-        putenv('STORAGE_TYPE=local_secure');
-
-        $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', 's3', 'filesystems.attachments', 's3');
-        $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', null, 'filesystems.attachments', 'local_secure');
-
-        putenv('STORAGE_TYPE=local');
+        $this->runWithEnv('STORAGE_TYPE', 'local_secure', function() {
+            $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', 's3', 'filesystems.attachments', 's3');
+            $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', null, 'filesystems.attachments', 'local_secure');
+        });
     }
 
     public function test_app_url_blank_if_old_default_value()
@@ -49,12 +45,9 @@ class ConfigTest extends TestCase
      */
     protected function checkEnvConfigResult(string $envName, $envVal, string $configKey, string $expectedResult)
     {
-        $originalVal = getenv($envName);
-        $envString = $envName . (is_null($envVal) ? '' : '=') . ($envVal ?? '');
-        putenv($envString);
-        $this->refreshApplication();
-        $this->assertEquals($expectedResult, config($configKey));
-        putenv($envString = $envName . (empty($originalVal) ? '' : '=') . ($originalVal ?? ''));
+        $this->runWithEnv($envName, $envVal, function() use ($configKey, $expectedResult) {
+            $this->assertEquals($expectedResult, config($configKey));
+        });
     }
 
 }
\ No newline at end of file
diff --git a/tests/Unit/PageRepoTest.php b/tests/Unit/PageRepoTest.php
deleted file mode 100644 (file)
index 41e7c2f..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-namespace Tests;
-
-use BookStack\Entities\Repos\PageRepo;
-
-class PageRepoTest extends TestCase
-{
-    /**
-     * @var PageRepo $pageRepo
-     */
-    protected $pageRepo;
-
-    protected function setUp()
-    {
-        parent::setUp();
-        $this->pageRepo = app()->make(PageRepo::class);
-    }
-
-    public function test_get_page_nav_sets_correct_properties()
-    {
-        $content = '<h1 id="testa">Hello</h1><h2 id="testb">There</h2><h3 id="testc">Donkey</h3>';
-        $navMap = $this->pageRepo->getPageNav($content);
-
-        $this->assertCount(3, $navMap);
-        $this->assertArraySubset([
-            'nodeName' => 'h1',
-            'link' => '#testa',
-            'text' => 'Hello',
-            'level' => 1,
-        ], $navMap[0]);
-        $this->assertArraySubset([
-            'nodeName' => 'h2',
-            'link' => '#testb',
-            'text' => 'There',
-            'level' => 2,
-        ], $navMap[1]);
-        $this->assertArraySubset([
-            'nodeName' => 'h3',
-            'link' => '#testc',
-            'text' => 'Donkey',
-            'level' => 3,
-        ], $navMap[2]);
-    }
-
-    public function test_get_page_nav_does_not_show_empty_titles()
-    {
-        $content = '<h1 id="testa">Hello</h1><h2 id="testb">&nbsp;</h2><h3 id="testc"></h3>';
-        $navMap = $this->pageRepo->getPageNav($content);
-
-        $this->assertCount(1, $navMap);
-        $this->assertArraySubset([
-            'nodeName' => 'h1',
-            'link' => '#testa',
-            'text' => 'Hello'
-        ], $navMap[0]);
-    }
-
-    public function test_get_page_nav_shifts_headers_if_only_smaller_ones_are_used()
-    {
-        $content = '<h4 id="testa">Hello</h4><h5 id="testb">There</h5><h6 id="testc">Donkey</h6>';
-        $navMap = $this->pageRepo->getPageNav($content);
-
-        $this->assertCount(3, $navMap);
-        $this->assertArraySubset([
-            'nodeName' => 'h4',
-            'level' => 1,
-        ], $navMap[0]);
-        $this->assertArraySubset([
-            'nodeName' => 'h5',
-            'level' => 2,
-        ], $navMap[1]);
-        $this->assertArraySubset([
-            'nodeName' => 'h6',
-            'level' => 3,
-        ], $navMap[2]);
-    }
-
-}
\ No newline at end of file
index 1667f5f7bab683c26b32a01ce9b4434e7679bed7..c2386443c6fdacd9260cc8b5bfb7777c74a5c208 100644 (file)
@@ -16,18 +16,16 @@ class UrlTest extends TestCase
 
     public function test_url_helper_takes_custom_url_into_account()
     {
-        putenv('APP_URL=http://example.com/bookstack');
-        $this->refreshApplication();
-        $this->assertEquals('http://example.com/bookstack/books', url('/books'));
-        putenv('APP_URL=');
+        $this->runWithEnv('APP_URL', 'http://example.com/bookstack', function() {
+            $this->assertEquals('http://example.com/bookstack/books', url('/books'));
+        });
     }
 
     public function test_url_helper_sets_correct_scheme_even_when_request_scheme_is_different()
     {
-        putenv('APP_URL=https://example.com/');
-        $this->refreshApplication();
-        $this->get('http://example.com/login')->assertSee('https://example.com/dist/styles.css');
-        putenv('APP_URL=');
+        $this->runWithEnv('APP_URL', 'https://example.com/', function() {
+            $this->get('http://example.com/login')->assertSee('https://example.com/dist/styles.css');
+        });
     }
 
 }
\ No newline at end of file
index 35ffda821ef35262f1b135fcde4c8cbc90c7a519..12b254d00a4c84ac87877d8e3e49917163f7c071 100644 (file)
@@ -78,7 +78,7 @@ class AttachmentTest extends TestCase
         $upload->assertStatus(200);
 
         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
-        $this->assertNotContains($fileName, $attachment->path);
+        $this->assertStringNotContainsString($fileName, $attachment->path);
         $this->assertStringEndsWith('.txt', $attachment->path);
     }
 
@@ -223,7 +223,7 @@ class AttachmentTest extends TestCase
     {
         $admin = $this->getAdmin();
         $viewer = $this->getViewer();
-        $page = Page::first();
+        $page = Page::first(); /** @var Page $page */
 
         $this->actingAs($admin);
         $fileName = 'permission_test.txt';
@@ -233,7 +233,7 @@ class AttachmentTest extends TestCase
         $page->restricted = true;
         $page->permissions()->delete();
         $page->save();
-        $this->app[PermissionService::class]->buildJointPermissionsForEntity($page);
+        $page->rebuildPermissions();
         $page->load('jointPermissions');
 
         $this->actingAs($viewer);
index f9265337889c94ef7f049159e92da6bb3aada780..0615a95ce52641cff442ce913d59bb65f812d2b3 100644 (file)
@@ -4,6 +4,7 @@ use BookStack\Entities\Repos\PageRepo;
 use BookStack\Uploads\Image;
 use BookStack\Entities\Page;
 use BookStack\Uploads\ImageService;
+use Illuminate\Support\Str;
 use Tests\TestCase;
 
 class ImageTest extends TestCase
@@ -43,7 +44,7 @@ class ImageTest extends TestCase
         $imgDetails = $this->uploadGalleryImage();
         $image = Image::query()->first();
 
-        $newName = str_random();
+        $newName = Str::random();
         $update = $this->put('/images/' . $image->id, ['name' => $newName]);
         $update->assertSuccessful();
         $update->assertJson([
@@ -89,7 +90,7 @@ class ImageTest extends TestCase
         $searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
         $searchHitRequest->assertSuccessful()->assertJson($resultJson);
 
-        $namePartial = str_random(16);
+        $namePartial = Str::random(16);
         $searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
         $searchHitRequest->assertSuccessful()->assertExactJson($emptyJson);
     }
@@ -208,7 +209,7 @@ class ImageTest extends TestCase
 
         $encodedImageContent = base64_encode(file_get_contents($expectedPath));
         $export = $this->get($page->getUrl('/export/html'));
-        $this->assertTrue(str_contains($export->getContent(), $encodedImageContent), 'Uploaded image in export content');
+        $this->assertTrue(strpos($export->getContent(), $encodedImageContent) !== false, 'Uploaded image in export content');
 
         if (file_exists($expectedPath)) {
             unlink($expectedPath);
@@ -366,7 +367,7 @@ class ImageTest extends TestCase
         $image = Image::where('type', '=', 'gallery')->first();
 
         $pageRepo = app(PageRepo::class);
-        $pageRepo->updatePage($page, $page->book_id, [
+        $pageRepo->update($page, [
             'name' => $page->name,
             'html' => $page->html . "<img src=\"{$image->url}\">",
             'summary' => ''
@@ -378,7 +379,7 @@ class ImageTest extends TestCase
         $this->assertCount(0, $toDelete);
 
         // Save a revision of our page without the image;
-        $pageRepo->updatePage($page, $page->book_id, [
+        $pageRepo->update($page, [
             'name' => $page->name,
             'html' => "<p>Hello</p>",
             'summary' => ''
index a7c7505a80db83867bdfb9c1636653589ccbe46a..fc1a529ae43076f59d38d2d642c534c0ff816cd7 100644 (file)
@@ -4,7 +4,7 @@ class UserProfileTest extends BrowserKitTest
 {
     protected $user;
 
-    public function setUp()
+    public function setUp(): void
     {
         parent::setUp();
         $this->user = \BookStack\Auth\User::all()->last();
diff --git a/version b/version
index 4494802a4fd0dc42714e3068afaba8ef2e351a31..d400403770b07fdcd3eb850c6b28c62b7c1a5d83 100644 (file)
--- a/version
+++ b/version
@@ -1 +1 @@
-v0.27-dev
+v0.28-dev
index 78b679a0a82f51fab622fb35ff60c0dece578119..e496340c46c63a4409d3b51702a3de98c54f7459 100644 (file)
@@ -7,10 +7,10 @@ const config = {
     target: 'web',
     mode: dev? 'development' : 'production',
     entry: {
-        app: './resources/assets/js/index.js',
-        styles: './resources/assets/sass/styles.scss',
-        "export-styles": './resources/assets/sass/export-styles.scss',
-        "print-styles": './resources/assets/sass/print-styles.scss',
+        app: './resources/js/index.js',
+        styles: './resources/sass/styles.scss',
+        "export-styles": './resources/sass/export-styles.scss',
+        "print-styles": './resources/sass/print-styles.scss',
     },
     output: {
         filename: '[name].js',